fix: deliver away-mode escalations through titled composers - #2385
Open
unhexquadium wants to merge 9 commits into
Open
fix: deliver away-mode escalations through titled composers#2385unhexquadium wants to merge 9 commits into
unhexquadium wants to merge 9 commits into
Conversation
Claude 2.1.232 draws the session's agent name as a reverse-video title inside the top rule of its own bare composer. Only a solid `─` row counted as a rule, so that title left the composer's untitled bottom rule unpaired, an unpaired rule below the candidate read as proof the candidate was stale, and cursorless selection was abandoned. Every idle claude pane classified `unknown`. The away-mode injector requires an affirmative `empty`, so it deferred every escalation while the supervisor pane sat idle; one episode buffered for 9.7 hours before the return catch-up delivered it. Steers to worker panes were unaffected because the herdr submit primitive confirms delivery from native agent-state and never reads the composer. Accept a rule that carries an embedded title, bounded exactly as _fm_composer_titled_bottom_ok already bounds a titled bottom border: rule glyphs at both ends and an ASCII-printable interior.
A wedge episode produced thousands of identical defer lines and no evidence. The defer reason names three possible causes and cannot separate them, so "the captain half-typed a line" and "this pane's rendering is not in the shape catalogue" are indistinguishable from durable state. Record the composer verdict and the captured screen in the wedge marker, once per max-defer window alongside the existing alarm, so the rendering that wedged is preserved rather than needing a live re-capture that may no longer show it. The verdict's meaning stays owned by bin/fm-composer-lib.sh.
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 the away-mode escalation delivery wedge in firstmate. While the captain was away, the sub-supervisor daemon buffered captain-relevant escalations and injected them into firstmate's own pane, but every injection deferred indefinitely - one episode sat 9.7 hours (34973s) undelivered, with 2845 defers and zero successful injections - so decision gates stalled overnight and delivery only happened via the return catch-up.
ROOT CAUSE, reproduced live on the running fleet: Claude Code 2.1.232 draws the session's agent name as a reverse-video title inside the TOP rule of its own bare composer. bin/fm-composer-lib.sh's _fm_composer_pi_separator_row only counted a row consisting of nothing but the rule glyph, so the titled top rule stopped counting as a rule, which left the composer's untitled BOTTOM rule unpaired. _fm_composer_select_cursorless treats an unpaired rule below the best candidate as proof that the candidate is stale scrollback, so it abandoned selection and every idle claude pane classified
unknown. The away-mode injector requires an affirmativeemptybefore typing, so it deferred forever: correct fail-closed behaviour applied to a wrong verdict. Steers to worker panes through fm-send.sh were unaffected the whole time, because the herdr submit primitive confirms delivery from native agent-state (agent get) and never reads the composer; the daemon's pre-injection composer guard is the only delivery check that consults the classifier.THE FIX accepts a rule carrying an embedded title, bounded exactly as _fm_composer_titled_bottom_ok already bounds a titled bottom BORDER: rule glyphs at both ends and an ASCII-printable interior. That bounded shape was chosen deliberately over a looser match so ordinary transcript prose can never be promoted into a composer boundary, and the new tests pin that boundary from both sides (prose ending in a rule, prose following a rule, and a non-ASCII title must all still fail to qualify). Verified live: the supervisor pane flips unknown -> empty, typed text still reads pending, and all 12 relevant suites pass.
DELIBERATE DECISIONS a reviewer reading only the diff would not know:
A secondary "bare-composer wrap-extension overreach" fix was in the originally accepted scope and was deliberately REMOVED after implementation disproved it. The proposed variant ("do not extend the wrap region when the pane has no cursor capability") is literally "delete the extension", because _fm_composer_select_cursorless only ever runs cursorless. Doing that turns the existing deliberate assertion in tests/fm-composer-lib.test.sh ("cursorless activity below bare row on herdr") from pending to empty - a composer holding real text classified as safe to inject into, on the exact backend the injector uses. The alternative variant required a regex over a user-configured statusLine, which cannot be built from verified evidence. The captain decided to drop it and file it as its own backlog item. Do not re-add it or any variant of it in this change.
The bin/fm-supervise-daemon.sh change is deliberate diagnostic hardening, not part of the classifier fix. The wedge produced thousands of identical defer lines and no evidence, and the defer reason names three possible causes it cannot separate. The wedge marker now records the composer verdict and the captured screen once per max-defer window, alongside the existing alarm. The verdict's meaning stays owned by bin/fm-composer-lib.sh; the daemon only reports it.
CONSTRAINTS AND KNOWN GAPS: ShellCheck was absent on this machine, so the repo-pinned 0.11.0 static binary was installed to ~/.local/bin and bin/fm-lint.sh now runs clean - lint is not being skipped. tests/fm-afk-inject-herdr-e2e.test.sh fails identically with and without this change ("the supervisor pane's shell did not become ready"); it needs a real spawned herdr pane, and is pre-existing and environmental, not a regression. tests/fm-composer-matrix-live-e2e.test.sh is the live guard that would have caught this regression, but it requires tmux, which this herdr-backed machine does not have; that is noted as follow-up and is deliberately out of scope here.
DELIVERY: this account has no write access to the upstream repo, so per the captain's decision the branch pushes to the fork unhexquadium/firstmate and the PR opens against kunchenguid/firstmate, matching how every recent PR on this repo ships. The fork is a PR staging vehicle only; nothing is ever merged into the fork itself.
What Changed
Risk Assessment
✅ Low: The private-marker fix establishes mode 0600 before writing sensitive content, preserves alarm ordering and single-snapshot diagnostics, and leaves the durable classifier fix intact.
Testing
The focused classifier, daemon, and Herdr adapter suites passed; a base-versus-target reproduction using the captured Claude 2.1.232 pane proved
unknownbecameempty, the production away-mode guard submitted exactly once while unsafe shapes still deferred, and the durable diagnostic preserved the buffer with a mode-0600 plain-text paired verdict and screen. A live spawned-pane run was not attempted because tmux is unavailable and the supplied intent identifies the real-Herdr shell-readiness failure as environmental.Evidence: Away-mode end-to-end transcript
same_claude_capture_baseline_verdict=unknown same_claude_capture_target_adapter_verdict=empty typed_draft_target_adapter_verdict=pending one_ended_prose_target_adapter_verdict=unknown non_ascii_title_target_adapter_verdict=unknown away_mode_injection_result=delivered away_mode_submit_calls=1Evidence: Plain-text durable wedge marker
composer verdict: empty captured screen: ──────────────────────── orchestrator ── ❯ ────────────────────────────────────────Evidence: Reproducible evidence harness
Evidence: Targeted test timing metadata
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/fm-supervise-daemon.sh:938- The new pane capture is appended to a marker created with ordinary shell redirection and never given restrictive permissions. With umask 022 and a traversable FM_HOME, the marker is 0644, allowing other local users to read terminal contents that may include credentials. Ensure the marker is mode 0600 before appending diagnostic evidence.🔧 Fix: Secure away-mode wedge evidence marker
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh --list --changed --base 6789876442d0fb6da9f70d86399a2930c5073ae2bin/fm-test-run.sh --json /tmp/no-mistakes-evidence/01M00KEYQHEKASD825G1EXZHAK/targeted-test-timings.json tests/fm-composer-lib.test.sh tests/fm-daemon.test.sh tests/fm-backend-herdr.test.shbash -o pipefail -c 'bash "$1" | tee "$2"' _ /tmp/no-mistakes-evidence/01M00KEYQHEKASD825G1EXZHAK/afk-wedge-e2e.sh /tmp/no-mistakes-evidence/01M00KEYQHEKASD825G1EXZHAK/afk-wedge-e2e.txtInspectedwedge-marker.txtand verified mode0600, preserved buffer, paired verdict and captured screen, and absence of ANSI escape bytes.Verified the working tree remained clean and testing left no transient project artifacts.✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.
Adjacent issue deliberately left out of scope
The review round that produced the
0600fix onstate/.subsuper-inject-wedgedraised aclass of problem, not a single instance.
state/.subsuper-escalations, written by the same daemon, holds buffered captain escalationtext and is created the same way, so under
umask 022it has the same world-readableweakness.
That one is pre-existing - it is not introduced or worsened by this branch - so it was
deliberately not fixed here rather than widening a validated change during its own pipeline
run.
It is worth its own item.