Skip to content

feat(coderd/x/chatd): identify tool calls in the dispatcher and dedupe execute replays - #27370

Draft
ibetitsmike wants to merge 1 commit into
mike/codagt-757-lite/01-agent-start-tokensfrom
mike/codagt-757-lite/02-chatd-client-tokens
Draft

ibetitsmike wants to merge 1 commit into
mike/codagt-757-lite/01-agent-start-tokensfrom
mike/codagt-757-lite/02-chatd-client-tokens

Conversation

@ibetitsmike

@ibetitsmike ibetitsmike commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Stack context

Part 2 of the CODAGT-757 stack. Builds on the run-once reservation mechanism from #27369 by identifying the tool call a reservation belongs to.

Correcting when replay happens

The earlier version of this PR claimed replay follows an interrupt, an edit, or a worker crash. The first two are wrong:

  • interrupt commits synthetic cancellation results for unresolved calls;
  • edit soft-deletes the suffix and synthesizes pending-tool cancellations.

Both mark those calls handled, and handledToolCallIDs suppresses replay for handled calls. The real driver is narrower: a worker that dies or an attempt that is retried between running a tool and committing its result, including a lost response. The honest framing for this stack is crash-window dedup, and the doc now says that.

What

The identity of a tool call is now established by the local tool dispatcher rather than by generation code through an execute-specific channel. chatloop.ExecuteLocalTools receives the chat and assistant message row for the batch, and attaches the identity of each call, including its provider tool call ID, before running it. Any tool that must run at most once per persisted call can read it without introducing another identity path. execute is the only consumer today.

The key

Following review, the key is now the plain string <assistantMessageID>-<toolCallID>. Both changes the reviewer asked for are in:

The assistant message ID stays. A regenerated call reuses its provider tool call ID, but a history edit re-inserts the assistant message under a new row ID, so the message ID is what keeps a regenerated call from aliasing the original. Dropping it would rest on an unverified provider guarantee that tool call IDs never repeat.

The message ID is decimal digits, so the first - splits the pair back apart unambiguously no matter what the tool call ID contains. TestIdempotencyKeyFromContext covers that with adversarial pairs (4+2-x against 42+-x) and with tool call IDs containing /, %, a space, and non-ASCII.

Conflicts and attach on replay

Start conflicts are classified by the code from #27369 instead of by searching error text. input_mismatch yields an error result and starts nothing; start_pending is unresolved, so the tool retries while its budget lasts and only reports an unresolved outcome if the budget runs out.

An attached foreground start also returns the process's original start time, so the replay waits only the remaining budget (started_at + timeout - now) rather than a fresh full timeout. An attached process that already exited reports its real result even past the deadline; one still running past the deadline returns the usual timed-out result carrying the background_process_id handle.

This PR was written and revised by Mux, an AI coding agent, operating on Mike's behalf.

@linear-code

linear-code Bot commented Jul 21, 2026

Copy link
Copy Markdown

CODAGT-757

Copy link
Copy Markdown
Collaborator Author

@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7aad78e8b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/x/chatd/chattool/execute.go
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from 7aad78e to db15eaa Compare July 21, 2026 11:22
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db15eaae70

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/x/chatd/chattool/execute.go Outdated
Comment thread coderd/x/chatd/chattool/execute.go
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from db15eaa to a0e39db Compare July 21, 2026 11:43
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0e39db035

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread coderd/x/chatd/chattool/execute.go Outdated
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from a0e39db to a19b772 Compare July 21, 2026 11:58
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

Reviewed commit: a19b77260c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from d3be23b to c6f4fb5 Compare July 21, 2026 13:07
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from a19b772 to b89e001 Compare July 21, 2026 13:07
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: b89e001b53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from d9b87ba to f3c8095 Compare July 29, 2026 12:08
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from f75158c to 30f965a Compare July 29, 2026 12:08
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from f3c8095 to 3dfed86 Compare July 29, 2026 13:55
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from 30f965a to 054e270 Compare July 29, 2026 13:55
@ibetitsmike ibetitsmike changed the title feat(coderd/x/chatd): send idempotent client tokens on execute dispatch and attach on replay feat(coderd/x/chatd): identify tool calls in the dispatcher and dedupe execute replays Jul 29, 2026
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

