Skip to content

fix: prevent false wedge escalations for healthy workers - #10

Open
josh-padnick wants to merge 5 commits into
mainfrom
fm/fm-wedge-timer-grok-pause
Open

josh-padnick wants to merge 5 commits into
mainfrom
fm/fm-wedge-timer-grok-pause

Conversation

@josh-padnick

Copy link
Copy Markdown
Owner

Intent

Stop the Firstmate stale/wedge detector from falsely escalating healthy workers in both observed defect classes. First, preserve the mechanism-level diagnosis: declared-pause harness bias caused an idle Grok pane with a durable paused: status to wedge-escalate while equivalent Claude pauses used the intended long cadence, and active-turn false idle caused productive Claude xhigh-thinking panes rendering token spinners and Grok panes rendering Waiting for response… token spinners to accumulate repeated wedge escalations. Declared paused: status must select the long-cadence recheck path regardless of pane harness while allowing authoritative newly active work to supersede an older pause. Positive active-turn evidence, whether from a verified native or semantic source or Grok's isolated rendered fallback, must clear or freeze the wedge ladder through the existing fm-busy-lib.sh ownership; do not add a parallel detector, and preserve verification gates so unverified, unknown, or genuinely uncertain harness state still escalates fail-safe. Remove the invalid completed-turn-age proxy that ages a new active turn from an older completed turn. Keep Grok rendered matching structural, isolated, and live-verified with independent signals. Preserve the fail-safe direction: this targets the two proven false-positive classes, not general quieting. Add non-vacuous executable regression tests with realistic declared-pause status files and realistic captured Grok waiting-spinner and Claude thinking/token-spinner fixtures that reproduce both classes, prove the wedge timer and escalation count are cleared for active turns and declared pauses, prove idle and unknown panes still surface or escalate, and include a real-harness drift guard for unavoidable rendered signals. Update the authoritative contracts, configuration, harness guidance, verification evidence, and test-family selection without duplicating ownership. Review may perform at most two fix rounds; a third fix-round request must stop for firstmate escalation. Yolo is off: every ask-user finding must be escalated to firstmate, and the PR must not be merged without the captain's explicit approval.

What Changed

  • Route declared pauses through the long recheck cadence regardless of harness state, while allowing confirmed active work to supersede an older pause.
  • Clear wedge timers and escalation counts when verified busy signals show an active turn, and remove the completed-turn age proxy that falsely escalated productive workers.
  • Add structural Grok spinner detection, captured Claude and Grok regression fixtures, a live Grok drift guard, and updated supervision documentation and test-family routing.

Risk Assessment

🚨 High: The source review is clean, but the authoritative intent explicitly requires the captain's approval before merge, so this change must not merge automatically.

Testing

No prior baseline results were supplied. Focused classifier, watcher subprocess, test-family selection, and real Grok 1.0.5 checks passed, demonstrating that active Claude and Grok turns clear wedge state, declared Grok pauses use long cadence, settled or uncertain panes remain fail-safe, and both independent live Grok signals survive harness drift.

Evidence: Busy-state behavioral transcript

Source: Busy-state behavioral transcript

ok - arm mints a gen sidecar and seeds busy fm-spawn at seq=1
ok - apply advances seq under the armed gen and attributes the writing source
ok - firstmate-owned interrupt and recovery events bind to the current gen
ok - apply is refused for a task whose busy contract was never armed
ok - retire waits for the writer lock and cannot remove a new incarnation
ok - retire treats only an absent sidecar as already retired
ok - a late event from a previous incarnation is rejected, record unchanged
ok - a record from a stale incarnation classifies unknown, never idle
ok - a converted adapter with no record classifies unknown, never idle
ok - malformed records classify unknown malformed, never busy or idle
ok - a record with no armed gen sidecar classifies unknown
ok - a record is trusted only by the adapter whose source wrote it
ok - converted adapters never classify busy from rendered footer text
ok - the grok fallback survives either independent live signal and remains scoped to grok
ok - codex classifies unknown until a semantic source passes its verification gate
ok - standalone kimi classifies unknown until the live verification gate opens
ok - cursor classifies only from its transcript fold, never rendered text or native state
ok - endpoint death is the only process-level override and yields dead, never busy
ok - herdr's native verdict is trusted for busy only, and records outrank it
ok - record parsing never clobbers the caller's positional parameters, glob setting, or fields
ok - the boolean view reports busy only on an exact busy verdict
all fm-busy-state tests passed
Evidence: Watcher end-to-end behavioral transcript

