fix(bin): report a live worker whose composer holds unsent doorbell text as unable to receive messages - #39
Merged
Merged
Conversation
…nging behind it A steer's doorbell rung while a claude worker is mid-turn waits in claude's queue above the composer, and the composer row shows only a dim placeholder that ghost stripping removes, so the pane read empty. When that queue was stranded on an idle worker, every watcher re-ring queued another doorbell behind it and the ladder ended in a generic stale wake that left the diagnosis to reading the pane by eye. The composer classifier now reads claude's queued, unsent input as pending from either of two signals: the queued-message placeholder on the composer row, or the send-now hint above it. The doorbell ring reports text it typed that the idle composer still holds, and the re-ring ladder counts attempts that found the composer holding unsent text. When every attempt of the budget did, the watcher raises a distinct stale wake saying the worker cannot receive messages. A busy worker whose queued text will still submit is never rung into or alarmed on, and nothing is cleared automatically.
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.
Scope
This PR delivers item 2 of 2 from the intent below: a live worker whose composer holds a steer's doorbell unsent is now reported as unable to receive messages, instead of every re-ring queuing behind it.
Item 1 - a spawn that reports success on a garbled launch, and relaunch clearing a stuck shell prompt - shipped separately in #37.
Recovery stays manual as the intent requires: nothing interrupts or clears a composer automatically.
Intent
"always make sure your lanes are filled with work" (2026-09-19). Standing lane rule (2026-09-17): firstmate's own work earns a lane when "they prevent firstmate from working properly"; "I cant get work done if firstmate is not shipshape".
Two recorded defects, both the same failure: text typed into a worker's terminal pane does not land, and nothing notices, so a worker silently never starts or silently stops.
A spawn reports success on a garbled launch (recorded 2026-09-18):
2026-09-18: fm-spawn.sh reported
spawned blu-orgunit-lock-live-tenant-walkthroughbut no agent ever started. The launch command typed into the fresh tmux pane arrived garbled mid-string ("...bin/fm-operational-in" followed directly by "env -u CURSOR_AGENT ..." from the start of the command), leaving zsh at adquote cmdsubst quote>continuation prompt. A followingfm-control.sh relaunchtyped its command into that same open quote and correctly reported that no agent came up; clearing the prompt with C-c and relaunching worked.Two defects: (1) spawn reports success without confirming an agent is running - same family as fm-spawn-accepts-missing-harness, but here the harness exists and the typed command itself was corrupted; (2) neither spawn nor relaunch clears a shell continuation prompt before typing, so one bad launch poisons every retry in that endpoint.
A steer's doorbell sits queued and unsent (recorded 2026-09-18):
A steer's doorbell can sit QUEUED and unsent in a live worker's composer, and every re-ring queues
behind it, so the escalation ladder never reaches the worker. Observed 2026-09-18 on
blu-3192-app-web-consume-lock-signals.
WHAT WAS SEEN:
"Press up to edit queued messages" and "ctrl+x ctrl+s to send now".
bin/fm-control.sh interruptcleared the composer; a fresh send then landed immediately and theworker acknowledged and resumed. Nothing was lost.
WHY THE EXISTING SAFETY NET DID NOT CATCH IT. The durable-inbox design is correct and is why no
work was lost: the doorbell is explicitly NOT treated as delivery proof, and the watcher re-rings
an unacknowledged message and escalates a stuck one. But a re-ring is another doorbell typed into
the SAME composer - which is still stuck - so each retry queues behind the last. The ladder climbs
a wall. The stale alarm did eventually fire, which is how firstmate found it, but the diagnosis
came from reading the pane by eye, not from anything the system reported.
Note the asymmetry worth preserving: for a DEAD agent the watcher already reports the right thing -
"unread firstmate instruction ... the worker's agent has exited or its endpoint is missing, so the
doorbell was not typed". There is no equivalent for a LIVE agent whose composer will not accept the
submit, which is the harder and quieter case.
THE CHANGE, in outline rather than prescription: the composer classifier in bin/fm-composer-lib.sh
already distinguishes empty / pending / pending-unproven / unknown, and the away daemon already
refuses to inject into anything but a confirmed-empty composer. That knowledge is not reaching the
steer path's retry logic. A doorbell whose composer reads pending across successive attempts is a
distinct condition - text stuck, agent alive - and should be reported as that rather than retried
indefinitely, so a supervisor is told "this worker cannot receive messages" instead of inferring it.
PROOF OBLIGATIONS - each needs a mutant that reds BY NAME:
that condition, not a generic stale wake.
holds queued text that WILL submit, is not alarmed on. This is the hard part - OpenCode keeps
queued text visible while working, and fm_composer_queued_enter_verdict exists precisely because
visible text alone does not prove a swallowed Enter. Do not turn a normal busy pane into noise.
whether an automatic interrupt is safe.
RELATED, same root family: fm-supervisor-ghost-injection, merged 2026-09-18, fixed the case where
an UNDELIVERABLE escalation was typed into a composer anyway and surfaced hours later. That fix
covers the away daemon's own escalations. It does NOT cover this path - a steer's doorbell to a
worker - so do not assume it is already handled.
What Changed
bin/fm-composer-lib.shnow reads a bare claude composer aspendingwhen it shows queued input. It checks two signals: thePress up to edit queued messagesplaceholder, and thectrl+x ctrl+s to send nowhint within a few rows above the composer. Before this, ghost stripping made such a pane readempty.fm_task_inbox_ringreturns a new code 4 when the composer still holds the text after the submit on a pane that is not busy.fm_task_inbox_record_ringcounts consecutive stuck attempts in a fourth.ring-statefield.fm_task_inbox_due_actionreturns a newstuckverb when every attempt in a spent budget found the composer holding text.fm-send.shandfm-remote-secondmate-control.shprint a notice for code 4.fm-watch.shraises a distinct wake,stale: <window> (worker cannot receive messages: ...), for thestuckverb. The wake never clears or interrupts the composer. A busy worker whose queued text later submits is not reported. The shared escalation code is factored intoinbox_steer_escalate. Thestuck-crewmate-recoveryskill anddocs/verification/runtime-backends.mddocument the alarm, and the docs cover the queued-input shape verified live on claude 2.1.278. Unit and live e2e tests were added or extended intests/fm-composer-lib.test.sh,tests/fm-task-inbox.test.shandtests/fm-send-inbox-doorbell-live-e2e.test.sh.Risk Assessment
✅ Low: The change is well bounded: the composer classifier reads queued claude input as pending, the ladder tracks consecutive stuck attempts and raises a distinct stale wake, and busy panes stay skipped before any attempt is counted. I found no concrete failing sequence, but I did not run the tests and read only the source diff.
Testing
I ran the two targeted test scripts, which drive the real watcher and inbox ring against tmux panes with stub composers. Both passed with no failures. The live-harness e2e test needs real model tokens and an opt-in flag, so I did not run it. Neither test reproduces the spawn garbled-launch defect (item 1 of the intent), so that scenario is untested.
Evidence: task-inbox test output
Source: task-inbox test output
Evidence: composer-lib test output
Source: composer-lib test output
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.
bash tests/fm-composer-lib.test.sh(0 failures)bash tests/fm-task-inbox.test.sh(0 failures)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.