Done on all four points.

Identity in the common dispatch path. ExecuteIdentity is gone. chatloop.ExecuteLocalTools now takes the chat and assistant message row for the batch and attaches each call's identity, including its provider tool-call ID, before running it. Any local tool can read it; execute remains the only consumer. No other tool changed. Covered by a dispatcher test asserting each concurrently dispatched call sees its own ID, since aliasing two calls onto one key is the failure that would matter.

Comparison fields. The fingerprint is command, requested workdir, env, background, and chat ownership. Display intent and chatd's wait budget are excluded.

process_output. Unchanged.

Machine-readable conflicts. strings.Contains on the error detail is gone. The agent returns input_mismatch or start_pending on a route-specific ProcessConflictError, and chatd branches on the code. I put it there rather than adding a code field to the global codersdk.Response, following the existing WatchError precedent; happy to move it if you would rather it were global.

One thing I got wrong in the original description, corrected here and in ARCHITECTURE.md: I claimed replay follows an interrupt, an edit, or a crash. Interrupt and edit both commit synthetic cancellation results, which marks the calls handled, and handledToolCallIDs then suppresses replay. The actual driver is the crash window between running a tool and committing its result, plus lost responses. The bug is real but narrower than I described it, so the stack is crash-window dedup.

Also worth noting for #27369: its comment says "tool-call ID" where this one correctly says the composed identity. The composed key is what landed, since bare provider tool-call IDs repeat across regenerations.

Reviewed and revised by Mux, an AI coding agent, operating on Mike's behalf.

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from 3dfed86 to 6c58dd6 Compare July 29, 2026 14:33
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from 054e270 to 6aefea3 Compare July 29, 2026 14:33
@ibetitsmike
ibetitsmike requested a review from mafredri July 29, 2026 15:48
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from 6c58dd6 to 7bedd8a Compare July 29, 2026 15:52
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from 6aefea3 to dbdb8fe Compare July 29, 2026 15:52

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't have time to properly look through the changes here, but at a cursory glance it seems alright, although could be simplified a bit. Comments and writing does need a bit of work though, as seems to be the case with this whole stack. 🙈

Comment thread coderd/x/chatd/chattool/execute.go Outdated
if !ok {
return ""
}
sum := sha256.Sum256(fmt.Appendf(nil, "%s|%d|%s", identity.ChatID, identity.AssistantMessageID, identity.ToolCallID))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really see the need for sha256 here. We could do identityAssistantMessageID.String() + identity.ToolCallID.String(). Although I suspect even message ID isn't necessary? We already transmit chat ID as part of the communication between chatd and workspace agent.

If we can't trust the chat ID already transmitted, we have bigger problems.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both parts taken.

The key is now fmt.Sprintf("%d-%s", identity.AssistantMessageID, identity.ToolCallID). No SHA-256: it is not a secret and does not need fixed width. No chat ID either, for exactly the reason you give, and it composes with your composite-key suggestion on #27369: the agent now keys reservations by {chatID, idempotencyKey}, so the chat is already the namespace and carrying it in the key too was redundant.

I kept the assistant message ID. A regenerated call reuses its provider tool call ID, but a history edit re-inserts the assistant message under a new row ID, so the message ID is what stops a regenerated call from aliasing the original. Dropping it would rest on an unverified provider guarantee that tool call IDs never repeat, which is the one thing here I did not want to assume.

The message ID is decimal digits, so the first - splits the pair unambiguously no matter what the tool call ID contains. TestIdempotencyKeyFromContext/DistinctPairsCannotCollide proves it with adversarial pairs (4+2-x vs 42+-x, 1+1-1 vs 11+-1).