Source: Watcher end-to-end behavioral transcript

ok - signal_reason_is_actionable: benign absorbed, captain verbs and coalesced batches surfaced
ok - stale_is_terminal: terminal status surfaces, non-terminal and no-status are benign
ok - scan_captain_relevant_statuses lists only captain-relevant statuses
ok - classifier primitives: keyed decisions and activity phases, captain relevance, window-to-task, and overrides
ok - crew_is_provably_working: only working+run-step/pane is provable; idle/finished/parked/failed/unknown surface
ok - status_is_paused: only the leading paused verb matches, and paused is not captain-relevant
ok - crew_absorb_class: working/paused/none from one read; crew_is_paused and crew_is_provably_working agree
ok - signal_crew_provably_working: benign only when every referenced crew is provably working
ok - a secondmate's status signal is never absorbed as provably working; crewmates are unaffected
ok - a no-verb signal whose crew is provably working is absorbed (no exit, no queue, suppressor advanced, beacon present)
ok - a bare turn-end whose crew is provably working (busy pane) is absorbed
ok - a bare turn-end whose crew is not provably working is surfaced (the swallowed-finish fix)
ok - a no-verb working: note whose crew is idle with no running pipeline is surfaced
ok - a secondmate's status note surfaces even while its own agent is busy
ok - a self-announced close never wakes its own home, and the next real note still does
ok - captain-relevant signal is surfaced (queue + exit) and marked surfaced
ok - a stale pane sitting on a terminal status is surfaced (queue + exit)
ok - a stale terminal-looking status is overridden and absorbed while a run is actively working, then wedge-escalated
ok - provably-working non-terminal stale is absorbed on first sight, then wedge-escalated past the threshold
ok - consecutive wedge escalations on the same pane accumulate and demand deep inspection at the threshold
ok - a pane becoming active again resets the consecutive wedge-escalation counter
ok - a not-provably-working non-terminal stale is surfaced immediately (never left to wait out the timer)
ok - a declared pause is absorbed on first sight, then re-surfaced as a recheck past the threshold, never wedge-escalated
ok - rendered Grok and semantic Claude active turns clear the wedge ladder
ok - a declared pause uses the long cadence even while the Grok process remains alive
ok - an exited captain-held worker retains its bounded recheck cadence
ok - a declared paused secondmate re-surfaces on the bounded normal-mode cadence
ok - a non-paused secondmate retains normal stale suppression
ok - a resumed secondmate clears pause and stale tracking before stale exemption
ok - unchanged stale hashes reclassify when a crew enters or leaves pause
ok - a declared pause is periodically rechecked against authoritative active-run state
ok - a paused status overridden by authoritative working preserves its wedge timer and escalates
ok - matching non-terminal stale suppressors repair missing or corrupt stale-since timers
ok - triage log capping handles wc byte counts with leading spaces
ok - a captured process-event result wakes a healthy watcher proactively, with no manual drain
ok - an unacknowledged process-event result re-drains until handling is acknowledged
ok - complete process-event queue keys map to distinct seen markers
ok - queue revalidation, proactive output, and marker commit serialize with drain
ok - surfacing failures replay until post-handling acknowledgement
ok - marker failure exits through the shared wake owner, releases its lock, and replays later
ok - a heartbeat with no captain-relevant change is absorbed and backs off the cadence
ok - heartbeat backstop fail-safe surfaces a captain-relevant status the per-wake path missed
ok - the liveness beacon stays fresh while the watcher absorbs benign wakes (fm-guard never false-alarms)
ok - with .afk present the watcher reverts to one-shot so the daemon owns triage (no double-triage)
ok - AFK changed paused panes hand off plain stale identities for daemon-owned pause triage
Evidence: Real Grok drift guard

