Skip to content

fix: deliver team instructions larger than tmux's 16 KiB command limit - #24

Merged
SergioTCG merged 1 commit into
mainfrom
fix/team-large-instruction-delivery
Oct 6, 2026
Merged

SergioTCG merged 1 commit into
mainfrom
fix/team-large-instruction-delivery

Conversation

@SergioTCG

Copy link
Copy Markdown
Owner

Summary

Team instructions and coordinator messages larger than about 16 KB never reached the worker. The task sat in dispatching until the reconciler failed it about five minutes later: "Delivery became uncertain before the provider acknowledged the delegated turn; SAB did not retry it to avoid duplicate work."

The cause is a tmux command-size limit in the paste transport, not the provider TUI. SAB now streams the paste buffer to tmux on stdin, and a failure that provably happened before anything was pasted is reported at once instead of as "uncertain".

Root cause

  • tmux bound: tmux sends each command from client to server as one message of at most 16 KiB.
    • On tmux 3.7b, tmux set-buffer -b NAME TEXT accepts 16,340 bytes and rejects 16,400 with command too long.
  • SAB's transport: tmuxPaste staged every payload exactly that way, as a command argument.
    • TEAM_MESSAGE_MAX_BYTES is 24 KiB, and the delegated-task envelope adds about 1.2 KB.
    • So every accepted instruction between about 16.3 KB and 25 KB was undeliverable.
  • Failure classification: injectText classified any tmux error on exact delegated input as uncertain.
    • dispatchTeamTask logged "provider input may have been accepted; refusing retry" and kept the claim.
    • No prompt hook or poller ever appeared, so the 5-minute reconciler failed the task as uncertain.
    • Coordinator follow-ups (sab team message/continue to an active task) of that size failed the same way.
  • Log leak: the failing execFile error echoed the full argv, so the whole instruction was written into daemon.log.

Evidence from the failed batch (Claude team, five workers)

  • Daemon log: five consecutive dispatches of 20–24 KB instructions each logged team task provider input may have been accepted; refusing retry … Command failed: tmux set-buffer -b sab-inject-… <sab-team-task …> followed by command too long.
  • Task timing: each task failed as uncertain about 5 minutes after its claim.
  • Pointer prompts: the ~1.1 KB pointer instructions sent right afterwards were acknowledged within seconds.
  • Codex team, a few days earlier: same signature. A 15,158-character task was delivered; tasks of 15,568 characters and more failed, since multibyte characters push them past 16,340 bytes.

Fix

  • tmuxLoadBuffer (daemon/util.mjs) streams the payload to tmux load-buffer -b NAME - on stdin.
    • There is no size bound, and the payload never appears in argv, the process list or tmux error text.
  • tmuxPaste has two outcomes:
    • A staging failure throws with inputNotDelivered: true, because nothing was pasted or submitted.
    • A paste or Enter failure stays uncertain, exactly as before. The private buffer is always released.
  • injectText, for exact delegated input:
    • inputNotDelivered → discard the staged generation, forget the injected text, and throw a definite TeamError.
    • dispatchTeamTask therefore fails the task at once: "Provider injection failed: tmux could not stage the input: … Nothing reached the provider." There is still no second transport.
    • Every other tmux error stays uncertain and is never retried.
  • injectCoordinatorTaskMessageOnce: inputNotDelivered → knownUndeliveredTeamMessage. That is the existing path for a provable pre-write rejection: it discards the staged generation and defers the message for retry.
  • Send-time limit: the 24 KiB cap is unchanged (413 message_too_large, "Team messages may be at most 24576 bytes."), and a boundary test now covers it. Every size it accepts is now deliverable, so no lower cap is needed.

Verified against real provider TUIs (isolated scratch sessions, env -i, no bridge)

The worktree's new tmuxPaste was used for every check.

Provider Payload sizes Result
Claude Code 2.1.291 8, 16.5, 24.7, 26.7, 40, 64, 100 and 256 KB byte-exact in the UserPromptSubmit prompt (after <pasted_content> unwrapping); hook about 0.9 s after the paste
Codex 0.159.2 8, 16.5, 26.7 and 64 KB byte-exact in the session rollout about 1 s after the paste

The existing 300 ms pause before Enter is enough at every size, so verifying the paste before Enter was unnecessary. It would also need brittle, provider-specific pane parsing.

Alternatives considered

  • Chunking produces several separate paste blocks and adds nothing once stdin removes the bound.
  • A per-task file plus a pointer prompt would add a file lifecycle and change what the worker sees. It would also need the file on the worker's own execution node.

Duplicate-delivery safety

  • One attempt per injection. No retry loop was added. Tests pin exactly one paste and at most one Enter per attempt, including when the paste or Enter fails.
  • Staging failure. Nothing reached the provider, so no late acknowledgement can follow. The staged generation is discarded, so even a forged or delayed prompt cannot claim the task.
  • Paste or Enter failure. It stays uncertain and keeps its staged generation, which survives restart. A late exact prompt promotes it once; a repeated hook cannot promote it twice.
  • Expired uncertain claim. A late acknowledgement cannot revive it: markTeamTaskRunning rejects a failed task, and the task stays failed.

