A dispatched subagent that ends its turn while waiting (typically on scripts/pm/os-verify-lock.sh) is not paused — it is dead. Nothing wakes it: no notifier fires, and a queued SendMessage does not resume it either. The dispatching seat sees a task the harness still reports as "running", and waits forever.
This cost three rounds in one shift. Filed by the domain:spec execution seat, session session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-13T08:40Z. ⛔ No domain:* or priority:* applied — routing and grading are triage's.
The reading
One instance, measured end to end today (the PR #17913 repair round):
| time (UTC) |
reading |
| 07:57:32Z |
round's last assistant text: "Now let me implement the required-reference refinement." |
| 08:05:02Z |
transcript file's last write — the round's last tool call |
| 08:24Z |
seat measures: lock is free, queue empty (so it is not lock-blocked); ps -e = 99 processes, zero node / vitest / pnpm / tsx. Control: two known-live bash PIDs (19218, 15261) both appear — the instrument is lit |
| 08:24:30Z |
seat sends SendMessage — tool returns "Message queued for delivery at its next tool round" |
| 08:37:58Z |
transcript mtime still 08:05:02Z. A 10-minute watcher polling stat confirms: the message never woke it |
| 08:38Z |
harness still lists the task as running; only an explicit TaskStop ended it |
⇒ 33 minutes of a seat's batch slot held by a corpse, and the only reason it was caught is that the seat happened to run ps with a control.
Why the obvious instruments all say the wrong thing
- The harness task list says running. It means "not reaped", not "doing work".
os-verify-lock.sh --status says free — which reads as "nothing is blocked", when the truth is "the thing you think is blocked already died". ⚠️ Two instruments, two definitions: the recurring shape.
- Transcript mtime advances on every tool call, so a long single call and a death look identical until you check the process table. The disambiguator is
ps -e for node/vitest/pnpm with a control PID, and it is not written down anywhere.
SendMessage's own contract says a send "resumes it from its transcript". Measured today: it did not.
Why the loss is bigger than one slot
The dead round's work is not lost — it sits uncommitted in its worktree. Today's had 11 modified files, +339/−97, and a finished ADR-0087 semantic migration entry as an untracked file. So the failure mode is not "work destroyed", it is "work invisible": a seat that quietly re-dispatches from scratch will either duplicate it or clobber it, and an untracked file survives neither.
What is owed (shapes, ⛔ not asserted)
- A liveness reading a seat can take in one command.
os-verify-lock.sh already knows about holders and waiters; something like --holders --with-pids that reports "ticket held by PID N — PID N does not exist" turns today's three-instrument deduction into one line. ⚠️ Must carry its own control, or a dead prober reports everything alive.
- Make the wait survivable rather than fatal. Every dispatch brief now has to carry the sentence "if you are waiting on the lock, poll inside a single Bash call — do NOT end your turn". A rule repeated in every brief is a rule that belongs in the harness or in the lock script's own waiting path, not in prose the next author may forget.
- A seat-side sweep: for each in-flight dispatch, if the transcript has not advanced in N minutes AND no matching process exists, report it rather than wait on it.
⚠️ ⛔ NOT MEASURED: why the round stopped (whether it hit the lock at all, or ended its turn for some other reason); whether SendMessage fails to wake every stopped subagent or only one in this state; and whether the other two losses this shift have the same signature — they were resumed by hand before anyone thought to take the reading.
Generated by Claude Code
A dispatched subagent that ends its turn while waiting (typically on
scripts/pm/os-verify-lock.sh) is not paused — it is dead. Nothing wakes it: no notifier fires, and a queuedSendMessagedoes not resume it either. The dispatching seat sees a task the harness still reports as "running", and waits forever.This cost three rounds in one shift. Filed by the
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-13T08:40Z. ⛔ Nodomain:*orpriority:*applied — routing and grading are triage's.The reading
One instance, measured end to end today (the PR #17913 repair round):
referencerefinement."ps -e= 99 processes, zero node / vitest / pnpm / tsx. Control: two known-live bash PIDs (19218, 15261) both appear — the instrument is litSendMessage— tool returns"Message queued for delivery at its next tool round"statconfirms: the message never woke itTaskStopended it⇒ 33 minutes of a seat's batch slot held by a corpse, and the only reason it was caught is that the seat happened to run
pswith a control.Why the obvious instruments all say the wrong thing
os-verify-lock.sh --statussays free — which reads as "nothing is blocked", when the truth is "the thing you think is blocked already died".ps -efor node/vitest/pnpm with a control PID, and it is not written down anywhere.SendMessage's own contract says a send "resumes it from its transcript". Measured today: it did not.Why the loss is bigger than one slot
The dead round's work is not lost — it sits uncommitted in its worktree. Today's had 11 modified files, +339/−97, and a finished ADR-0087 semantic migration entry as an untracked file. So the failure mode is not "work destroyed", it is "work invisible": a seat that quietly re-dispatches from scratch will either duplicate it or clobber it, and an untracked file survives neither.
What is owed (shapes, ⛔ not asserted)
os-verify-lock.shalready knows about holders and waiters; something like--holders --with-pidsthat reports "ticket held by PID N — PID N does not exist" turns today's three-instrument deduction into one line.SendMessagefails to wake every stopped subagent or only one in this state; and whether the other two losses this shift have the same signature — they were resumed by hand before anyone thought to take the reading.Generated by Claude Code