GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — feat: run chat tool calls once per tool call ID on the agent by mafredri · Pull Request #29946 · coder/coder

Conversation

@mafredri

@mafredri mafredri commented Sep 25, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

A chatd retry or replica takeover resends unresolved tool calls to the workspace agent, and execute starts a new process each time (the CODAGT-757 incident started one command 36 times). An interrupt can neither stop the process nor tell the model what happened.

The agent now runs a request that carries Coder-Tool-Call-Id once per chat and tool call ID, and responds to repeats with the first response. StartProcess, EditFiles, and WriteFile send the header. POST /api/v0/tool-calls/{id}/cancel refuses later requests for the call, waits for a running one to finish, stops its process, and returns the call's response.

  • A process started with a tool call ID takes it as its process ID. Its timeout_ms counts from the first start, and an output wait with timeout_from_start_process=true returns there. Output reports timed_out and canceled.
  • Processes are keyed by chat ID, so a chat sees only its own processes, and a request without a chat ID sees only processes started without one.
  • Exited processes stay listed for an hour instead of 5 minutes (redesign: CODAGT-1220).

Requests without the header behave as before. An old agent ignores the header and responds to a cancel with 404.

Part 1 of 2 of the CODAGT-757 stack.

Refs https://linear.app/codercom/issue/CODAGT-757

Design: run chat tool calls once, and cancel them truthfully

CODAGT-757: run chat tool calls once, and cancel them truthfully

