`fleet send` on an IDLE worker was a silent no-op: turns incremented,
status returned to idle, and compose_prompt had already drained (and
audited) the mail -- but the worker never saw it.
Cause: dispatch_bg put a byte-identical pointer in argv on every dispatch
("Read <task> and follow it exactly."). On a fork-steer the resumed
transcript already contains a Read(<task>) tool_use AND its tool_result
for that exact path, so the model satisfied the pointer from cache, never
re-read the rewritten file, and the <MANAGER MESSAGE> was dropped.
Delivery depended on the model choosing to re-issue a tool call it
believed it had already made.
Fix: _dispatch_argv_prompt() -- fresh dispatches keep the pointer
(unchanged argv, empty transcript); RESUMED dispatches inline the body,
leading with the real <MANAGER MESSAGE> block. Oversized bodies (>8 KiB)
fall back to a pointer carrying a content digest + an explicit
'it has CHANGED, re-read it' directive, which at least is not
byte-identical to the cached instruction.
The marker match is line-anchored on purpose: the preamble itself mentions
`<MANAGER MESSAGE>` inline, so a plain find() slices the preamble
mid-sentence (caught live -- a worker received a prompt starting
"\`; treat them as user instructions.").
Only the two resume_sid call sites change behaviour (_cmd_send_native
fork-steer, _resume_one_limited_native). cmd_spawn/cmd_respawn are
untouched; sup_handoff_begin is annotated exempt (fresh session +
per-incarnation unique task path).
Verified live against a LongCat-2.0 worker: before, the steer text never
appeared in the dispatched turn at all; after, it is present in the user
message. (Whether a weak model then obeys is separate -- LongCat-2.0 still
anchored on its transcript in one run.)
Tests: 2 regression tests in TestDispatchBg. Suite 1418 passed; the single
failure (test_receipts[claim-nonce.md]) is PRE-EXISTING -- verified by
stashing this change and re-running.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bug:
fleet sendon an idle worker was a silent no-opFound while live-testing fleet workers against a LongCat-2.0 backend (isolated
CLAUDE_CONFIG_DIRnamespace, perdocs/longcat-fleet-usage.md).Symptom
fleet send <worker> "<msg>"on an idle worker did nothing. No error —fleet statusshowedturnsincremented and status back toidle, andcompose_prompthad already drained and audited (mail_drained) the message. Silent data loss.Reproduced deterministically twice (steer #1 "reply PONG2", steer #2 "Ignore all previous instructions… reply BANANA" → worker replied
PONGboth times).Root cause
dispatch_bgput a byte-identical pointer in argv on every dispatch:On a fork-steer the session is resumed (
--resume), so the carried transcript already contains aRead(<that exact path>)tool_use and itstool_result. The model satisfies the identical instruction from cache, never re-reads the rewritten task file, and the<MANAGER MESSAGE>never enters context.Transcript proof (turn 2 has no
Readcall):Delivery depended on the model choosing to re-issue a tool call it believed it had already made — a volition dependency, not a mechanism. (The mid-turn hook path works precisely because it inlines text via
additionalContext.)Secondary effect: each fork re-carries the growing transcript, so input tokens climbed 268 → 12,238 across dropped steers — burning budget to accomplish nothing.
Fix
New pure helper
_dispatch_argv_prompt(task_path, prompt_body, resume_sid):resume_sidfalsy) → unchanged pointer. Empty transcript, worker must Read and does. argv is byte-identical to before, socmd_spawn/cmd_respawnare untouched.<MANAGER MESSAGE>block (the resumed transcript already carries the preamble).NATIVE_INLINE_PROMPT_MAX_BYTES= 8 KiB) → pointer + content digest + explicit "it has CHANGED, re-read it, do NOT answer from an earlier Read result". Still volition-dependent, but no longer byte-identical to the cached instruction. 8 KiB sits well under the 32,767-char Windows limit (SPEC §6 G8) and POSIXMAX_ARG_STRLEN.The marker match is line-anchored (
re.search(r"^<MANAGER MESSAGE>$", body, re.M)) on purpose: the preamble itself mentions`<MANAGER MESSAGE>`inline, so a plainfind()slices the preamble mid-sentence — caught live, a worker received a prompt starting`; treat them as user instructions.Blast radius
Only the two
resume_sidcall sites change behaviour:_cmd_send_native(fork-steer) and_resume_one_limited_native.cmd_sup_handoff_beginis annotated exempt (fresh session + per-incarnation unique task path). No signature changes;mail_drained/ claim-bracket semantics untouched.Verification
PONG— it anchors on its PONG-heavy transcript. The same text delivered via the Stop-hook path in that session producedBANANA, so the model can follow it. This PR fixes delivery; it cannot make a weak model obey.TestDispatchBg(fresh argv unchanged; resume inlines + leads with the real block; oversized falls back to the digest pointer). Suite: 1420 passed.tests/test_receipts.py::test_receipts_reproduce[claim-nonce.md]fails — PRE-EXISTING, verified by stashing this change and re-running (fails identically on a clean tree).python3.10not installed on this machine); only 3.13 was exercised. Please runpy -3.10 -m pytest -qbefore merge.Follow-ups (not in this PR)
docs/SPEC.md§6 G8 states "argv carries only the tiny fixed prompt" — now true only for the non-resume case; needs narrowing.fleet peekreported "no transcript found" for a just-forked session whose.jsonlexisted on disk (timing/lookup race).fleet spawnprintedworker name already existswhile nonetheless creating and running the worker (non-atomic registration).🤖 Generated with Claude Code