Launch command is typed into the pane and silently cut at 1024 bytes #220

Closed
opened 2026-09-01 09:12:17 +02:00 by ltms · 0 comments
Owner

Fixed on main @ c3fa113. Filed after the fact so the finding is written down.

What happened

After #211/#213/#214 were merged and deployed, every claude-code spawn failed:

spawn timed out — worker pane never reached injectable state:
worker pane w8:p17 did not reach injectable state within 20000ms

opencode members spawned fine. Running the same claude command by hand — same flags, same
config dir, in a real worktree, under a pty — started normally. A process sampler during a failing
spawn found no claude process at all, and the daemon logged tab.close(...) ignored — already gone: the pane had died on its own.

The cause

herdr does not exec the launch command. It types it into the pane. A pty line buffer holds
1024 bytes (BSD/macOS MAX_CANON), and everything past that byte is dropped.

Nothing reports it. herdr answers "agent started", the backend exits on the mangled argument it was
handed, the pane closes, and the only symptom is the readiness gate timing out 20 seconds later
with no reason at all.

The reply charter rode inline on --append-system-prompt, so the command was already 978 bytes.
#214 added --session-id <uuid> — 50 bytes — making it 1028. The 4 bytes that got cut were the end
of the last argument:

... --model claude-sonnet-5 --autocompact 25

--autocompact 25 is outside Claude Code's 100k–1M band, so claude refused it and exited. Measured
on the live pane, the cut is at byte 1024 exactly.

So #214 was correct and is not being reverted — it only exposed a latent limit that the charter had
already eaten almost all of.

The fix (c3fa113)

  • ClaudeCodeLauncher — the charter now ALWAYS travels as --append-system-prompt-file. That
    file path already existed for the two-charter case; the inline form only ever saved writing a temp
    file, and it cost ~800 bytes of the line budget.
  • HerdrPeerLauncher.checkPaneCommandFits — refuse a command that cannot fit, naming the byte
    count and the longest argument, instead of spawning something that cannot work. The estimate is
    deliberately conservative: fleetd cannot see herdr's quoting, and an under-estimate would let the
    silent truncation back in.
  • HerdrPeerLauncher.waitUntilInjectableOrThrow — log the pane tail and the last herdr status
    before stop() closes the pane. Without this the gate reports only that it timed out, which is
    true of every possible cause. This is what found the bug, and it stays.

Something else the guard caught

A profile with ideMcpUrl set assembles 1084 bytes — already over the limit before this ticket.
The CB-634 IDE mount was one config key away from the same silent failure. It cannot ship that way
now.

Evidence

  • 3 tests, all watched failing first. With the inline charter restored, the guard fires in the new
    fit test, in the pre-existing autocompact test, and in the IDE mount test.
  • Full suite: 1081 tests, 0 failures, 0 errors.
  • Live: sonnet spawns again, the member obeys the file-delivered charter and ends its turn with
    fleet_reply, and fleet_list reports the #214 agentSessionId.
**Fixed on main @ `c3fa113`. Filed after the fact so the finding is written down.** ## What happened After #211/#213/#214 were merged and deployed, every `claude-code` spawn failed: ``` spawn timed out — worker pane never reached injectable state: worker pane w8:p17 did not reach injectable state within 20000ms ``` `opencode` members spawned fine. Running the same `claude` command by hand — same flags, same config dir, in a real worktree, under a pty — started normally. A process sampler during a failing spawn found **no claude process at all**, and the daemon logged `tab.close(...) ignored — already gone`: the pane had died on its own. ## The cause herdr does not `exec` the launch command. It **types** it into the pane. A pty line buffer holds **1024 bytes** (BSD/macOS `MAX_CANON`), and everything past that byte is dropped. Nothing reports it. herdr answers "agent started", the backend exits on the mangled argument it was handed, the pane closes, and the only symptom is the readiness gate timing out 20 seconds later with no reason at all. The reply charter rode inline on `--append-system-prompt`, so the command was already **978 bytes**. #214 added `--session-id <uuid>` — 50 bytes — making it 1028. The 4 bytes that got cut were the end of the last argument: ``` ... --model claude-sonnet-5 --autocompact 25 ``` `--autocompact 25` is outside Claude Code's 100k–1M band, so claude refused it and exited. Measured on the live pane, the cut is at byte 1024 exactly. So #214 was correct and is not being reverted — it only exposed a latent limit that the charter had already eaten almost all of. ## The fix (`c3fa113`) - **`ClaudeCodeLauncher`** — the charter now ALWAYS travels as `--append-system-prompt-file`. That file path already existed for the two-charter case; the inline form only ever saved writing a temp file, and it cost ~800 bytes of the line budget. - **`HerdrPeerLauncher.checkPaneCommandFits`** — refuse a command that cannot fit, naming the byte count and the longest argument, instead of spawning something that cannot work. The estimate is deliberately conservative: fleetd cannot see herdr's quoting, and an under-estimate would let the silent truncation back in. - **`HerdrPeerLauncher.waitUntilInjectableOrThrow`** — log the pane tail and the last herdr status **before** `stop()` closes the pane. Without this the gate reports only that it timed out, which is true of every possible cause. This is what found the bug, and it stays. ## Something else the guard caught A profile with `ideMcpUrl` set assembles **1084 bytes** — already over the limit before this ticket. The CB-634 IDE mount was one config key away from the same silent failure. It cannot ship that way now. ## Evidence - 3 tests, all watched failing first. With the inline charter restored, the guard fires in the new fit test, in the pre-existing autocompact test, and in the IDE mount test. - Full suite: **1081 tests, 0 failures, 0 errors**. - Live: sonnet spawns again, the member obeys the file-delivered charter and ends its turn with `fleet_reply`, and `fleet_list` reports the #214 `agentSessionId`.
ltms closed this issue 2026-09-01 09:12:28 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#220