Skip to content

fix: classify wrapped idle Claude panes for away-mode delivery - #109

Merged
withally merged 7 commits into
mainfrom
fm/fm-afk-claude-herdr-unreadable-wedge-f1
Sep 3, 2026
Merged

withally merged 7 commits into
mainfrom
fm/fm-afk-claude-herdr-unreadable-wedge-f1

Conversation

@withally

@withally withally commented Sep 3, 2026 •

Copy link
Copy Markdown
Owner

Intent

Fix the away-mode defect where a Claude Code primary on Herdr, hosting the daemon as its native background bash job, has its genuinely idle pane classified unreadable and therefore receives no escalation digest. Reproduce the failure in the named isolated Herdr lab with a native background sleep loop and capture the actual wrapped Claude background-task footer. Fix shared shape handling only in bin/fm-composer-lib.sh, using bin/backends/herdr.sh only if capture or capability facts require it, with no adapter-local shape copies. A genuinely idle Claude composer with a background job must classify empty and rendered-idle, while pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer. Commit captured idle and busy ANSI fixtures and behavioral regressions. Verify end to end that one escalation digest is delivered exactly once into the idle pane within one housekeeping tick while the background job remains running. Document current behavior in docs/herdr-backend.md Away-mode supervisor support and record dated evidence in docs/verification/supervision.md. Do not change the delivered-once journal contract or wedge alarm. Keep this to one fix. The broad changed-suite Muse harness detection failure is pre-existing on unchanged main cc6793f and filed separately as fm-muse-harness-detect-test-f1; do not modify tests/fm-muse-harness.test.sh or bin/fm-harness.sh for this PR, and treat that exact failure as pre-existing if the test gate asks.

What Changed

  • Hardened shared Claude footer parsing for bounded wrapped status furniture, while unrecognized or garbled shapes remain unreadable and defer.
  • Added Claude Code 2.1.258 ANSI fixtures and composer, daemon, and live Herdr regressions covering idle background shells, active spinners, pending text, dead shells, garbled captures, and exactly-once delivery.
  • Updated Herdr/runtime supervision documentation and the documentation-audience registry with the 2026-09-03 wrapped-footer verification record.

Risk Assessment

🚨 High: The wrapped-footer path can still allow injection during an active Claude turn or wedge a genuinely idle pane.

Testing

Focused composer, daemon, and documentation tests passed. The isolated real Herdr+Claude run launched Claude 2.1.259, executed /afk, and captured the actual wrapped footer with composer=empty and rendered-idle classification, but Herdr reported agent_status=idle at the unchanged readiness gate, so the one-tick exactly-once delivery proof was not reached. Reviewer-visible terminal and classifier logs were preserved.

Evidence: Live Herdr+Claude diagnostic transcript

Source: Live Herdr+Claude diagnostic transcript

away-idle diagnostics: status=idle composer=empty rendered_busy=0
Claude pane:
/private/var/folders/3n/3wfcplrn3clf44hjjkfgq8t00000gn/T/fm-afk-herdr-claude-guard.ZUs6bK/lau
nch-claude.sh
ivan@Ivans-MacBook-Pro 01M1J6XQJJ2F2RC7KSX2GSK55T % /private/var/folders/3n/3wfcplrn3clf44hjj
kfgq8t00000gn/T/fm-afk-herdr-claude-guard.ZUs6bK/launch-claude.sh
 ▐▛███▛█   Claude Code v2.1.259
▝▜██████▀  Opus 5 (1M context) with high effort · Claude Max
  ▝▝ ▝▝    ~/…/37852af5566c/01M1J6XQJJ2F2RC7KSX2GSK55T

⚠ 1 MCP server needs authentication · run /mcp

❯ Reply with the single word ready and stop.

⏺ ready

✻ Crunched for 3s · done 9:40 AM

❯ /afk

⏺ Bash(ls bin/fm-afk-launch.sh bin/fm-afk-start.sh 2>&1 &&
      ./bin/fm-afk-launch.sh start-native 2>&1)
  ⎿  bin/fm-afk-launch.sh
     bin/fm-afk-start.sh

  Listed 1 directory (ctrl+o to expand)

⏺ Bash(./bin/fm-afk-launch.sh start-native; echo "EXIT=$?")
  ⎿  fm-afk-launch: reconciling leaked daemon terminal none:-
     EXIT=0