One consequence worth flagging: the key is no longer guaranteed hex, and #27372 addresses processes by it. I moved that route's key from the URL path into the request body rather than escaping it, so no key value can break routing. Reasoning is in that PR's description.

Reply written by Mux, an AI coding agent, operating on Mike's behalf.

@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from 7bedd8a to 121599a Compare August 1, 2026 03:26
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from dbdb8fe to 202fa50 Compare August 1, 2026 03:27
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from 121599a to 3f1ac82 Compare August 1, 2026 04:01
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from 202fa50 to a3f2866 Compare August 1, 2026 04:01
@ibetitsmike

Copy link
Copy Markdown
Collaborator Author

Revised. Both parts of your key comment are in, and I took the "comments and writing need work" note as applying here too rather than only where you flagged it.

Key

Now <assistantMessageID>-<toolCallID>. No SHA-256, and no chat ID: after the composite-key change in #27369 the agent already scopes reservations to the chat that sent them, so carrying it in the key was redundant.

I kept the assistant message ID and said so explicitly rather than dropping it silently. A regenerated call reuses its provider tool call ID, but a history edit re-inserts the assistant message under a new row ID, so the message ID is what stops a regenerated call from aliasing the original. Dropping it would rest on an unverified provider guarantee.

The message ID is decimal digits, so the first - splits the pair unambiguously. TestIdempotencyKeyFromContext proves that with adversarial pairs (4+2-x vs 42+-x) and with tool call IDs containing /, %, a space, and non-ASCII.

Consequence worth flagging

The key is no longer guaranteed hex, and #27372 addresses processes by it over HTTP. I measured chi rather than assuming: routing works with url.PathEscape, but chi.URLParam returns an escaped value when RawPath is set and a decoded one otherwise, and a key containing a literal % lands in the second case where url.PathUnescape then errors. Rather than write a conditional unescape against a chi internal, I moved the key into the request body in #27372. That deletes the bug class instead of handling it.

Comments

Added comments in execute.go went from 37% of added code to 26%, and identity.go lost the claims about provider behavior that I could not verify from this code.

Revised by Mux, an AI coding agent, operating on Mike's behalf.

@github-actions github-actions Bot added the stale This issue is like stale bread. label Aug 9, 2026
@github-actions github-actions Bot closed this Aug 12, 2026
@ibetitsmike ibetitsmike reopened this Aug 27, 2026
…e execute replays

A resumed turn replays tool calls that history still shows as
unresolved, which for the execute tool means running the shell command
a second time. Interrupts and edits are not the cause: both commit
synthetic cancellation results, which marks the calls handled. What
remains is the crash window between running a tool and committing its
result, including a lost response.

The local tool dispatcher now attaches the identity of the call it is
running (chat, assistant message row, and provider tool call ID) to
the execution context, so any tool that must run at most once per
persisted call can derive a key from it without its own identity path.
The execute tool is the only consumer today: it hashes the identity
into the client token it sends with StartProcess, and a replay of the
same persisted call derives the same token and attaches to the process
already running or recently exited.

Start conflicts are classified by the code the agent returns rather
than by matching its error text. A parameter mismatch is permanent and
yields an error result; a start still pending means the outcome is
unresolved, so the tool retries while its budget lasts.
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/01-agent-start-tokens branch from 3f1ac82 to 22dc279 Compare August 27, 2026 14:41
@ibetitsmike
ibetitsmike force-pushed the mike/codagt-757-lite/02-chatd-client-tokens branch from a3f2866 to 0599aa1 Compare August 27, 2026 14:41
@ibetitsmike
ibetitsmike requested a review from mafredri August 27, 2026 14:44
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Aug 28, 2026
@github-actions github-actions Bot added the stale This issue is like stale bread. label Sep 5, 2026
@github-actions github-actions Bot closed this Sep 8, 2026
@mafredri mafredri reopened this Sep 9, 2026
@github-actions github-actions Bot removed the stale This issue is like stale bread. label Sep 10, 2026
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