Tests

9 new tests; test/tmux-input.test.mjs was updated for the stdin staging step.

Transport and size boundaries:

  • Payloads of 16,340, 16,400 and 16,385 bytes, plus a maximum delegated task (24 KiB of multibyte instruction plus envelope), are staged exactly once on stdin. They follow the order load → paste → delete → Enter, and no tmux argv element carries the payload.
  • A real tmux server on a private TMUX_TMPDIR socket rejects the maximum delegated task through set-buffer with command too long and stores it byte-exact through tmuxLoadBuffer. The test is skipped when tmux is not installed.
  • tmuxLoadBuffer writes the payload on stdin and surfaces tmux's stderr.
  • Send time: exactly 24,576 UTF-8 bytes are accepted, and one byte more is rejected with message_too_large / 413.

Failure classification:

  • A staging failure is inputNotDelivered and never pastes or presses Enter.
  • A paste or Enter failure is uncertain and never repeated.
  • An unstaged write leaves nothing to acknowledge; an uncertain write is acknowledged once.
  • An expired uncertain dispatch is never revived by a late acknowledgement.
  • A daemon contract pins:
    • one tmux attempt per injection;
    • an uncertain branch with no retry;
    • a definite branch that discards the staged turn and fails before any fallback;
    • immediate task failure in dispatchTeamTask;
    • the coordinator message's known-undelivered conversion.

Mutations. Each was applied on its own, then the files were restored byte-identical:

Mutation Result
tmuxPaste stages through set-buffer argv again red: 4 transport tests
Staging failure not marked as undelivered red
Paste failure misclassified as undelivered red
Paste retried after a failure (duplicate) red
load-buffer puts the payload in argv red: stdin test and real-tmux test
injectText drops the definite branch red: daemon contract
injectText treats every failure as uncertain again red: daemon contract
Coordinator message drops the known-undelivered conversion red: daemon contract

Validation

Rollout notes

  • Daemon restart needed: yes.
    • The change is in daemon/util.mjs and daemon/daemon.mjs, both loaded at daemon start.
    • Hooks, the CLI and the MCP channel are unchanged. There is no state migration and no new persisted field.
  • Merging deploys. The self-updater fast-forwards a clean main and restarts the daemon, so merge only when you want to roll.
  • Running sessions survive a restart. Provider sessions are tmux-owned and re-adopted. Restart with launchctl kickstart -k gui/$(id -u)/si.sergej.claudeslackproxy.
  • Existing restart semantics still apply to an active team:
    • a worker mid-turn is re-adopted with live-turn proof;
    • an idle worker with a running task is released as failed;
    • awaiting_release tasks are kept;
    • a dispatching claim is never retried.
      So roll when no task is dispatching, ideally between batches.
  • Tasks already failed as uncertain stay failed. None of them reached its worker, so they can simply be sent again after the rollout.
  • Independent of fix: keep team tasks and coordinator authority across Claude notifications #23. Either can roll first.

🤖 Generated with Claude Code

https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R

tmux sends each command to its server as one message of at most 16 KiB.
tmuxPaste staged the paste buffer with `tmux set-buffer -b NAME TEXT`, so any
payload above about 16,340 bytes failed with "command too long" before
anything was pasted. Team messages may be 24 KiB, and a delegated task adds
its envelope on top.

injectText classified every tmux error on exact delegated input as
uncertain. Dispatch therefore kept the claim while the worker sat at an
empty prompt, and the reconciler failed the task five minutes later with
"Delivery became uncertain". Coordinator follow-ups above the limit failed
the same way.

tmuxPaste now streams the payload to `tmux load-buffer -` on stdin. This
has no size bound and keeps the payload out of argv, the process list and
tmux error text; the old failure logged whole instructions into daemon.log.

A failure while staging proves nothing reached the provider:
- a delegated task now fails at once with its cause and no staged turn;
- a coordinator message becomes a known non-delivery that is deferred for
  retry.
A failure at paste or Enter stays uncertain and is never retried.

Verified against the Claude Code 2.1.291 and Codex 0.159.2 TUIs in
isolated scratch sessions. The new transport delivered prompts byte-exact
from 8 KB to 256 KB on Claude and from 8 KB to 64 KB on Codex, with each
hook or rollout entry about 1 s after the paste. No pane check before
Enter is needed.

Validated:
- Full suite passes (557 tests, 9 new, including a real tmux regression on
  a private socket), plus shellcheck, .mjs syntax, npm run check and the
  Pi extension load.
- Each of eight mutations turns the new tests red.
- npm run audit fails on the newly published GHSA-jqcg-44mw-7w3h
  (proxy-addr); this change touches no dependencies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Xs8AVBVrTGfpzUMiCQp2R
@SergioTCG
SergioTCG force-pushed the fix/team-large-instruction-delivery branch from 72e7f4a to e7f44d8 Compare October 6, 2026 11:13
@SergioTCG
SergioTCG merged commit 3210088 into main Oct 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants