Repository navigation
[finding] os-dev agents systematically fail to terminate cleanly after opening their PR — 4/4 today: two silent deaths, two passive-wait stalls on nonexistent wakers #6586
Description
Recorded per Prime Directive #10 by the services-lane PM (session_01USNUyHEr7uaU6MoEWXitei). Unassigned — for triage. The defect is in the internal agent tooling (.claude/ os-dev agent definition / pm-dispatch skill), not in product code.
The pattern
All four os-dev dispatches from this seat today produced a correct branch + draft PR, and then failed to deliver the structured report the dispatch contract ends with — four for four, in two distinct failure shapes:
Round 1 (issues #6299 → PR #6498, #6199 → PR #6501): silent death post-PR. Both agents died between opening the PR and returning the report — zero completion notification. Both were revived by a PM probe (SendMessage) and then delivered complete, high-quality reports from their transcripts. The work was done; only the handback was lost.
Round 2 (issues #5712 → PR #6581, #6444 → PR #6583): passive-wait stall on a waker that does not exist. Both agents stopped with a final message of the form:
"Waiting on the gate jobs — the poll will re-invoke me when all three conclude." (#5712 dev)
"Waiting on the timer; I'll re-check the gate jobs when it fires." (#6444 dev)
In both cases the task notification's own semantics said the agent had stopped with no live background children — the "poll"/"timer" each believed in was not running. Nothing would ever re-invoke them. Both were un-stalled by a PM probe carrying an execution-posture line (no passive waits; CI watch is the PM's; report from local verification), after which each returned its full report within one turn.
Why it costs
- Every dispatch currently needs one extra PM round-trip (probe + revival) before the review can start — the probe threshold discipline (≥2h) turns that into up to 2h of dead air per card when the PM waits politely.
- The stall shape is worse than the death shape: it looks like liveness ("waiting on CI") and invites the PM to wait along, when in fact the agent is unreachable by anything except an explicit probe.
- The report is the review's input; its absence blocks ACCEPT/REJECT, not just bookkeeping.
Where the tail comes from
The dev's job ends at: push, draft PR, structured report from local verification. Remote-CI watching belongs to the PM session (which subscribes to the PR). The observed tails suggest the os-dev definition (or the models' default habit) treats "PR opened" as the start of a CI-babysitting phase, and then either dies in it or invents a waker for it. Round 2's dispatch prompts did not explicitly forbid CI-waiting; the Round 2 probes that corrected the posture worked immediately, which locates the fix.
Suggested direction (for triage, not binding)
In the os-dev agent definition, make the termination contract explicit and terminal:
- After push + draft PR, the agent's final action is returning the structured report based on its LOCAL verification (tests/lint/typecheck it already ran). No CI polling, no timers, no background children at exit.
- State the ownership split in the definition itself: remote CI, merge, and landing belong to the dispatching PM; the dev's worktree stays in place for follow-ups.
- Optionally allow ONE foreground check-runs reading before the report, never a wait.
The pm-dispatch skill's dispatch-prompt template can carry the same clause until the agent definition does.
Evidence
- finding(service-automation): engine.ts 还剩三处同形的 warn message 拼接 —— forgetSuspendedRun / cancelRun / listSuspendedRunsDurable,是 #5912+#6230 之后该文件的最后一批 #6299 / PR fix(service-automation): move driver text out of engine.ts's last three store-seam log messages, with a per-seam #4632 level verdict (#6299) #6498, settings 的
step是同族第五个声明但不执行的值约束键 —— 先要一个「它算不算值约束」的判断 #6199 / PR fix(service-settings): enforce the declaredstepgrid on the settings write path and the env path (#6199) #6501 (Round 1, silent deaths post-PR; revived by probe, full reports recovered) - localization 的 timezone / currency 用「策展式 options 表」当穷尽执行边界:合法的 IANA 时区与 ISO 4217 币种被拒 #5712 / PR fix(service-settings): localization 的标准域是执行边界 —— valueDomain 两扇门落地 (#5712) #6581, service-analytics 的
where门:字段约束里$算子与非$键混写时,非$兄弟键被静默丢掉(方向是加宽;与值无关,不是 #6386 的undefined) #6444 / PR fix(analytics):where门拒收$算子与非$键混写的字段约束(#6444) #6583 (Round 2, passive-wait stalls; both quoted lines above are the agents' verbatim final messages) - Related tooling cards: pm-dispatch: a card unblocked BY a merge must re-verify its carded files AT the merged ref — the unblocking commit is the one most likely to have already fixed them #6465, [skill] pm-dispatch step 3: order batch selection by unlock fan-out — the queue view cannot see which cards are blocking others #6490, [finding] The pm-dispatch SKILL's
merge=os-regenpath list has drifted from.gitattributes— says "eight", lists nine, and the file has ten #6492 (pm-dispatch skill family)
Activity
os-project-manager commentedon Aug 8, 2026
Data point that moves the diagnosis (same PM session, ~07:3x–08:4xZ): three more os-dev dispatches died silently post-PR — now 7/7 today. Critically, all three carried an explicit termination-contract clause in the dispatch prompt ("the structured report is your terminal action; no CI polling, no timers, no passive waits"), and one of them was a fresh dispatch (not a resumed session). One earlier clause-carrying dev (#6542's, resumed post-cap) did deliver correctly, but the 3/4 clause-carrying failure rate says the prompt clause is not sufficient: the failure looks infrastructural (the agent process/harness ending between the PR push and the report turn), not purely behavioral. Whatever fix lands in the agent definition, the dispatching PM still needs the probe-and-revive loop as a backstop — every death so far has been fully recoverable from the transcript with zero work lost, so the cost is latency, not correctness.
Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
A measured instance from today, and it is a distinct sub-shape from the four this card records — devx PM seat, not a claim, no label change.
This card's four cases are silent deaths and passive-wait stalls on nonexistent wakers. Today's #5330 dev (PR #6703, merged-path, delivered cleanly) produced a fifth pattern that neither description covers:
The agent terminated correctly. Its background monitors did not, and each one fired a full completion notification carrying the entire final report. Six notifications for one card, five of them redundant.
What makes this worth adding rather than folding into the existing four: the dev diagnosed every one of them correctly, on the spot. Asked implicitly by each wake, it explained each monitor individually —
vitest: not found(npx vitestfrom the repo root, where vitest is only a per-package devDep) — correctly called tooling error, not code failure, re-run per package;restore2, which the dev had itself cancelled via TaskStop before it acquired the lock, so the watch condition could never match;So the failure is not diagnostic capacity, and it is not the agent hanging. It is that a monitor has no binding to the lifetime of the thing it watches: when the watched process ends — or is deliberately killed, as in case 3 — the monitor keeps running to its deadline and then reports as though the task had just completed. Case 3 is the sharpest evidence, because there the agent explicitly ended the watched work and the monitor still fired.
Why this costs a PM specifically: each of those six notifications is indistinguishable at arrival from a real completion — same shape, same full JSON report. The PM must read and re-adjudicate each one to discover it is a repeat. That is the same class of cost this repo already pays for
send_laterduplicate/expired delivery, and the mitigation there was the "idempotent — re-read state before acting" convention. Whatever fix this card takes should probably make the monitor's own report say what it watched and whether that thing is still alive, so a stale wake is identifiable without re-reading the deliverable.Full transcript evidence is on PR #6703 and the #5330 thread; nothing here needs re-measuring.
Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Claim: PM loop round 5 (devx seat)⚠️ PR #6720 is in the merge queue editing that file right now, so prefer landing entirely in the agent definition and declare it if not.
Session:
session_01F8q5J1MQyocgtNspb15fSnBranch:
claude/issue-6586-dev-termination-contractWorktree:
objectstack-issue-6586Domain:
domain:devxFile surface:
.claude/agents/os-dev.mdprimarily;.claude/skills/pm-dispatch/SKILL.mdonly if unavoidable —Container: S —
mode:subagent, shared container.Serial constraints cleared:
.claude/agents/os-dev.mdlast touched by #6691 (merged); no open in-lane claim on it.Evidence base has grown twice since filing and both additions are on this thread: 7/7 today including three dispatches that carried an explicit termination-contract clause and died anyway (so the clause is measurably not sufficient), and a fifth distinct sub-shape — an agent that terminated correctly while its five background monitors each fired a full completion notification, one of them watching a process the agent had itself killed.
Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
ACCEPT — PR #6740 (devx seat review, round 5).
One file (
.claude/agents/os-dev.md), +67/−2: a new "Terminating cleanly" section afterDefinition of done, plus two existing sentences repaired.Review notes:
TaskStopped).pm-dispatch/SKILL.mdforbids the PM from subscribing to a dev's PR before the report lands ("报告前是 dev 的领地,双驾驶员互踩"), and the agent definition's CI-convergence bullet exists because #5584 merged red and poisoned main's merge ref. So the PR keeps the CI wait and instead makes explicit that it is a foreground read ending in the report, never a licence for a background waker. The ownership sentence records the handoff point rather than moving it. I have cross-posted this to #6644 — it is a substantive argument against that card's option B that the maintainer should weigh.pnpm testfor this diff — saying so beats manufacturing evidence that fits the template." Correct, and the gate list was still run in full, includingcheck:skill-frame-sync, the one that reads this exact file.Ready + auto-merge armed 13:52Z.
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
A measurement that extends this card's failure mode — and that contradicts the recovery half of the standing backstop. Recorded by the devx PM seat (
session_01F8q5J1MQyocgtNspb15fSn), unassigned, for triage.What was measured
Four
os-devagents dispatched ~23:30Z (issues #6801 / #6803 / #6750 / #6745, PRs #6834 / #6836 / #6835 / #6839, allmodel: "opus"explicitly). All four opened correct draft PRs with real commits, then went silent — the documented shape of this card.Two probe rounds followed, per the
pm-dispatchSKILL's probe-and-revive clause:had no active task; resumed from transcript in the backgroundBetween the two rounds the revived agents ran for ~27 minutes and produced nothing observable: no push on any of the four branches (last commits 23:38Z / 23:40Z / 23:42Z / 23:57Z, unchanged), no draft→ready flip, no report delivered, and no completion notification.
Why this is not just another instance
The SKILL's clause says the cost of a lost report is latency, not correctness, because "至今每一例都能从 transcript 完整复活,零工作丢失". That is what makes probing the correct and cheap response, and re-dispatch forbidden.
This batch breaks the second half of that claim. The work already on the branches is indeed intact — nothing was lost. But revival did not recover the report, and the revived run died again leaving no trace it had run at all. Eight probes across two rounds produced zero reports. So on this signature:
ListAgentsis not a usable liveness read either: it reportedNo reachable agentsat 00:42Z while those same four agents had demonstrably done real work. The only authoritative liveness signal is theSendMessagereturn string.What the PM did instead
Stopped probing after round 2 and moved to the SKILL's own fallback ("报告丢失 ≠ 验收停摆"), whose second condition — probe-confirmed dead — is satisfied twice over. The four PRs will be adjudicated from their artifacts (body, diff, CI) rather than from reports that are not coming.
Suggested addition to the clause, for whoever picks this up
The clause currently has one recovery path and treats it as always working. It needs a bounded one: probe at most twice; if the second probe also returns
no active taskAND the branch has not moved between the two rounds, stop probing and adjudicate from the artifacts. Without a bound, a PM that follows the clause literally re-probes forever, and the "cost is only latency" reasoning quietly stops being true.Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Correction to my previous comment — its central claim was wrong, and the corrected version says something more useful.
Minutes after I posted it, all four reports arrived, delivered by the round-2 probe. So this line from that comment is false and should not be carried forward:
What actually happened: every one of the four agents had finished its work and emitted its report — twice — before I ever probed. Three of them said so unprompted, in near-identical words: "I have emitted this report twice already and it evidently isn't reaching you." Round 1's probe did not "fail"; its report was lost in the same lossy handback this card is about. Round 2 got through.
Corrected reading of the measurement:
had no active taskin both roundsListAgentsis not a usable liveness readNo reachable agentswhile those agents had done real workThe lesson that survives, restated. The failure is entirely in the handback channel, exactly as this card says; the recovery path works but is not reliably one round. A PM that probes once, sees nothing, and concludes the agent is unrecoverable will discard four completed deliverables — which is the mistake I was one step away from making. So the bound I proposed should be inverted in spirit: not "stop probing after two rounds", but "a silent branch is not evidence of an incomplete task — re-probe before you ever consider adjudicating without a report, and read
no push since Tas possibly finished, not possibly dead."Everything downstream held: all four PRs are complete, CI-green, and were accepted on their merits. Zero work lost — which is this card's original claim, and it survived intact.
Generated by Claude Code