Skip to content

fix: gate secondmate stall alerts on delivery progress - #3107

Open
zachlandes wants to merge 11 commits into
kunchenguid:mainfrom
zachlandes:fm/stall-detector-false-positive
Open

fix: gate secondmate stall alerts on delivery progress#3107
zachlandes wants to merge 11 commits into
kunchenguid:mainfrom
zachlandes:fm/stall-detector-false-positive

Conversation

@zachlandes

@zachlandes zachlandes commented Aug 26, 2026

Copy link
Copy Markdown

Intent

The primary watcher decided a secondmate's wake loop had stalled using the age of a single row: if the oldest row in that mate's durable wake queue had sat longer than FM_SECONDMATE_WAKE_STALL_SECS, it raised a stall alert. Age alone does not mean stalled. A mate that is steadily delivering actionable wakes still has an oldest row that keeps getting older, so healthy homes produced stall alerts that were simply wrong, and the alert lost its meaning.

This gates the alert on evidence of delivery instead of elapsed time. A mate now publishes a durable progress marker when its wake() transition actually delivers an actionable wake, and the primary suppresses the stall alert only when that marker proves a delivery happened at or after the aged row was enqueued. Covering progress suppresses the alert at any row age; an empty or younger queue stays silent as before.

The marker is committed causally rather than optimistically, which is where most of the diff goes. The transition takes bounded queue and progress locks, publishes a short-lived in-flight record before its bounded output, and commits the monotonic timestamp and authoritative queue-sequence watermark only after direct output succeeds, or the arm/checkpoint wrapper confirms it forwarded unchanged, or the away-mode daemon confirms durable handling. Serialization or forwarding failure leaves the progress unusable. The effect is that a clock change cannot reorder causality, and an absent, unreadable, malformed, future-dated, unconfirmed, abandoned, or lower-watermark marker never suppresses an alert - every one of those still alerts.

The deliberate cost: a home whose watcher polls but whose handling turn never begins is no longer alerted by the parent. That case is now invisible to this detector. It is accepted because the previous behaviour only caught it by also crying wolf on every healthy mate, which is what made the alert ignorable in practice.

Worth knowing before touching this: the primary only ever reads the foreign marker and queue. It never locks, consumes, or rewrites another home's wake queue, and the tests pin byte-for-byte preservation of foreign rows.

Verified by exercising the real watcher, checkpoint, and away-daemon interfaces end to end rather than by stubbing them - covering progress suppressed the false alert, while malformed, future-dated, unforwarded, abandoned, and pre-durable-crash progress each still produced one. The transcript is linked under Testing below.

Written with AI assistance and reviewed by the submitter before opening.

What Changed

  • Publish causal, queue-watermarked progress when actionable wakes are successfully delivered or confirmed by watcher wrappers and away-mode supervision.
  • Suppress secondmate wake-loop stall alerts only when valid committed or live in-flight progress covers the oldest queued row.
  • Document and test delivery confirmation, malformed or abandoned progress, clock changes, and lock-contention fallbacks.

Risk Assessment

✅ Low: The target preserves both reviewed and upstream ancestry, and the sole merge conflict is correctly resolved as the union of two independent documentation edits with no substantiated source regression.

Testing

Verified the merge descends from both the reviewed head and current upstream main, confirmed the sole conflict resolution preserves both documentation edits, and exercised the real watcher/checkpoint/daemon interfaces end-to-end: covering causal progress suppressed the false alert, while malformed, future-dated, unforwarded, abandoned, and pre-durable-crash progress still produced stall alerts. Focused tests succeeded and transient worktree files were removed.

Evidence: End-to-end stall detector behavior transcript

Source: End-to-end stall detector behavior transcript

=== Delivered actionable wake ===
check: sustained handoff
=== Covered aged row (expected: no stall alert) ===
<no stall alert>
=== Malformed progress on abandoned row (expected: alert) ===
check: secondmate wake-loop stalled: mate=mate row=7 age=704s
=== Future-dated in-flight marker (expected: alert) ===
check: secondmate wake-loop stalled: mate=mate row=14 age=510s
=== Failed checkpoint forwarding (expected: alert remains) ===
check: secondmate wake-loop stalled: mate=mate row=7 age=601s
=== Direct watcher delivery ===
check: rearm-resurface
=== Direct delivery covers row (expected: no stall alert) ===
<no stall alert>
=== Away-daemon crash before durable handling (expected: alert remains) ===
check: secondmate wake-loop stalled: mate=mate row=1 age=600s
Evidence: Merge ancestry and union conflict-resolution evidence

Source: Merge ancestry and union conflict-resolution evidence