Source: Real Grok drift guard

ok - Grok (grok 1.0.5 (5115b46bc909) [stable]): each live signal independently classifies busy, then settles idle

ok - Grok (grok 1.0.5 (5115b46bc909) [stable]): each live signal independently classifies busy, then settles idle

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (3) ✅
  • 🚨 bin/fm-busy-lib.sh:94 - Intent requires, “Keep Grok rendered matching structural, isolated, and live-verified with independent signals” and preservation of fail-safe escalation. The new alternatives are not structurally constrained: recent completed output quoting Waiting for response… 12m 07s ⇣33.4k is classified busy, clearing the wedge ladder indefinitely, while \[stop\][[:space:]]*$ does not match the realistic bordered fixture ending [stop] ... │. The live guard waits for both strings and classifies only the combined capture, so it can pass solely through the waiting-response arm. Constrain both alternatives to verified UI structure at fm_busy_grok_tail_busy, then exercise each live signal independently and verify copied footer text remains idle.
  • 🚨 docs/architecture.md:24 - Intent requires, “Declared paused: status must select the long-cadence recheck path regardless of pane harness” and “Update the authoritative contracts.” The architecture contract still says an ordinary paused: endpoint gets that cadence only after the backend confirms the agent dead, contradicting the new harness-neutral branch at fm-watch.sh:362. Update this paragraph to distinguish direct paused: handling from death-gated captain-held handling.

🔧 Fix: Constrain Grok signals and clarify pause contract
1 error still open:

  • 🚨 bin/fm-busy-lib.sh:94 - Intent requires “Keep Grok rendered matching structural.” The [stop] alternative matches any bordered line containing [stop], including ordinary Grok transcript content such as │ use [stop] to continue │ within the last 12 lines. This incorrectly returns busy grok-regex, clears the wedge ladder, and can hide an idle worker. The negative test covers only unbordered text, while the live guard derives its stop-only sample from an active footer. Constrain this arm to the verified footer structure at fm_busy_grok_tail_busy and cover bordered completed output.

🔧 Fix: Constrain Grok stop matching to verified footer
2 errors still open:

  • 🚨 bin/fm-busy-lib.sh:94 - Intent requires “Keep Grok rendered matching structural,” and the approved correction requires copied or completed footer text to remain idle. Both alternatives still accept a single bordered transcript row: │ Waiting for response… 12m 07s ⇣33.4k │ matches the waiting arm, while │ copied ⇣33.4k [stop] │ matches the stop arm. Grok transcript rows use the same │...│ framing, so completed output can return busy grok-regex and indefinitely clear the wedge ladder. After two same-theme fix rounds, stop for firstmate escalation. The matcher needs verified footer position or adjacency in fm_busy_grok_tail_busy, plus a bordered copied-footer regression.
  • 🚨 docs/verification/supervision.md:217 - Intent requires Grok signals to be “live-verified with independent signals” and verification evidence to be updated. The recorded output predates the fix-round logic that isolates each signal and no longer matches the current guard's output text (each live signal independently...). Therefore the evidence only proves the earlier combined-capture guard ran, not that the current independent arms passed against a real Grok pane. After correcting the structural matcher, rerun the current opt-in guard and record its actual versioned output.

🔧 Fix: Match Grok busy detection to live footer
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • tests/fm-busy-state.test.sh
  • tests/fm-watch-triage.test.sh
  • FM_GROK_BUSY_LIVE_E2E=1 tests/fm-grok-busy-live-e2e.test.sh
  • tests/fm-test-run.test.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

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.

1 participant