fix: recover Claude away-mode digest delivery on Herdr - #96
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix away-mode digest injection wedging on a Claude primary running on Herdr. Reproduce live first in a named isolated Herdr lab through the real native /afk path; capture ANSI rows and daemon sub-causes; verify current main delivery and reconstruct Incident B to identify the exact pre-PR-94 matched row, treating Incident C as the same family unless evidence differs. Separate trigger, masking condition, and symptom, and prove the smallest row or footer-token counterfactual. Keep one fleet-wide Claude classifier/composer-boundary owner and preserve genuine-turn rendered-busy deferral plus bright human composer pending deferral; every deferral must log native-busy, rendered-busy with the matched row, or composer verdict. Add narrow fail-safe post-max-alarm recovery: only after a rendered-busy matched row is byte-identical for N polls while native state is not working or working only because of the tracked background shell, re-read the composer and proceed once only when affirmatively empty, never pending or unknown. Add portable regressions for exact Incident B bypass-permissions, agent-count, Update installed, dim suggestion, and dark truecolor ghost rows as idle while genuine spinner and esc-to-interrupt rows remain busy. Extend and run the live opt-in guard to prove exactly one idle post-/afk delivery, real foreground-turn deferral, and typed-text deferral against installed Claude and Herdr; update both verification docs with dated exact commands, versions, and outputs; run lint and tests. Keep one fix per PR. A separately observed prompt-submit-to-spinner delivery race is an independent follow-up recorded in the report and excluded from this PR. Cap review-gate rounds at two; residual findings become PR follow-ups.
What Changed
Risk Assessment
🚨 High: Captain, the post-alarm recovery can send without the required raw-row stability proof, while the change also regresses generic ANSI matching and leaves a required verification record non-reproducible.
Testing
Fresh portable composer, daemon, and Herdr backend behavior suites passed. The real opt-in Herdr 0.8.2 + Claude Code 2.1.251 guard passed through native
/afk, including exactly-once idle delivery, foreground rendered-busy deferral with native-state and matched-row evidence, and bright typed-text deferral. The 75-line ANSI transcript is stored at the evidence path above; the worktree is clean. Lint/static-analysis and the full repository suite were not run because this assigned phase explicitly forbids them.Evidence: Herdr + Claude live away-mode guard
Source: Herdr + Claude live away-mode guard
Live guard exited 0 and exercised native Claude/afkin isolated Herdr sessionfm-lab-fm-afk-inject-we-86841-4718, proving idle delivery, rendered-busy deferral, matched ANSI rows, and pending human-text preservation.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-composer-lib.sh:382- The active-context classifier hardcodes styled=1, but Herdr call sites pass the plain fm_backend_capture result. A real dimPress up to edit queued messageshint is then classified as pending; the early return skips the spinner check, pane_is_busy reports idle, and inject_msg can send during a genuine foreground turn. Use an ANSI capture or pass the actual capture capabilities.bin/fm-supervise-daemon.sh:688- The intent permits recovery only while native state is not working or is working solely because of the tracked background shell. This code treats nativeunknownas eligible because it checks the terminal record only for the literalworking; a transient agent.get failure plus a stable rendered row and empty composer can therefore send. Please confirm whetherunknownis authorized, or keep it ineligible.tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:227- The broad matcher stores its row in FM_BUSY_MATCHED_ROW, but the evidence line prints FM_PANE_BUSY_MATCHED_ROW. For the idle Incident-B case that value is empty, so the exact pre-PR-94 matched row is not captured.tests/fm-composer-lib.test.sh:709- The Incident-B matrix accepts any nonzero return code, sounknown(rc=2) passes as idle. That can leave pane_is_busy deferring while the regression still passes; assert the required idle verdict rc=1.docs/verification/supervision.md:210- The required report record is incomplete: the updated verification section does not identify the exact pre-PR-94 matched row, state whether Incident C is the same family, or record the separately observed prompt-submit-to-spinner race as an excluded follow-up. It only gives a generic footer fragment and one spinner example.docs/verification/supervision.md:218- The documented live command hardcodes/Users/ivan/Projects/firstmate/bin/fm-herdr-lab.sh, which can run a helper from another checkout while the test sources the current worktree. Use the current checkout helper or record the external helper's exact version and hash.🔧 Fix: Harden Claude away-mode capture and recovery guards
4 issues (2 errors, 2 warnings) still open:
bin/fm-composer-lib.sh:398-FM_CLAUDE_BUSY_MATCHED_ROWis derived after ANSI stripping and normalization, then compared across polls by recovery. Two captures with identical visible text but different rendered bytes (for example bright busy versus dim ghost styling) can therefore unlock delivery. This contradicts the required criterion: “only after a rendered-busy matched row is byte-identical for N polls.” Propagate the raw matched row or fail closed when raw bytes are unavailable.bin/backends/herdr.sh:2698- The new ANSI-first Herdr path passes raw ANSI rows to the generic matcher. An anchored custom matcher such as^⏵⏵matched the previous plain capture but fails when the row begins with SGR bytes, causing a genuinely busy non-Claude pane to be reported idle. Strip ANSI before generic matching or keep ANSI capture scoped to Claude.bin/fm-composer-lib.sh:394- The active-composer exception matchesPress up to edit queued messageswithout verifying that the row is dim or ghosted. A bright human draft such asPlease explain Press up to edit queued messages, alongside a real tool/spinner row, is reported as rendered-busy instead of composer-pending, contrary to preserving bright human composer pending deferral. Require the hint's rendered styling or otherwise distinguish the system hint from typed text.docs/verification/runtime-backends.md:644- The second verification record still invokes/Users/ivan/Projects/firstmate/bin/fm-herdr-lab.shfrom another checkout and records only generic proof prose rather than the exact observed output. This contradicts the required criterion to “update both verification docs with dated exact commands, versions, and outputs” and is not reproducible from this worktree. Use$(git rev-parse --show-toplevel)/bin/fm-herdr-lab.shand include the exact captured output here.✅ **Test** - passed
✅ No issues found.
herdr --versionandclaude --versionbash tests/fm-composer-lib.test.shbash tests/fm-daemon.test.shbash tests/fm-backend-herdr.test.shHERDR_LAB_HELPER="$(git rev-parse --show-toplevel)/bin/fm-herdr-lab.sh" FM_AFK_HERDR_CLAUDE_LIVE=1 tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.shEvidence existence/tail check andgit status --short --untracked-files=all✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: Quote native-unknown recovery subcause; fm-lint passes
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.