head=99ac28f42863e1b63beed3a871afa29aba1fcc2e
parents=246a22a60636251e551a9d9df08261d081fd0944 4ad8cbaeafc109a17c1af3911867b7fe9e04e801
subject=chore: merge upstream main into the stall-detector fix branch
reviewed_head_ancestor=yes
upstream_main_ancestor=yes
resolved_configuration_entries:
FM_PAUSE_RESURFACE_SECS=3600       # seconds before the watcher re-surfaces a declared external wait or verified captain-held transfer for a recheck, including a live busy pane past FM_BUSY_TURN_MAX_SECS; the away-mode daemon uses the same setting for a declared external wait or verified captain-held transfer, ageing its window against the crew's own latest status line rather than pane busy state
FM_SECONDMATE_WAKE_STALL_SECS=60   # minimum age of the oldest valid foreign wake-queue row before an endpoint-recorded local secondmate produces one durable parent wake-loop-stall notification when the mate has no causal actionable-handoff progress covering that row; covering progress suppresses the notification at any row age, and zero or invalid values use 60

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • git status --short, git log --oneline --decorate --graph -8, and targeted commit diffs
  • git merge-base --is-ancestor 246a22a60636251e551a9d9df08261d081fd0944 HEAD and git merge-base --is-ancestor 4ad8cbaeafc109a17c1af3911867b7fe9e04e801 HEAD
  • git show --remerge-diff --format= --no-ext-diff HEAD -- docs/configuration.md
  • Generated a transient runner for test_secondmate_stall_follows_actionable_handoff_progress, test_secondmate_unforwarded_checkpoint_does_not_commit_progress, and test_secondmate_away_daemon_crash_does_not_commit_progress, then removed it
  • Repeated the focused executable scenarios while capturing delivered wakes, suppressed false positives, and retained true-positive alerts into reviewer-visible evidence
  • Verified git status --short after cleanup
✅ **Document** - passed

✅ No issues found.

🔧 **Lint** - 1 issue found → auto-fixed ✅
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: Confirm lint passes with pinned tooling
✅ Re-checked - no issues remain.

✅ **Push** - passed

✅ No issues found.

* The stall detector's only test was the arrival age of the oldest
  unacknowledged row in a mate's foreign wake queue. Under the auto-arm
  model that mate's watcher appends at a turn boundary and the rows
  clear only once that turn's handling acknowledges them, so the age
  measured the mate's current turn rather than an abandoned queue, and
  alerted on every turn that outlived the threshold. One healthy mate
  produced 17 such alerts over two days, at ages from 63s to 506s.
* No queue-derived measurement could separate the two cases, because
  every one of them is bounded by turn length and turn length is
  unbounded. Raising the threshold would only have traded the false
  positive for a later true positive, so the signal had to come from
  outside the queue.
* Gated the existing alert on the mate's own liveness beacon, which its
  watcher touches on every poll and which this repo already trusts as
  the one record of a home's supervision being alive. Reading it only
  to suppress cannot blind a genuinely unserved queue, since a home
  nothing polls has a stale beacon under every supervision model. At
  each of the four confirmed false positives the mate's beacon was well
  inside the supervision grace.
* Suppressed before any marker or receipt write, so a row held while
  the loop was alive still notifies if that loop later lapses.

Claude-Session: https://claude.ai/code/session_01ULP6pSaTbTPgmw9ZreF9kF
@zachlandes
zachlandes force-pushed the fm/stall-detector-false-positive branch from e50baa3 to 246a22a Compare August 26, 2026 10:23
@greptile-apps

greptile-apps Bot commented Aug 26, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The PR does not appear safe to merge until progress suppression respects row-level actor ownership.

A branch can acknowledge only a newer eligible row while the committed global sequence watermark also covers an older main-owned row; the detector then suppresses that older row indefinitely even though it was never handled.

Files Needing Attention: bin/fm-push-transition-lib.sh, bin/fm-watch.sh

Reviews (2): Last reviewed commit: "no-mistakes(document): Clarify secondmat..." | Re-trigger Greptile

Comment thread bin/fm-watch.sh
Comment on lines +459 to +463
if [ -z "$extra" ] && [ "$progress_seq" -ge "$seq" ]; then
case "$progress_state" in
committed)
continue
;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Global watermark masks rows

When a Pi branch handles an eligible row while an older main-owned row remains queued, the committed queue-wide watermark covers that excluded row and this unconditional continue suppresses its stall alert indefinitely, even though the branch actor cannot consume it.

* Refreshed the branch past upstream main so the pull request is
  mergeable again after upstream moved underneath the reviewed change.
  Merged rather than rebased so no already-pushed, already-reviewed
  commit is rewritten and no force-push is needed.
* Resolved the one conflict, in docs/configuration.md, where two
  unrelated single-line edits happened to land on adjacent lines. The
  three-way comparison showed each line was changed by exactly one
  side, so the resolution is the union: upstream keeps its new
  FM_PAUSE_RESURFACE_SECS clause, and this branch keeps its
  FM_SECONDMATE_WAKE_STALL_SECS clause. No behavior was changed.
@zachlandes zachlandes changed the title fix: prevent false secondmate wake-loop stall alerts fix: gate secondmate stall alerts on delivery progress Aug 31, 2026
@tiago-peixoto

Copy link
Copy Markdown
Contributor

We are following the maintainer's direction on #2912 to help this existing proposal rather than open a competing stall detector. Our operational evidence distinguishes three separate facts: an agent can be alive, its outer supervision can run, and its durable input can still remain unconsumed. A delivery marker therefore must be described as delivery evidence, not proof that the handling turn consumed the row.

We can supply a bounded regression scenario for a busy supervisor versus genuinely unconsumed input, preserving foreign-queue bytes and making the documented detector limit explicit. We have not re-reviewed this branch or reproduced its current head; this offer does not prescribe a second progress mechanism or change the scope the maintainer accepted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants