fix(coding-agent): keep retried agent messages on one supervisor journal key - #892
Open
Hotragn wants to merge 1 commit into
Open
fix(coding-agent): keep retried agent messages on one supervisor journal key#892Hotragn wants to merge 1 commit into
Hotragn wants to merge 1 commit into
Conversation
…nal key The supervisor journals mutating daemon commands under [clientId, commandId] and replays a completed result instead of re-executing. DaemonMode.sendRemoteAgentSessionMessage, the one loop that re-sends send_message across the accept window, builds a new DaemonClient per attempt. Both halves of that key are per-instance: protocolClientId is a fresh uuid and the request counter restarts at 0. Every retry therefore presented a different key for the same logical message, the journal could not recognize the duplicate, and acceptAgentSessionMessage has no dedupe of its own, so the target received N independent steering prompts under N distinct agentmsg ids. The window is real: request() times out locally without sending any cancellation, so a timeout does not mean the supervisor declined the message. A socket drop mid-response or a supervisor restart has the same shape. Let a caller pin the identity: DaemonClient accepts a clientId, and request() accepts a commandId. sendRemoteAgentSessionMessage mints both once per logical send and reuses them for every attempt, so a retry after the supervisor already accepted replays the recorded receipt, and a retry that races an in-flight attempt gets command_result_uncertain rather than a duplicate. Callers that pass neither keep a fresh identity per client, unchanged. A command id reused while still in flight on one client is rejected. fixes PrimeIntellect-ai#821
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.
Fixes #821.
The supervisor already has durable idempotency for mutating daemon commands:
CommandRecoveryJournalkeys completed results oncreateCommandIdempotencyKey(clientId, commandId)and replays the stored response instead of re-executing.send_messageis mutating, so it is covered in principle.DaemonMode.sendRemoteAgentSessionMessage— the one loop in the tree that re-sendssend_messageacross the accept window — steps outside it. It constructs a newDaemonClienton every attempt, and both halves of the journal key are per-instance:So each retry presented a different key for the same logical message. The journal could not recognize the duplicate,
acceptAgentSessionMessageperforms no dedupe of its own, and the target received N independent steering prompts under N distinctagentmsg_ids.The window is real rather than theoretical.
DaemonClient.requesttimes out locally and sends no cancellation, so a timeout does not mean the supervisor declined the message — it may already have accepted and enqueued it. A socket drop mid-response or a supervisor restart has the same shape. The existingreceivedResponseguard correctly refuses to retry once a response was parsed, but that is precisely the case this window excludes.Change
Let a caller pin the identity, and have the one loop that needs it do so:
DaemonClienttakes an optionalclientId, used in place of the per-instance uuid.DaemonClientRequestOptionstakes an optionalcommandId, used in place ofdaemon_${++this.requestId}.sendRemoteAgentSessionMessagemints oneclientId+commandIdper logical send, before the retry loop, and passes them to every attempt.Behaviour after the change:
completeand replays the recorded receipt. Exactly one message is enqueued and the caller gets the real receipt rather than an error.pendingand the supervisor answerscommand_result_uncertain. That is a definite answer instead of a silent duplicate, which is what the journal was built to provide.connect/waitForHellofailed, the common restart case): no journal entry exists, so the attempt proceeds normally. Unchanged.Callers that pass neither option keep a fresh identity per client, so every other
DaemonClientconsumer is unchanged — this is opt-in, not a global behaviour change. Reusing a command id while it is still in flight on the same client would clobber that client's pending-request map, so it is rejected with a clear error.No protocol or schema change:
clientIdandidare existing envelope fields, and this only controls what is put in them.Tests
packages/coding-agent/test/suite/regressions/821-agent-message-retry-idempotency.test.ts(4 cases), using the same mocked-socket harness asdaemon-client.test.tsso it needs no real daemon:DaemonClientinstances with a pinned identity write envelopes whoseclientIdandidare identical, andcreateCommandIdempotencyKeyover the two envelopes produces the same key — the defect stated directly.CommandRecoveryJournalwith the two captured identities: the firstbeginisnew, the recorded receipt is stored, and the retry'sbeginreturnscompletewith that same receipt instead of admitting a second command. The same test then shows an unpinned identity would have been admitted asnew— the duplicate steering prompt.Verified:
npm run checkclean; the new suite plusdaemon-client.test.tspass (33).command-recovery-journal.test.tshas one pre-existing Windows-only failure (EPERMon directoryfsync, tracked as #666) that does not touch this path.Note
Fix duplicate steering prompts on retried agent messages by pinning supervisor journal keys
send_messagetransport call fails after the supervisor has already accepted the message, retries previously enqueued duplicate steering prompts. This fix pins a stableclientIdandcommandIdfor the duration of eachsend_messageoperation so all retry attempts share the same supervisor journal key and replay the recorded receipt instead.DaemonClientconstructor now accepts an optionalclientIdto pin the protocol client identity, andDaemonClient.requestaccepts an optionalcommandIdto pin the wire command id.commandIdis already in-flight on the same client instance, preventing accidental duplicate submissions.821-agent-message-retry-idempotency.test.tscovers pinned and default identity behavior, supervisor journal replay, and in-flight duplicate rejection.Macroscope summarized 11e6916.