Status: revision 4, being implemented as two draft PRs. Earlier revisions are kept outside the repo (/home/coder/wt/PLAN-revision-2.md, PLAN-revision-3.md) and as local tags codagt-757-rev2/* and codagt-757-rev3/*.

Goal: the fewest lines that stop duplicate side effects and give truthful interrupt results. Rare cases that cost more than they are worth keep today's behavior (section 4).

1. Terms

Term Meaning
tool call ID A UUIDv5 of the chat ID, the assistant message ID, and the provider tool call ID, computed by chatd. Every replica and every attempt computes the same value from committed rows. For execute it is also the process ID.
Coder-Tool-Call-Id The request header that carries the tool call ID on start, edit, and write requests.
unresolved tool call A tool call in the latest assistant message without a committed result (existing term).
saved response The status, Content-Type, and body the agent returned for a tool call ID the first time.

2. User stories

  • US1 One run per tool call. A task retry or an ownership change sends the same tool call ID; the agent returns the saved response (for execute, the running or exited process) instead of running again. Holds while the agent keeps running.
  • US2 Interrupt stops unresolved execute. Foreground and background processes of unresolved calls are killed. The result is "tool call was canceled while running" with the output so far, or the real result if the command had finished.
  • US3 Interrupted edits have a definite result. "tool call was canceled before it ran; no changes were made" if the agent never received the edit, otherwise the real edit result.
  • US4 No run after an interrupt. A start, edit, or write that reaches the agent after the interrupt's cancel is refused.
  • US5 Timeouts are not restarted. The agent keeps waitUntil from the first start, so a retried foreground execute waits only for the rest of its timeout.
  • US6 Processes the model knows about survive. Only unresolved calls are canceled.
  • US7 Old agents behave as today. They ignore the header; the cancel route responds with 404, which gives today's generic interrupt result. No detection code.
  • US8 Chats stay apart. The agent keys tool call records and processes by chat ID. A tool call ID is read only from a request that has a chat ID; a process started without a chat ID is keyed by the zero UUID.

3. Design

Agent (agent/agenttoolcall, agentproc)

  • Coder-Tool-Call-Id is read by agentchat.Middleware into agentchat.Context.ToolCallID, next to the chat ID (and logged as tool_call_id). Handlers and the middleware below read it from the context.

  • A table keyed by chat ID and tool call ID. A record is running (with a channel that closes when the run finishes), done (with the saved response), or canceled.

  • One middleware, registered once for the whole agent API right after agentchat.Middleware: a request whose context carries a tool call ID runs once per chat and ID. Without a tool call ID: unchanged. With it:

    Record Action
    none run the handler once, save its response, return it
    running or done wait, return the saved response
    canceled 409, do nothing

    A handler panic saves a 500 saying the outcome is unknown, so repeats and cancels do not wait forever.

  • POST /api/v0/tool-calls/{id}/cancel, mounted from Table.Routes(): no record stores a canceled record, kills a process with that ID (it outlives the record), and responds with received: false. A record is marked canceled, the cancel waits for the run to finish, kills its process if the ID has one, and responds with received: true and the saved response.

  • Processes are keyed by chat ID and process ID. A process started with a tool call ID uses it as its process ID; a start for a key that already has a process returns that process. Output, signal, list, and kill see only the requester's chat's processes. The start request's timeout_ms sets the process's waitUntil at the first start; a repeat does not move it, and the process keeps running after it. A start with both background and a non-zero timeout_ms is rejected with 400, since only regular output waits read background processes. A blocking output wait with timeout_from_start_process=true (the execute tool's own wait) returns at exit, at waitUntil, or at the existing 5-minute cap; other waits, such as process_output's, ignore waitUntil. The output response reports timed_out (running past waitUntil) and canceled (killed by a cancel).

  • Adding a record deletes records that no request or cancel has used for an hour (forgetAfter), to bound memory. chatd resends every unresolved call on each retry, and a long batch is retried about every 15 minutes, so a batch that chatd is still retrying keeps its records.

  • Exited processes stay listed for an hour after they exit (5 minutes on main). Redesigning this retention is CODAGT-1220.

SDK (codersdk/workspacesdk)

  • CoderToolCallIDHeader, WithToolCallID(ctx, id), ToolCallIDFromContext(ctx). Only StartProcess, EditFiles, and WriteFile send the header, because only they act.
  • CancelToolCall(ctx, id) (CancelToolCallResponse, error); CancelToolCallResponse{Received bool; Status int; ContentType string; Body []byte} with one decoder per saved response, each calling the decode code of the method it mirrors: StartProcessResult(), EditFilesResult(), WriteFileResult().
  • StartProcessRequest.TimeoutMs; ProcessOutputOptions.TimeoutFromStartProcess; ProcessOutputResponse.TimedOut, Canceled.

chatd (coderd/x/chatd)

  • The dispatcher gives each tool call's context its tool call ID (chattool.ToolCallIDs), attached with WithToolCallID. Only the three acting SDK methods send it. A call whose provider tool call ID is empty or repeats in the message gets none and behaves as today, since calls sharing an ID would get each other's saved responses.

  • execute foreground sends timeout_ms, waits with timeout_from_start_process=true, and waits until the output response reports exit or timed_out. chatd's own timeout, counted from each attempt, stays as a backstop, which gives an old agent (it ignores timeout_ms) today's behavior.

  • The interrupt task reads the unresolved execute, edit_files, and write_file calls that have a tool call ID in its locked read (the same read that checks the chat is still in the state the task started for). After releasing the lock, it dials the agent once and sends one cancel per call, all at once, under one 30-second timeout. A cancel waits for a running call to finish, so sending them at once keeps one slow call from using up the others' time. A panic in one cancel gives that call the generic text. Results:

    Response execute edit_files / write_file
    received: false read the output: not found gives the plain-text error tool call was canceled before it ran; no changes were made; found is handled like a saved success tool call was canceled before it ran; no changes were made
    saved error response the start error, as today the edit or write error, as today
    saved success read the output after the process exits (the kill does not wait for the exit): canceled gives tool call was canceled while running with the output so far, marked "canceled": true (the UI shows the killed marker); exited gives the real result the edit or write result
    404, error, timeout today's generic text, and a warning log today's generic text, and a warning log

    Results go into the existing commit path (committedPendingLocalToolCancellationMessages, FinishInterruption) as a map by provider tool call ID.

4. Not handled

Retries keep today's behavior (the tool runs again):

  • An agent restart mid-call. The restart already killed the process.
  • A retry more than an hour after chatd last sent the call (for example after a coderd outage), once a newer tool call deleted the record, unless the process is still listed.
  • An agent that stays connected but never responds.
  • Two agent processes with one token at the same time.
  • Windows: a cancel kills the sh process only, as process_signal does today.
  • A process that exits on its own just before a cancel's kill can be reported as canceled while running.

An execute that finished early in a batch that stays unresolved (for example next to a long sibling) keeps its record while chatd retries, but its process is removed an hour after it exits, the next time any chat lists processes. Later retries then get an output error ("get process output: ...") instead of its result. It does not run again, but its output is lost (CODAGT-1220).

After a retry, an execute result's wall_duration_ms counts only the last attempt (a 20-minute timeout showed "for 5m" in UAT): chatd measures it per attempt, and the agent's responses do not carry the process's start time. The timeout itself counts from the first start (CODAGT-1243).

Interrupts give a false "canceled before it ran" when the agent has no record of a call that did reach it. Today these cases give the generic text. Accepted to keep the change small; a complete fix needs a record that survives the agent process or a comparison of the call's age with the agent's uptime, both rejected (section 6):

  • an edit or write interrupted more than an hour after chatd last sent it (only after an outage, since every retry sends it again);
  • an agent restart, or the chat moving to another agent, after the call ran;
  • a rolling chatd upgrade in which an old replica ran the call without the header;
  • an execute whose process exited more than an hour earlier and was dropped from the process list after the record was deleted.

A workspace that cannot be reached during the interrupt records no cancel, so a request that arrives later still runs. The committed result is today's generic text.

5. PRs

  1. feat: run chat tool calls once per tool call ID on the agent #29946 mafredri/codagt-757/01-sdk-tool-call: agent and SDK.
  2. feat: send tool call IDs and cancel unresolved calls on interrupt #29954 mafredri/codagt-757/02-agent-tool-call-records: chatd.

#29958, #29965, #29976, and #29978 are closed as superseded. Branches are rewritten from main and force-pushed (operator approval, 2026-09-28).

6. Decisions (2026-09-28, operator)

  • LOC reduction first; do not pay for rare cases; relax user stories to allow simpler designs.
  • No agent protobuf, manifest, agent API version, or coderd database changes. The contract lives in the agent HTTP API.
  • No comparison between chatd's and the agent's clocks, and no old-agent detection. The one-hour expiry uses only the agent's clock.
  • Interrupts kill unresolved background processes and processes past waitUntil (accepted earlier).
  • Edits, archives, and other generic cancellations sending a cancel first: follow-up work (CODAGT-1222).
  • Implement fast; reviews and fixes afterwards. Hooks are off in this workspace.
  • After review: received: false for execute falls back to the process list, and the remaining false results are documented in section 4 rather than fixed. Attach and kill respect chat boundaries.
  • Third-party review: adopted the panic fix, chat checks, the empty or repeated provider ID guard, the section 4 additions, and tests for concurrent requests, panics, cancels during a run, and cross-chat access. Rejected: buffering request bodies (write_file is atomic, so a cut-off upload saves a true error); retention counted from run end (recorded runs are short; the process list covers execute); restricting IDs to three tools and per-route middleware (the SDK sends the header only from the three acting methods); a mixed clock (already one clock); reporting time left on retry (the timeout counts from the first start).
  • Only the execute tool's own wait stops at waitUntil (timeout_from_execute=true); process_output waits keep their own timeout. A background start with a timeout is rejected (operator, after the first review round).
  • 2026-09-29, after Codex and Copilot review: interrupt cancels are sent at once, and the timeout is 30 seconds instead of 10, because a chance of a real result is worth the wait more than unknown results. Processes are keyed by chat ID and process ID; non-chat requests seeing chat processes was a bug, not compatibility to keep, and the key makes Codex's cross-chat start race unreachable. Exited processes stay an hour instead of 5 minutes; the redesign is CODAGT-1220. entryMaxAge is renamed forgetAfter.
  • 2026-09-29, second review round: the interrupt reads its cancelable calls under the chat lock (ARCHITECTURE.md runner rule); the output read after a cancel waits for the exit; each cancel goroutine recovers panics. Not changed: a cancel that gives up (timeout, or chatd went away) does not kill a process whose start is still running, since the interrupt then records the generic text; the size cap keeps the default budget.
  • 2026-09-29, third-party finding: a record's age was counted from when it was added, so in a batch with a long execute, an edit's record could be deleted while chatd still retried the batch, and the edit ran again. Every request or cancel that finds a record now resets its age. This replaces the earlier rejection of retention counted from run end.
  • 2026-09-29, UAT round 1 (FAIL on one UI regression: canceled and not-run execute cards read "Ran ... for 0ms"): the canceled result passes the agent's canceled flag through as "canceled": true, and the UI shows it with the marker it already uses for commands killed by process_signal; "not run" is a plain-text error like the edits' "not applied" ("Failed to run"); a zero wall_duration_ms is left out. Rejected: marking the canceled JSON result is_error, which says "Failed to run" for a command that ran, makes interrupts the only execute failures flagged as errors, and would drop the output from the model on replay (only the error field of an is_error JSON part reaches it). The collapsed card not marking any failed command (exit code, timeout) is a separate UI question. The per-attempt duration after a retry is documented in section 4 (operator: a chatd and agent API issue; a WebSocket between them in the future would remove the blindness, and also the start-then-wait of execute).
  • 2026-09-30, operator: one result text per outcome for all three tools, without naming the transport: tool call was canceled before it ran; no changes were made and tool call was canceled while running. CancelToolCall has a case per tool name and rejects other tools before sending. The agent's cancel hook is named cancel, not kill, because it also serves edits. ARCHITECTURE.md states the at-most-once exception and the interrupt's cancel step instead of a TODO. Comments keep only what the code does not show. TimeoutFromExecute (timeout_from_execute) is renamed TimeoutFromStartProcess (timeout_from_start_process), because it is part of the agent process API and should not be named after a chatd tool.
  • Tests: each behavior is asserted once, at the highest layer that reaches it (real SDK and agent; one chatd end-to-end interrupt test with a real agent); unit tests only for what those cannot reach. Tests are deterministic: fake clock for time, no real-time deadlines or timed polls, assertions that hold in every interleaving.

🤖 This PR was created with the help of Coder Agents, and will be reviewed by a human. 🏂🏻

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

chatgpt-codex-connector[bot] reacted with thumbs up emoji
All reactions
  • 👍 1 reaction
@linear-code

linear-code Bot commented Sep 25, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

CODAGT-757

CODAGT-1220

CODAGT-1222

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@github-actions github-actions Bot assigned mafredri Sep 25, 2026
@mafredri mafredri mentioned this pull request Sep 25, 2026
feat: send tool call IDs and cancel unresolved calls on interrupt #29954
Merged
@mafredri
mafredri added this pull request to stack #29985 September 26, 2026 12:50
This was referenced Sep 26, 2026
feat(coderd/x/chatd): run execute at most once per tool call #29965
Closed
feat: run tool call processes at most once #29958
Closed
feat(coderd/x/chatd): resolve execute tool calls on user interrupt #29976
Closed
feat: run file edit and write tool calls at most once #29978
Closed
@mafredri
mafredri force-pushed the mafredri/codagt-757/01-sdk-tool-call branch 2 times, most recently from 62535b5 to c7b47ba Compare September 28, 2026 12:50
@mafredri mafredri changed the title feat(codersdk/workspacesdk): add tool call identity to agent requests feat: run chat tool calls once per tool call ID on the agent Sep 28, 2026
@mafredri
mafredri removed this pull request from stack #29985 September 28, 2026 13:06
@mafredri
mafredri added this pull request to stack #30043 September 28, 2026 13:07
@mafredri
mafredri force-pushed the mafredri/codagt-757/01-sdk-tool-call branch 4 times, most recently from eda760c to 026131a Compare September 28, 2026 15:10
This was referenced Sep 28, 2026
perf(coderd/x/chatd): reuse agent connection across steps #30051
Draft
perf(coderd/x/chatd): keep agent conn across steps #30052
Closed
perf: reuse workspace agent API connections in chatd #30053
Draft
@mafredri
mafredri requested a balanced review from Copilot September 29, 2026 08:43
@mafredri

mafredri commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T08:46:56.039913Z 2a7852f Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Copilot started reviewing on behalf of mafredri September 29, 2026 08:43
View session
chatgpt-codex-connector[bot]
chatgpt-codex-connector Bot reviewed Sep 29, 2026
View reviewed changes

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

Copy link
Copy Markdown

There was a problem hiding this comment.

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: 12924f189b

ℹ️ 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".

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread agent/agentproc/process.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Copilot AI reviewed Sep 29, 2026
View reviewed changes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Expiration and process reaping can return stale process IDs, while the existing concurrent cross-chat start race remains unresolved.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)
  • Medium severity Retain tool-call processes until record expiry · New
  • Medium severity Expire stale tool-call records during retry lookup · New
  • Medium severity Ignore expired records during cancellation · New
What changed in this PR

Adds agent-side deduplication and cancellation for chat tool calls, including process timeout and ownership tracking.

Changes:

  • Adds tool-call IDs and cancellation APIs to the workspace SDK.
  • Records and replays tool-call responses per chat.
  • Extends process execution with cancellation and timeout state.
File Description
agent/​agent.go Initializes tool-call tracking.
agent/​api.go Registers middleware and cancellation routes.
agent/​agentchat/​headers.go Parses tool-call headers.
agent/​agentchat/​log.go Adds tool-call context and logging.
agent/​agentchat/​log_test.go Tests tool-call log fields.
agent/​agentproc/​api.go Adds process timeout and cancellation behavior.
agent/​agentproc/​api_test.go Tests process attachment and timeout validation.
agent/​agentproc/​process.go Associates processes with tool-call IDs.
agent/​agenttoolcall/​agenttoolcall.go Implements run-once and cancellation records.
agent/​agenttoolcall/​agenttoolcall_test.go Tests deduplication and cancellation concurrency.
agent/​agent_test.go Adds end-to-end agent behavior tests.
codersdk/​workspacesdk/​agentconn.go Exposes headers, cancellation, and timeout fields.
codersdk/​workspacesdk/​agentconnmock/​agentconnmock.go Updates the generated SDK mock.
codersdk/​workspacesdk/​toolcall.go Defines tool-call context and saved-response decoding.
Files not reviewed (1)
  • codersdk/workspacesdk/agentconnmock/agentconnmock.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread agent/agentproc/process.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread agent/agenttoolcall/agenttoolcall.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

Comment thread agent/agenttoolcall/agenttoolcall.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mafredri
mafredri requested a balanced review from Copilot September 29, 2026 10:09
@mafredri

mafredri commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

Copilot started reviewing on behalf of mafredri September 29, 2026 10:09
View session
chatgpt-codex-connector[bot]
chatgpt-codex-connector Bot reviewed Sep 29, 2026
View reviewed changes

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

Copy link
Copy Markdown

There was a problem hiding this comment.

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: b616076fd2

ℹ️ 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".

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread agent/agenttoolcall/agenttoolcall.go Resolved

Uh oh!

There was an error while loading. Please reload this page.

@mafredri

mafredri commented Sep 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 96c13dccba

ℹ️ 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".

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

ibetitsmike
ibetitsmike approved these changes Sep 30, 2026
View reviewed changes
mafredri added 14 commits September 30, 2026 06:52
@mafredri
feat: run agent tool call requests once and cancel them
dbdd6fc
chatd retries and ownership changes can resend a start, edit, or write
for a tool call that already ran, and an interrupt cannot tell whether
the agent received the call. StartProcess, EditFiles, and WriteFile now
send Coder-Tool-Call-Id; the agent runs a request with it once per chat
and tool call ID and answers repeats with the saved response.
POST /api/v0/tool-calls/{id}/cancel refuses calls the agent never
received and kills the process of calls it did. The agent keeps the
execute deadline from timeout_ms, so a retried start does not restart
it, and process output reports timed_out and canceled.
@mafredri
fix: keep tool call cancels truthful and chat-scoped
ecf014b
A cancel for a tool call the agent has no record of answered "not
received" even when a process started with that ID was still running,
for example after the one-hour record expired. The cancel now kills
such a process too, so chatd can read its output instead of reporting
"not run".

A handler panic left the entry running forever, so repeats and cancels
for that ID waited until their callers gave up. The run now saves a 500
saying the outcome is unknown before the panic reaches httpmw.Recover.

Attaching to a process and killing it looked the process up by ID
only. Both now require the process to belong to the requesting chat,
as the output and signal routes already do.
@mafredri
fix: keep process_output waits independent of the start timeout
9f976ec
Every blocking output wait on a process started with timeout_ms ended
at that timeout. After a foreground execute timed out, a later
process_output wait on the same process therefore returned at once
instead of waiting for its own timeout. Only the execute tool's own
wait should stop there, so it now asks for that with
stop_at_start_timeout, and other waits keep their own timeout.
@mafredri
test(agent): test tool call behavior through tables
27fd06a
The tool call tests now state each behavior as a table case: what came
before the request, and what the request answers. The integration test
covers each acting SDK method the same way, checking that a repeat does
not act again, instead of re-testing the table's cases. The test-only
export is gone: the cancel-during-run test detects the recorded cancel
from the refusal a repeat gets.
@mafredri
fix: rename the execute wait option to timeout_from_execute
7707a52
stop_at_start_timeout read like a timeout for starting a process. The
option makes an output wait also end at the execute timeout, so it is
now TimeoutFromExecute (timeout_from_execute=true).

Background processes are only read by regular output waits, so the
agent now rejects a start that sets both background and timeout_ms.

The wait behavior is now tested through the real agent with a timeout
that has already passed, so the outcome does not depend on timing, and
the test also catches the SDK and agent disagreeing on the parameter.
The fake-clock wait test and the separate kill test are gone: the
plain-wait case covers the kill.
@mafredri
test(agent/agenttoolcall): keep only what the real agent cannot reach
d9a8099
TestAgent_ToolCall covers running once, repeats, and cancels through
the real agent, so the table's copies of those cases are gone. The
cancel-during-run test now releases the run when the cancel reaches
the table instead of polling, and asserts only what holds whichever
of the two finishes first.
@mafredri
test(agent): catch timeout, header, and cross-chat regressions
5658045
Removing the time check from timed_out, or sending the tool call ID
from ProcessOutput, passed every test. The execute timeout test now
has a case with a timeout that cannot pass during the test, and both
agent tests send the tool call context on every call, as chatd does,
so a method that must not send the header gets a saved response.

The cross-chat test read private fields, and its check that another
chat's cancel did not kill the process read Running right after the
kill, which cannot see a wrong kill. It now runs through the real
agent with two chats and checks Canceled, which a cancel sets before
it answers. Only the repeated start from the same chat stays a unit
test, since the real agent's table answers repeats first.

Each execute timeout case now cancels its process on cleanup, so the
agent no longer waits for "sleep 300" when it closes. The tool call
ID log field is asserted in one middleware test instead of two, and
the cancel-during-run comment now says the run is released when the
cancel request reaches the cancel route.
@mafredri
fix(agent/agentproc): key processes by chat ID and process ID
de8bfda
Requests without a chat ID could list, read, and signal every chat's
processes, and a chat could read processes started without one. Keying
the map by chat ID and process ID gives each chat, and requests without
a chat ID (the zero UUID, as in the tool call middleware), only their
own processes, and replaces the scattered ownership checks.

Two chats that start the same tool call ID now get separate processes
instead of an error for the second. The tool call middleware already
serializes starts per chat ID and tool call ID, so start needs no new
locking.
@mafredri
refactor(agent/agenttoolcall): rename entryMaxAge to forgetAfter
af99752
The old comment said a retry after the age runs the tool call again,
but a record is only deleted when a later record is added. The comment
now says that, and what a retry and a cancel get after deletion.
@mafredri
fix(agent/agentproc): keep exited processes for an hour
667b7e0
A retried execute gets the saved start response and then reads the
process output, and the tool call record lives for an hour. With a
5-minute reap age the process could be gone while the record still
answered retries with its ID. An hour after exit is at least as long
as the record lives.
@mafredri
docs(agent): correct process retention and tool call ID comments
681d009
@mafredri
test(agent): drop redundant running check
93913e1
@mafredri
docs: say responds instead of answers in comments
97af2a0
@mafredri
fix(agent): keep tool call records while they are requested
2a7852f
chatd commits tool results once per batch and resends every call in
the batch on each retry. A fast call in a batch with an execute that
runs for over an hour lost its record to the first unrelated add, so
the next retry ran it again. Count the hour from the last request or
cancel for a record instead of from when it was added.
@mafredri
mafredri force-pushed the mafredri/codagt-757/01-sdk-tool-call branch from 96c13dc to 2a7852f Compare September 30, 2026 06:57
@mafredri
mafredri marked this pull request as ready for review September 30, 2026 08:40
@mafredri

mafredri commented Sep 30, 2026

Copy link
Copy Markdown
Member Author

@codex review

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026

Copy link
Copy Markdown

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

Reviewed commit: 2a7852f8b5

ℹ️ 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".

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@mafredri
refactor(agent): rename the tool call kill hook to cancel
e9b05b3
agenttoolcall cancels execute, edit_files, and write_file calls, so a
hook named kill described only the execute case. The process API keeps
killing the process; only the names change.
@mafredri
mafredri force-pushed the mafredri/codagt-757/01-sdk-tool-call branch from a820e96 to dd9f955 Compare September 30, 2026 10:23
@mafredri
docs: rewrite tool call comments
bbbea78
Keep only comments that state a contract, invariant, or reason the code
does not show, and write them as Go doc comments.
@mafredri
mafredri force-pushed the mafredri/codagt-757/01-sdk-tool-call branch from dd9f955 to bbbea78 Compare September 30, 2026 10:29
mafredri added 2 commits September 30, 2026 10:32
@mafredri
refactor: rename TimeoutFromExecute to TimeoutFromStartProcess
f4007c2
The option belongs to the agent process API, so name it after the
StartProcess timeout it applies instead of chatd's execute tool. The
query parameter becomes timeout_from_start_process.
@mafredri
docs: put short struct field comments on the field line
26bff74
@mafredri
mafredri merged commit 1ba09d2 into main Sep 30, 2026
29 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@mafredri
mafredri deleted the mafredri/codagt-757/01-sdk-tool-call branch September 30, 2026 11:08
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

Copilot code review Copilot
Copilot review effort
Applies to this pull request for everyone.Learn more about Copilot code review.
Copilot left review comments
@chatgpt-codex-connector chatgpt-codex-connector[bot] chatgpt-codex-connector[bot] left review comments
@ibetitsmike ibetitsmike ibetitsmike approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@mafredri mafredri

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@mafredri @ibetitsmike
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.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.