Repository navigation
send-message: payload from stdin or a file; messages over 256 KB are delivered; an unacknowledged send fails - #501
Merged
Merged
Conversation
--data took the body only as a command-line argument, which the OS caps: a 1 MB payload fails with "Argument list too long" and Linux stops a single argument at 128 KiB. Add "--data -" (stdin) and --data-file <path>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ged send Confirming --data-file against real nodes showed three problems: - send-message rejected --data-file: its flag allow-list was not updated. - The daemon refused any single stream write larger than MaxNagleBuf (256 KB) with ErrSendBufFull. An IPC send has no reply, so the refusal went nowhere: a 1 MB message was dropped while pilotctl printed status ok. SendData now feeds a large write through the buffer in whole-segment pieces, blocking on the window like any other write. The cap on the buffer is unchanged; the unit test that pinned the refusal is split so the cap is still asserted, now during a blocked oversized write. - send-message exited 0 when no ACK came back. It now fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
force-pushed
the
feat/send-message-data-file
branch
from
October 7, 2026 13:09
1985d5d to
539c540
Compare
…ssage Review fixes for the large-payload change. Daemon: - A write over one Nagle piece went through the buffer in pieces with the send lock released between them, so two writers on one connection could interleave their bytes. A per-connection WriteMu now holds for the whole write. - The piece size is a whole number of 1152-byte segments, and whether a short tail may be held is decided once from the whole write, so a large write sends its last piece at once instead of holding it for an ACK. pilotctl: - The payload is read and checked before the daemon is contacted. A payload over the data-exchange frame limit is refused with a size error instead of being dropped by the receiver; an empty --data-file is named. - A send that failed outright exited 0 with an error field; it now fails. - With --count, a message that failed, was not acknowledged or was refused by the receiver made no difference to the exit status; any such message now fails the command with a count and the first reason. - The ACK read error is kept in the result and shown when a message is not acknowledged. - The usage text and docs/cli-reference.md mention --data-file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a close Second review round. Daemon: - A tunnel send error part-way through a large write ended the write. The failed segment is tracked and retransmitted, but the pieces not yet buffered were dropped, and the connection's next write landed in their place in the stream. The write now keeps draining after such an error and stops only when the connection is gone. - A large write blocked on the window went on sending after a local close. The state is checked again after WriteMu and between pieces, so at most the piece already buffered follows the FIN. - The tail test now leaves a short segment in flight first, which is when deciding per piece would have held the last byte; it failed against that. pilotctl: - With --trace a receiver's refusal is in inner_ack; both the single and the multi-message checks now see it. - A failed --count run keeps every message's result in the error, so the caller can tell which messages to send again. A run where the receiver refused every failed message uses the same code as a single refusal. - The usage line, the command catalogue and the changelog mention --data -. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
pushed a commit
that referenced
this pull request
Oct 7, 2026
- A --trace message is not sent again after a lost ack: receivers do not suppress repeated trace frames (dedupeEligible), so even a current receiver would store it twice. - The lost-ack retry still runs inside sendOne, before #501 judges the send: a retry that is acknowledged is delivered, and a send whose retry is not acknowledged fails with the retry's ack error. - The echo-history scan runs in a goroutine started with the inbox snapshot and is waited for only when an untagged reply has to be judged, so it no longer delays the request. It checks for the sender and reply_to bytes before parsing, skips records over 1 MiB and stops after 16 MiB as well as after 1000 files or 200 records from the peer. - The hold runs until 0.75 s after the message arrived (its received_at), and awaitReply polls when it ends, so it is no longer up to a poll interval longer than stated. - --reply-to accepts an inbox file name with .json. - The help and CHANGELOG say a receiver through v1.13.9 can store a retried message twice when the ack of its untagged copy is lost; from v1.13.10 the repeat is suppressed. - inbox read lines up its labels; a misleading poll comment is fixed. - Tests no longer call t.Fatal from goroutines or receiver callbacks, wait with a timeout, and allow 400 ms for a reply taken at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
pushed a commit
that referenced
this pull request
Oct 7, 2026
- A --trace message is not sent again after a lost ack: receivers do not suppress repeated trace frames (dedupeEligible), so even a current receiver would store it twice. - The lost-ack retry still runs inside sendOne, before #501 judges the send: a retry that is acknowledged is delivered, and a send whose retry is not acknowledged fails with the retry's ack error. - The echo-history scan runs in a goroutine started with the inbox snapshot and is waited for only when an untagged reply has to be judged, so it no longer delays the request. It checks for the sender and reply_to bytes before parsing, skips records over 1 MiB and stops after 16 MiB as well as after 1000 files or 200 records from the peer. - The hold runs until 0.75 s after the message arrived (its received_at), and awaitReply polls when it ends, so it is no longer up to a poll interval longer than stated. - --reply-to accepts an inbox file name with .json. - The help and CHANGELOG say a receiver through v1.13.9 can store a retried message twice when the ack of its untagged copy is lost; from v1.13.10 the repeat is suppressed. - inbox read lines up its labels; a misleading poll comment is fixed. - Tests no longer call t.Fatal from goroutines or receiver callbacks, wait with a timeout, and allow 400 ms for a reply taken at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TeoSlayer
added a commit
that referenced
this pull request
Oct 7, 2026
* pilotctl: send-message --wait matches the reply by message ID --wait took the oldest new inbox message from the peer. Two concurrent requests to one peer, or anything else the peer sent in the window, could hand a caller another request's answer. send-message now sends every message through dataexchange's tagged path (Client.Send) with a new message ID, and the inbox watch uses it: - a message from the peer whose reply_to is one of our IDs is the reply; - a message whose reply_to names another request is never taken; - a message without reply_to is matched by sender and arrival time, as before, so peers that do not echo the ID (every responder today) keep working unchanged. The first-contact re-send gets an ID of its own, because the receiver drops a repeat of the same ID and bytes as a duplicate; the watch accepts a reply to either. --trace is covered: the TRACE wrapper is built here and carries the ID outside it. New flag --reply-to <id> sends a message as the answer to a received one. The JSON result gains message_id, tagged and (with --reply-to) reply_to. Receivers that predate message IDs are reached through the library's fallback: the frame is re-sent in the old format on the same connection, and the result says "tagged": false. An "ERR ..." answer is still reported as the ack, and a send whose ack could not be read still succeeds, as before. No dependency change: dataexchange v0.2.3, already pinned, has the IDs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pilotctl: hold an untagged reply for a tagged one; safer --reply-to; inbox shows IDs send-message --wait took the first untagged message from the peer at once, so a reply naming our request that arrived a poll later lost to another client's untagged answer. When our request reached the receiver with its ID, an untagged message is now held for 0.75 s (bounded by the wait) in case our tagged reply follows, and once the peer has been seen naming another request in reply_to, only a reply naming ours is taken. --reply-to refuses a bare flag (parsed as "true") and maps an inbox file id to the message_id stored in that record, refusing it when the record has none. pilotctl inbox shows message_id and reply_to in the listing, --json, and inbox read; the help says which field to use. A lost ack on the tagged first attempt is retried once on a new connection with the same frame and ID: a current receiver keeps one copy, one through v1.13.9 gets the untagged copy. --no-resend opts out. tagged is reported only when the receiver's ack was read. CHANGELOG no longer claims the concurrency bug is fixed for responders that do not echo reply_to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pilotctl: hold an untagged reply only from a peer known to echo IDs No service responder echoes reply_to yet, so holding every untagged reply for 0.75 s whenever the receiver's daemon knows message IDs would slow nearly every send-message --wait once daemons upgrade, for nothing. An untagged reply is now taken at once, as before, unless the peer is known to echo IDs: one of its newest messages already in the inbox carries a reply_to. newInboxWatch checks this once, from the snapshot it already reads, looking at no more than the newest 1000 records and 200 from the peer. For such a peer the 0.75 s hold stays, and a peer seen naming another request during the wait still gets only a reply naming ours taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pilotctl: review fixes for send-message reply matching after #501 - A --trace message is not sent again after a lost ack: receivers do not suppress repeated trace frames (dedupeEligible), so even a current receiver would store it twice. - The lost-ack retry still runs inside sendOne, before #501 judges the send: a retry that is acknowledged is delivered, and a send whose retry is not acknowledged fails with the retry's ack error. - The echo-history scan runs in a goroutine started with the inbox snapshot and is waited for only when an untagged reply has to be judged, so it no longer delays the request. It checks for the sender and reply_to bytes before parsing, skips records over 1 MiB and stops after 16 MiB as well as after 1000 files or 200 records from the peer. - The hold runs until 0.75 s after the message arrived (its received_at), and awaitReply polls when it ends, so it is no longer up to a poll interval longer than stated. - --reply-to accepts an inbox file name with .json. - The help and CHANGELOG say a receiver through v1.13.9 can store a retried message twice when the ack of its untagged copy is lost; from v1.13.10 the repeat is suppressed. - inbox read lines up its labels; a misleading poll comment is fixed. - Tests no longer call t.Fatal from goroutines or receiver callbacks, wait with a timeout, and allow 400 ms for a reply taken at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pilotctl: the untagged-reply hold starts no earlier than the ack Review fix. The hold counted from the message's received_at even when that was before our request was acknowledged, so after a slow first-contact dial, a large payload or a lost-ack retry, an untagged message from a peer known to echo IDs was taken at the first poll, ahead of the tagged reply. A message that arrived before the ack is the least likely to be the reply; its hold now starts at the ack. The echo-history scan also skips anything that is not a regular file, so a FIFO in the inbox cannot block it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * pilotctl: justify the inbox reads gosec flags; close the retry connection explicitly Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Teo Calin <calinteodor@Teos-MacBook-Pro.local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pull Request
Summary
send-message --datatook the body only as a command-line argument, which the OS caps: a 1 MB payload fails withArgument list too long, and Linux stops a single argument at 128 KiB. Adding a way around that exposed an older bug behind it: any message over 256 KB was silently dropped by the sending daemon whilepilotctlreported success.Changes
pilotctl
--data -reads the payload from stdin;--data-file <path>reads it from a file.send-messagenow fails when no ACK comes back. Every receiver answers a stored message with an ACK; with none, the command used to print"status":"ok"and exit 0.Daemon
MaxNagleBuf(256 KB) was refused withErrSendBufFull. An IPC send has no reply, so the refusal went nowhere (the driver sends up to 1 MiB per chunk).SendDatanow feeds a large write through the buffer in whole-segment pieces, blocking on the window like any other write.For review: a pinned test changed
TestSendDataNagleBufGrowsUnboundedasserted that a 5 MiB write returnsErrSendBufFullimmediately — the v1.9.1 fix for unbounded buffer growth. That refusal is what dropped large messages. The memory guarantee it protects is kept and still tested: the newTestSendDataOversizedWriteStaysWithinNagleCapchecks that, against a peer that never ACKs, the buffer stays within the cap while the oversized write is blocked, and that closing the connection releases the writer with an error. The behaviour change is "blocks under back-pressure" instead of "rejected at once" for a write larger than the buffer.Confirmed against real nodes (two containers)
--dataArgument list too long--data-file"status":"ok", nothing stored; daemon logIPC stream send failed ... send buffer full--data-fileThe first version of this PR was caught by that run:
--data-filewas rejected by the command's flag allow-list, which the unit test of the helper did not exercise.Test Plan
go build ./...,go vet ./..., unit suite withGOWORK=offTestMessagePayload,TestSendDataOversizedWriteStaysWithinNagleCapTestSingleWriteLargerThanSendBufferIsDelivered(integration; fails on main: "a 1 MB write was not delivered within 20s")go test -parallel 4 -count=1 ./tests/: one failure,TestManualSnapshotTrigger, which binds fixed port 127.0.0.1:18080 held by another process on the test machinescripts/gen-cli-reference.shleavesdocs/cli-reference.mdunchangedChecklist
go.mod/go.sumunchanged🤖 Generated with Claude Code