node send drops the head of a multi-chunk message, keeps the tail, and reports delivered. The receiving loop gets a message that begins mid-sentence and reads as complete, so neither side has any signal that anything was lost.
This is the same silent-loss class that d3f7c9a ("Type large messages into a session in PTY-queue-sized pieces", 0.1.29) fixed for the oversized single write. That fix capped one write at maxSendChunkBytes = 2048 and split above it. The split path itself now loses its first chunk.
Measured
Four node send calls from one loop to another (TripleAgentSpawn → IssueTriage), local project, both Claude Code backends. Sizes are what arrived in the receiver's session transcript.
| Sent |
Chunks |
Arrived |
Lost |
Result |
| 1944 B |
1 |
1944 B |
0 |
✅ intact |
| 1985 B |
1 |
1985 B |
0 |
✅ intact |
| ~2600 B |
2 |
646 B |
~75% |
❌ head clipped |
| ~2400 B |
2 |
1304 B |
~46% |
❌ head clipped |
| ~2600 B |
2 |
61 B |
~98% |
❌ head clipped |
Every send returned delivered. Every single-chunk message arrived whole. Every multi-chunk message arrived headless.
Examples of what the receiver actually saw, each the start of a message as delivered:
der does not go fix the truncation and think they fixed this.
R does not regress it, and file a follow-up issue for it so it is not lost. TWO, finding 6
daemon mid-PTY-spawn on a loaded codespace. The chain is: ensure exits 1, startRemote, run
Why this is worse than dropping the message
A clipped message looks like an arrived message. The receiver has no way to know a head is missing — it reads as a message that happens to start abruptly. In the run where this was found, two code-review findings reached the receiving loop as bare line numbers with no defect description, and a third arrived as nothing but its closing clause. The receiving loop acted on what it had and reported back as though it had the whole thing.
It also defeats diagnosis. The [graphcode] <Sender>: prefix lives in the first chunk, so a clipped message loses its own attribution. Searching a transcript for [graphcode] — the obvious way to audit delivery — returns only the messages that arrived intact, and the damaged ones are invisible to exactly the query you would use to find them. This bug hides its own evidence.
Where
ZmxSessionLauncher.swift:265 send(_:to:projectPath:) — the local path: for (index, chunk) in messageChunks(text).enumerated(), one PTYProcessSession per chunk with interChunkDelay (150 ms) between, then a separate submit keystroke.
ZmxSessionLauncher.swift:55 messageChunks(_:limit:) — the splitter. Cuts on character boundaries; the splitting logic itself looks correct on inspection.
ZmxSessionLauncher.swift:46 maxSendChunkBytes = 2048.
ZmxSessionLauncher.swift:1446 remoteSendInvocation — the remote path chains the same chunks into one ssh round-trip with && sleep 0.15 && between writes. Not tested here; a remote loop may or may not share the fault.
The chunk loop returns false if any zmx send fails, so a chunk that errored would surface. These reported success — consistent with the write being accepted and the text never reaching the composer, which is precisely the failure mode maxSendChunkBytes's own doc comment describes for a single oversized write.
Repro
- Two loops in a local project.
- From one:
graphcode node send <project> <target-id> <a message over 2048 bytes>.
- It prints
delivered.
- Read the target's session transcript. The message is present, beginning mid-sentence, with the leading ~2048 bytes and the
[graphcode] <Sender>: prefix gone.
Under 2048 bytes the same send arrives intact, which is the control.
Suggested direction
Not prescribing a fix, but three things seem worth separating:
- Find why the first chunk specifically is lost. The later chunks land, so the transport works; something about the first write into a composer that is about to receive more, or the state of the session when the first write arrives, is different.
- Make a partial delivery detectable. Right now
delivered means "every zmx send exited 0", which has been shown twice to be compatible with the text never arriving. A receipt the receiver can verify — a byte count, or the sender's own prefix checked for on the far side — would turn a silent corruption into a reportable failure.
- Consider whether the prefix should ride the last chunk rather than the first, so that attribution survives a head loss and the damaged messages are findable.
Impact
Loop-to-loop coordination is unreliable in a way nothing reports, and the damage is invisible to the audit you would naturally run. Any orchestration that sends detailed instructions between loops — review findings, merge criteria, handoffs — can silently deliver a fraction of them. Found while orchestrating #272 and #273, where it corrupted the review-findings handoff twice before it was noticed.
node senddrops the head of a multi-chunk message, keeps the tail, and reportsdelivered. The receiving loop gets a message that begins mid-sentence and reads as complete, so neither side has any signal that anything was lost.This is the same silent-loss class that
d3f7c9a("Type large messages into a session in PTY-queue-sized pieces", 0.1.29) fixed for the oversized single write. That fix capped one write atmaxSendChunkBytes = 2048and split above it. The split path itself now loses its first chunk.Measured
Four
node sendcalls from one loop to another (TripleAgentSpawn→IssueTriage), local project, both Claude Code backends. Sizes are what arrived in the receiver's session transcript.Every send returned
delivered. Every single-chunk message arrived whole. Every multi-chunk message arrived headless.Examples of what the receiver actually saw, each the start of a message as delivered:
Why this is worse than dropping the message
A clipped message looks like an arrived message. The receiver has no way to know a head is missing — it reads as a message that happens to start abruptly. In the run where this was found, two code-review findings reached the receiving loop as bare line numbers with no defect description, and a third arrived as nothing but its closing clause. The receiving loop acted on what it had and reported back as though it had the whole thing.
It also defeats diagnosis. The
[graphcode] <Sender>:prefix lives in the first chunk, so a clipped message loses its own attribution. Searching a transcript for[graphcode]— the obvious way to audit delivery — returns only the messages that arrived intact, and the damaged ones are invisible to exactly the query you would use to find them. This bug hides its own evidence.Where
ZmxSessionLauncher.swift:265send(_:to:projectPath:)— the local path:for (index, chunk) in messageChunks(text).enumerated(), onePTYProcessSessionper chunk withinterChunkDelay(150 ms) between, then a separate submit keystroke.ZmxSessionLauncher.swift:55messageChunks(_:limit:)— the splitter. Cuts on character boundaries; the splitting logic itself looks correct on inspection.ZmxSessionLauncher.swift:46maxSendChunkBytes = 2048.ZmxSessionLauncher.swift:1446remoteSendInvocation— the remote path chains the same chunks into one ssh round-trip with&& sleep 0.15 &&between writes. Not tested here; a remote loop may or may not share the fault.The chunk loop returns
falseif anyzmx sendfails, so a chunk that errored would surface. These reported success — consistent with the write being accepted and the text never reaching the composer, which is precisely the failure modemaxSendChunkBytes's own doc comment describes for a single oversized write.Repro
graphcode node send <project> <target-id> <a message over 2048 bytes>.delivered.[graphcode] <Sender>:prefix gone.Under 2048 bytes the same send arrives intact, which is the control.
Suggested direction
Not prescribing a fix, but three things seem worth separating:
deliveredmeans "everyzmx sendexited 0", which has been shown twice to be compatible with the text never arriving. A receipt the receiver can verify — a byte count, or the sender's own prefix checked for on the far side — would turn a silent corruption into a reportable failure.Impact
Loop-to-loop coordination is unreliable in a way nothing reports, and the damage is invisible to the audit you would naturally run. Any orchestration that sends detailed instructions between loops — review findings, merge criteria, handoffs — can silently deliver a fraction of them. Found while orchestrating #272 and #273, where it corrupted the review-findings handoff twice before it was noticed.