⏺ Bash(echo "FM_HOME=${FM_HOME:-unset}"; sed -n '1,60p'
      bin/fm-afk-launch.sh | grep -n "FM_HOME\|STATE\|afk" )
  ⎿  FM_HOME=~/.no-mistakes/worktrees/37852af5566c/
     01M1J6XQJJ2F2RC7KSX2GSK55T
     2:# fm-afk-launch.sh - the single owner of the away-mode
     … +18 lines (ctrl+o to expand)

  Searched for 1 pattern, listed 1 directory (ctrl+o to expand)

⏺ Bash(sed -n '175,210p' bin/fm-afk-launch.sh; echo ---; grep -n
      "fm_afk_launch_start_native()" -A 40 bin/fm-afk-launch.sh |
      head -60)
  ⎿    pi_session_dir=${4:-${PI_CODING_AGENT_SESSION_DIR:-}}
       if [ -n "$pi_session_dir" ]; then
         daemon_env+=" PI_CODING_AGENT_SESSION_DIR=$(printf '
     … +75 lines (ctrl+o to expand)

⏺ Bash(pwd; ls -la "$FM_HOME/state" 2>&1 | head; echo
      "OVERRIDE=${FM_STATE_OVERRIDE:-unset}")
  ⎿  ~/.no-mistakes/worktrees/37852af5566c/01M1J6XQ
     JJ2F2RC7KSX2GSK55T
     ls: ~/.no-mistakes/worktrees/37852af5566c/01M1
     … +3 lines (ctrl+o to expand)

  Listed 1 directory (ctrl+o to expand)

⏺ Bash(FM_AFK_STATE_PREPARED=1 ./bin/fm-afk-start.sh)
  ⎿  Running in the background (↓ to manage)

⏺ Captain, away mode is active; I will batch routine updates and
  surface only decisions, failures, credentials, or review-ready
  work until you return.

✻ Cogitated for 48s · done 9:41 AM · 1 shell still running

──────────────────────────────────────────────────────────────────
❯
──────────────────────────────────────────────────────────────────
  ⏵⏵ bypass permissions on · 1 shell · ← 1 agent · ↓ to manage
                                                             /rc
not ok - Herdr did not report working with an idle Claude composer after the foreground /afk turn
Evidence: Wrapped Claude classifier behavior matrix

Source: Wrapped Claude classifier behavior matrix

target=943b3aee74e61847f3420fa3e0cd7804bb64fe4c
behavior=shared Claude footer/composer execution
fixture=idle-background-narrow composer=empty footer_rc=1 matched=none
fixture=busy-background-narrow composer=empty footer_rc=0 matched=✳ Effecting… (1m 27s · ↓ 1.4k tokens)
fixture=active-spinner-wrapped-narrow composer=empty footer_rc=0 matched=✳ Effecting… (1m 27s · ↓ 1.4k tokens)
fixture=active-spinner-tip-wrapped-narrow composer=empty footer_rc=0 matched=✳ Effecting… (1m 27s · ↓ 1.4k tokens)
fixture=garbled-suffix-background-narrow composer=empty footer_rc=2 matched=none
fixture=attached-garbled-suffix-background-narrow composer=empty footer_rc=2 matched=none
- Outcome: ⚠️ 1 warning across 1 run (16m8s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 error
  • 🚨 bin/fm-composer-lib.sh:319 - Required criterion: “unreadable or garbled captures must still defer.” The new primary regex validates only the prefix and accepts arbitrary suffixes; a capture ending with ⏵⏵ bypass permissions on GARBLED followed by /rc is stripped as a valid footer, then returns idle with an empty composer, allowing injection. Require a fully verified primary shape or an unreadable result; the added garbled regression does not cover this case.
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:427 - The one-tick timer starts only after the queue wait of up to 60 seconds, and stops when the token is merely visible, before wait_for_single_delivery confirms submission. A delayed queue or Enter can therefore still report delivery-seconds=0/1; time the status creation through confirmed delivery instead.
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:437 - The live test checks native working only before publishing the escalation. Its final delivery predicate checks token/ack/composer/buffer but not that the background job is still running, so the test can pass after the native sleep exits and cannot prove the required delivery occurred while it remained active.
  • ⚠️ docs/verification/supervision.md:248 - The documented live command hardcodes ~/Projects/firstmate/bin/fm-herdr-lab.sh, so rerunning the evidence can use a different checkout than the reviewed source. Use the repository-root helper ($ROOT/bin/fm-herdr-lab.sh or git rev-parse --show-toplevel) instead.

🔧 Fix: Hardened Claude footer parsing and delivery assertions
5 issues (2 errors, 3 warnings) still open:

  • 🚨 bin/fm-composer-lib.sh:420 - Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer.” With a current spinner, empty bare composer, valid primary footer, and /rc, the new split yields screen_verdict=empty while active_hint=0 and active_tool=0; the spinner is skipped and the function returns idle, allowing injection into a foreground turn. Recognize current spinner activity in this wrapped context and defer.
  • 🚨 docs/documentation-audiences.json:432 - The newly tracked garbled-suffix-background-narrow.ansi.txt is absent from documentation-audiences.json. The audience checker classifies all tracked *.txt files and rejects any unclassified path, so this change deterministically fails that gate. Add the fixture as maintainer-verification.
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:262 - The required end-to-end proof says delivery must occur “while the background job remains running,” but line 262 checks only aggregate Herdr agent_status=working. If the native daemon exits just after submitting while Claude processes the acknowledgement, all predicates can still pass. Add an independent witness for that specific native job's liveness at confirmation.
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:415 - The one-housekeeping-tick requirement is not enforced: date +%s truncates both endpoints and DELIVERY_ELAPSED <= 2 allows delivery spanning two one-second cycles, potentially nearly three seconds, to pass. Use a monotonic subsecond bound matching the required one-second tick.
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:275 - The live footer witness independently searches pane history for a shell-status line and any standalone /rc. An earlier transcript /rc plus a current unwrapped status line can satisfy both checks, so the test can pass without reproducing an adjacent wrapped footer. Match the primary row and continuation as one bounded structure.

🔧 Fix: Harden wrapped Claude spinner and footer witnesses
3 issues (2 errors, 1 warning) still open:

  • 🚨 bin/fm-composer-lib.sh:439 - Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer.” With a valid wrapped footer, empty composer, active ✳ Effecting… row, and a non-edge tip row before the top rule, this loop overwrites the spinner with the tip. The new branch then misses busy activity and the later empty-composer path returns idle, allowing injection into the active turn. Preserve or scan every row in the preceding block for a busy match before allowing idle.
  • 🚨 bin/fm-composer-lib.sh:319 - Required criterion: “unreadable or garbled captures must still defer.” The invalid-prefix regex only recognizes on when followed by whitespace or end-of-line. A malformed status such as ⏵⏵ bypass permissions onGARBLED bypasses both the exact and invalid matches, falls into the legacy footer fallback, passes the empty-composer checks, and returns idle. Treat status-looking rows that fail the exact primary shape as unreadable before fallback.
  • ⚠️ bin/fm-composer-lib.sh:446 - The wrapped-footer branch pipes into fm_busy_lines_match, which assigns FM_BUSY_MATCHED_ROW in a pipeline subshell. The parent therefore sees an empty value and records unknown at line 448 instead of the exact matched spinner row, breaking the rendered-busy diagnostic contract. Invoke the matcher with a here-string or otherwise return the matched row without a pipeline subshell.

🔧 Fix: Prevent Claude spinner drops and malformed footer fallthrough
1 error still open:

  • 🚨 bin/fm-composer-lib.sh:432 - Required criterion: “pending composer text, an active mid-turn spinner, a dead shell, and unreadable or garbled captures must still defer,” while a genuinely idle background-job composer must be rendered-idle. The wrapped-footer branch only examines rows containing literal ✳; a wrapped footer with an empty composer and an existing supported spinner such as ✶ or ✢ therefore falls through as idle and can allow injection. Conversely, any stale ✳ spinner in the preceding transcript tail is treated as current and wedges an idle pane. Bind the full Claude spinner signature to a structurally current row before merging.
⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh:207 - The current isolated Herdr+Claude run reached the real wrapped /rc footer and observed composer=empty with rendered_busy=0, but Herdr reported agent_status=idle at the readiness gate on line 207. The test stopped before escalation delivery, so exactly-once one-tick delivery while the native job remains running is unproven. This appears to be a live-runtime/test-witness mismatch rather than a shared classifier failure; decide whether to refresh the live environment or accept the prior dated evidence.
  • bash tests/fm-composer-lib.test.sh
  • bash tests/fm-daemon.test.sh
  • bash tests/fm-documentation-audiences.test.sh
  • FM_AFK_HERDR_CLAUDE_LIVE=1 HERDR_LAB_HELPER="$(git rev-parse --show-toplevel)/bin/fm-herdr-lab.sh" tests/fm-afk-herdr-claude-busy-guard-live-e2e.test.sh
  • Direct execution of fm_claude_current_footer_busy and fm_composer_classify_screen over the six committed Claude/Herdr fixtures
  • Final HEAD, clean-worktree, evidence-presence, and lab-cleanup check
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@withally
withally merged commit dea21d1 into main Sep 3, 2026
14 checks passed
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