Skip to content

fix(agent): surface interrupted local tool calls on every terminal path - #3782

Merged
kwakayama merged 4 commits into
mainfrom
fix/issue-3737
Aug 16, 2026
Merged

fix(agent): surface interrupted local tool calls on every terminal path#3782
kwakayama merged 4 commits into
mainfrom
fix/issue-3737

Conversation

@kwakayama

Copy link
Copy Markdown
Contributor

fix(agent): surface interrupted local tool calls on every terminal path

The terminal announce for an incomplete local tool call was gated on
declinedRecoveryForExposedReasoning, so the buffered tool-input-start
was only flushed when the step happened to carry a persisted reasoning
block. inputAnnounced stays false for exactly this class of call, so
emitSse: toolCall.inputAnnounced === true suppressed the
tool-output-error on every other terminal path — maxSteps exhaustion, a
second interruption in the same run, and an exposed local sibling or
provider-executed call. The stream ended with no text, no tool call and
no error while history recorded a failure.

Pass announceInput: true unconditionally in the !shouldContinue loop.
Whether recovery was declined for reasoning, a spent step budget or an
exposed sibling is invisible to the user, so it must not decide whether
the failure renders. recordIncompleteLocalToolError still guards on
providerExecuted, incompleteness and a final result, so only genuinely
truncated local calls are announced, and announceStreamedToolCallInput
is idempotent for anything already surfaced upstream.

Exactly one failure event per call still reaches the wire. The
tool-input-error emission at index.ts:~2332 is gated on the same
inputAnnounced flag and runs before this loop, so it stays dead on this
path; all four affected tests pin tool-input-error at 0 alongside a
tool-output-error count of 1.

Issue point 1 (bare {} placeholders): they now surface on the terminal
path. preserveRecoverablePlaceholderToolCalls already keeps such a call
in the assistant message and recordIncompleteLocalToolError already
writes a matching tool-result error and an errored toolCalls entry, so
withholding it from the wire made persisted history and the live stream
disagree about the same run. A placeholder that is still recoverable is
unaffected: it is re-asked rather than terminalized, so it never enters
this loop.

Two existing assertions in refresh.test.ts changed:

  • "fails closed after a local sibling was exposed, with or without a
    final result": tool-output-error count 0 -> 1. Failing closed means
    not re-running the batch, not hiding the truncation. The exposed
    sibling is complete, so it never enters the incomplete branch and
    contributes no error; the single error belongs to
    truncated-agent-after-exposure, which the same step already writes
    into history. Tightened to assert the toolCallId on both the announce
    and the error, plus tool-input-error 0.

  • "recovers a placeholder after assistant text only once":
    tool-output-error count 0 -> 1. The test already asserts that the
    step-2 placeholder toolu_repeated_placeholder is kept in the
    assistant message with a tool-result error and an errored entry in
    toolCalls — that is the history/wire disagreement above, in the same
    test. The step-1 placeholder toolu_placeholder_after_text is
    recovered rather than terminalized, and a new assertion pins its
    announce count at 0 so the once-only recovery guarantee is still
    covered.

Added "surfaces a truncated local tool call when maxSteps exhaustion ends
the run", which fails on the pre-fix runtime with 0 announces for the
truncated call.

Closes #3737


Reviewer: read the assertion changes first

This PR changes two existing assertions from 0 to 1. That is the deliberate behaviour decision #3737 asks for, not a test being relaxed to go green, and it is the thing to scrutinise. Both are justified line by line in the commit message above.

The invariant the issue calls out as a hard requirement — exactly one failure event on the wire — is preserved and now pinned in all four affected tests: tool-output-error count 1 alongside tool-input-error count 0.

Red evidence: the new maxSteps test fails on the pre-fix runtime with 0 announces for the truncated call.

Closes #3737

The terminal announce for an incomplete local tool call was gated on
`declinedRecoveryForExposedReasoning`, so the buffered `tool-input-start`
was only flushed when the step happened to carry a persisted reasoning
block. `inputAnnounced` stays false for exactly this class of call, so
`emitSse: toolCall.inputAnnounced === true` suppressed the
`tool-output-error` on every other terminal path — maxSteps exhaustion, a
second interruption in the same run, and an exposed local sibling or
provider-executed call. The stream ended with no text, no tool call and
no error while history recorded a failure.

Pass `announceInput: true` unconditionally in the `!shouldContinue` loop.
Whether recovery was declined for reasoning, a spent step budget or an
exposed sibling is invisible to the user, so it must not decide whether
the failure renders. `recordIncompleteLocalToolError` still guards on
`providerExecuted`, incompleteness and a final result, so only genuinely
truncated local calls are announced, and `announceStreamedToolCallInput`
is idempotent for anything already surfaced upstream.

Exactly one failure event per call still reaches the wire. The
`tool-input-error` emission at index.ts:~2332 is gated on the same
`inputAnnounced` flag and runs before this loop, so it stays dead on this
path; all four affected tests pin `tool-input-error` at 0 alongside a
`tool-output-error` count of 1.

Issue point 1 (bare `{}` placeholders): they now surface on the terminal
path. `preserveRecoverablePlaceholderToolCalls` already keeps such a call
in the assistant message and `recordIncompleteLocalToolError` already
writes a matching tool-result error and an errored `toolCalls` entry, so
withholding it from the wire made persisted history and the live stream
disagree about the same run. A placeholder that is still recoverable is
unaffected: it is re-asked rather than terminalized, so it never enters
this loop.

Two existing assertions in refresh.test.ts changed:

- "fails closed after a local sibling was exposed, with or without a
  final result": `tool-output-error` count 0 -> 1. Failing closed means
  not re-running the batch, not hiding the truncation. The exposed
  sibling is complete, so it never enters the incomplete branch and
  contributes no error; the single error belongs to
  `truncated-agent-after-exposure`, which the same step already writes
  into history. Tightened to assert the toolCallId on both the announce
  and the error, plus `tool-input-error` 0.

- "recovers a placeholder after assistant text only once":
  `tool-output-error` count 0 -> 1. The test already asserts that the
  step-2 placeholder `toolu_repeated_placeholder` is kept in the
  assistant message with a tool-result error and an errored entry in
  `toolCalls` — that is the history/wire disagreement above, in the same
  test. The step-1 placeholder `toolu_placeholder_after_text` is
  recovered rather than terminalized, and a new assertion pins its
  announce count at 0 so the once-only recovery guarantee is still
  covered.

Added "surfaces a truncated local tool call when maxSteps exhaustion ends
the run", which fails on the pre-fix runtime with 0 announces for the
truncated call.

Closes #3737
@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Limit details: You’ve used all 3 included reviews currently available under your plan.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 983b48a9-453b-4dc3-8153-8fd9b0b772c8

📥 Commits

Reviewing files that changed from the base of the PR and between 7b42a75 and 4ae6408.

📒 Files selected for processing (6)
  • docs/api-reference/veryfront/agent.md
  • src/agent/ag-ui/browser-encoder.test.ts
  • src/agent/ag-ui/browser-encoder.ts
  • src/agent/runtime/index.ts
  • src/agent/runtime/refresh.test.ts
  • src/internal-agents/ag-ui-sse.test.ts

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

📦 Client bundle boundary

Entrypoint Modules Source size Server leaks
src/index.client.ts 321 1908 KiB ✅ 0

A server module in a client graph aborts hydration in the browser. New leaks fail CI; known leaks are tracked in scripts/lint/client-bundle-baseline.json to burn down.

@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: 17cf097ef3

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

// only genuinely truncated local calls are announced, and
// `announceStreamedToolCallInput` is idempotent for any already
// surfaced upstream.
await recordIncompleteLocalToolError(toolCall, { announceInput: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close the incomplete tool input before emitting its result

When this newly widened branch handles a truncated call without exposed reasoning, such as maxSteps exhaustion or a second interruption, it emits tool-input-start and partial deltas followed directly by tool-output-error. The AG-UI mapping in src/agent/ag-ui/browser-encoder.ts:926-932 converts an output error only to ToolCallResult, unlike tool-input-error and tool-input-available, which call completeToolInput first. Consequently AG-UI clients receive ToolCallStart, ToolCallArgs, and ToolCallResult without the required ToolCallEnd, leaving the tool-input lifecycle open. Terminalizing this path must either emit an input terminal event or teach the AG-UI encoder to close an incomplete input before its result.

Useful? React with 👍 / 👎.

Addresses the review finding on #3782.

A truncated local tool call terminalizes as `tool-input-start` (plus any
partial deltas) and then straight to `tool-output-error`. In the browser
encoder, `tool-input-available` and `tool-input-error` both close the input
through `completeToolInput`, which emits `ToolCallEnd`; the `tool-output-error`
branch only emitted `ToolCallResult`. AG-UI clients were therefore left with
ToolCallStart, ToolCallArgs and ToolCallResult and no ToolCallEnd, holding the
tool-input lifecycle open.

This predates #3782 — the declined-reasoning path #3735 added already produced
it — but widening the announce to every terminal path takes it from one rare
case to all of them, so it is fixed here rather than filed.

Fixed in the encoder rather than by emitting an extra runtime event, so the
lifecycle closes for any producer of `tool-output-error`, not just this path.
`openToolCallIds` tracks calls whose `ToolCallStart` has been emitted and not
yet closed; it is distinct from `streamedToolInputIds`, which records whether
args were streamed rather than whether the call is open.

No synthetic args are emitted on this path. `completeToolInput` back-fills a
`ToolCallArgs` when none streamed, but here the model never committed any
input, and writing `{}` would claim it did.

Red: "closes an open tool input before emitting its output error" fails with
ToolCallResult alone. A second test pins that a call already closed by
`tool-input-available` does not get a duplicate ToolCallEnd — that one passes
before and after, and exists to stop the fix over-reaching.

Green: browser-encoder 3 passed (24 steps); full ag-ui suite 29 passed
(157 steps); runtime refresh.test.ts 2 passed (56 steps).
Two consequences of closing the tool input on `tool-output-error`.

`src/internal-agents/ag-ui-sse.test.ts` opened `tool-1` with
`tool-input-start` and went straight to `tool-output-error`, asserting the
result alone. That expectation was encoding the defect: the sequence it
described leaves the client holding an open tool-input lifecycle for the rest
of the run. It now expects `ToolCallEnd` before `ToolCallResult`, with the
reason recorded inline.

This is the only stale expectation. Every suite in `src/` that references
`tool-output-error` was run together to check — 31 passed, 298 steps, 0 failed
— rather than discovering them one CI round at a time.

`docs/api-reference/veryfront/agent.md` is regenerated: `openToolCallIds` is a
documented field on the exported `AgUiBrowserEncoderState`, so `deno task docs`
output moved with it and `ci (lint)` fails on a stale reference.
@kwakayama

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@codex review

Head has moved to 15b4728 since the last review. Two commits since: the AG-UI ToolCallEnd fix for the finding you raised, and an update to a stale expectation in src/internal-agents/ag-ui-sse.test.ts that was asserting the unclosed lifecycle as correct. Please re-review the head.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown

@kwakayama I will re-review pull request #3782 at 15b47289f. I will verify the AG-UI ToolCallEnd lifecycle fix and the updated test expectation.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 15b47289fe

ℹ️ 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 src/agent/ag-ui/browser-encoder.ts Outdated
* a `ToolCallEnd`. Distinct from `streamedToolInputIds`, which tracks
* whether any args were streamed, not whether the call is still open.
*/
openToolCallIds: Set<string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the new tool-call tracker backward compatible

When a consumer passes a manually constructed AgUiBrowserEncoderState using the previously valid public shape, mapping tool-input-start or tool-output-error now calls add or delete on an undefined openToolCallIds and crashes. This state type and the mapping function are re-exported from veryfront/agent, and the adjacent reasoningSpanIndex is already optional specifically to preserve older state objects, so make this field optional and initialize it lazily rather than requiring every existing caller to adopt it immediately.

AGENTS.md reference: AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

…urvive

Review finding on the head commit, and correct. `AgUiBrowserEncoderState` is
re-exported from `veryfront/agent` (src/agent/index.ts:752), so a consumer can
hold a state object built against the shape the type had before this tracker
existed. As a required field, the first `tool-input-start` called `.add` on
undefined and crashed.

Optional and populated lazily now, which is exactly why `reasoningSpanIndex`
directly above it is optional — its comment already says "Optional so a state
object built before this counter existed stays valid".

`closeOpenToolInput` checks `?.delete(...) !== true` rather than `!...delete()`
so an absent set reads as "nothing open" instead of throwing.

Covered by a test that deletes the field from a freshly created state and
drives the same start/output-error sequence, asserting it still closes the
lifecycle rather than crashing.
@kwakayama

Copy link
Copy Markdown
Contributor Author

@codex review
@coderabbitai review

Head is now 4ae6408. Since your last pass: openToolCallIds is optional and lazily initialized, per your backward-compatibility finding, plus a test that deletes the field from a fresh state and drives the same sequence. Docs regenerated.

@coderabbitai

coderabbitai Bot commented Aug 16, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== HEAD =='
git rev-parse HEAD
git show -s --format='%H%n%s%n%b' HEAD

printf '%s\n' '== Changed files since previous head =='
git diff --name-status 15b47289f..HEAD

printf '%s\n' '== Diff statistics =='
git diff --stat 15b47289f..HEAD

printf '%s\n' '== Relevant diff =='
git diff --find-renames --find-copies 15b47289f..HEAD -- \
  src/internal-agents/ag-ui-sse.ts \
  src/internal-agents/ag-ui-sse.test.ts \
  docs

Length of output: 20301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Source outline =='
ast-grep outline src/agent/ag-ui/browser-encoder.ts --items all

printf '%s\n' '== Test outline =='
ast-grep outline src/agent/ag-ui/browser-encoder.test.ts --items all

printf '%s\n' '== Implementation and test diff =='
git diff --find-renames --find-copies 15b47289f..HEAD -- \
  src/agent/ag-ui/browser-encoder.ts \
  src/agent/ag-ui/browser-encoder.test.ts

printf '%s\n' '== All openToolCallIds references =='
rg -n -C 5 '\bopenToolCallIds\b' src/agent/ag-ui

Length of output: 12745


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
printf '== Merge base ==\n%s\n' "$base"

printf '%s\n' '== Full PR changed files =='
git diff --name-status "$base"..HEAD

printf '%s\n' '== Full implementation diff =='
git diff --find-renames "$base"..HEAD -- src/agent/ag-ui/browser-encoder.ts

printf '%s\n' '== Full test diff =='
git diff --find-renames "$base"..HEAD -- src/agent/ag-ui/browser-encoder.test.ts

printf '%s\n' '== Lifecycle implementation context =='
sed -n '540,625p' src/agent/ag-ui/browser-encoder.ts
sed -n '850,940p' src/agent/ag-ui/browser-encoder.ts
sed -n '1020,1095p' src/agent/ag-ui/browser-encoder.ts

printf '%s\n' '== Lifecycle test context =='
sed -n '880,1055p' src/agent/ag-ui/browser-encoder.test.ts
rg -n -C 4 '"ToolCallEnd"|tool-output-error|tool-input-start' src/agent/ag-ui/browser-encoder.test.ts

Length of output: 26163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"

printf '%s\n' '== Runtime implementation diff =='
git diff --find-renames "$base"..HEAD -- src/agent/runtime/index.ts

printf '%s\n' '== Runtime regression-test diff =='
git diff --find-renames "$base"..HEAD -- src/agent/runtime/refresh.test.ts

printf '%s\n' '== AG-UI SSE regression-test diff =='
git diff --find-renames "$base"..HEAD -- src/internal-agents/ag-ui-sse.test.ts

printf '%s\n' '== Runtime terminal-path context =='
rg -n -C 12 'announceInput|recordIncompleteLocalToolError|declinedRecoveryForExposedReasoning' \
  src/agent/runtime/index.ts

printf '%s\n' '== Changed integration-test context =='
rg -n -C 16 'ToolCallEnd|ToolCallResult|tool-output-error|truncated' \
  src/internal-agents/ag-ui-sse.test.ts

Length of output: 25527


@kwakayama I reviewed 4ae6408da.

The optional, lazy openToolCallIds change addresses the backward-compatibility issue. All tracker accesses now handle legacy state objects safely.

The AG-UI encoder emits one ToolCallEnd before ToolCallResult for truncated local tool calls. Completed input paths do not emit a duplicate end event.

The new regression tests cover the legacy state shape and the corrected lifecycle. I found no new issues.


🧠 Learnings used
Learnt from: kojiwakayama
Repo: veryfront/veryfront-code PR: 3726
File: src/agent/runtime/refresh.test.ts:1702-1705
Timestamp: 2026-08-14T19:44:42.295Z
Learning: In `src/agent/runtime/index.ts`, interrupted local tool-batch recovery records an error for every call. For an incomplete call, it emits a `tool-output-error` SSE event only when `StreamingToolCall.inputAnnounced === true`. A raw, unfinalized `tool-input-start` remains unannounced and therefore has no output SSE lifecycle event.
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 4ae6408da7

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

@kwakayama

Copy link
Copy Markdown
Contributor Author

@codex review

Head is 4ae6408 (your last pass was 15b4728). The delta is the backward-compatibility fix you asked for: openToolCallIds optional + lazily initialized, a test that deletes the field from a fresh state and drives the same start/output-error sequence, and regenerated API docs.

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: 4ae6408da7

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

@kwakayama

Copy link
Copy Markdown
Contributor Author

Review: 93/100 — merging

What was verified, not assumed

The riskiest part of this PR is that it changes two existing assertions from 0 to 1. That is the classic way to fake a green, so it got the most scrutiny:

  • Both went 0 → 1, not deleted.
  • Each is justified individually in the commit message, naming which call the surviving error belongs to and why.
  • tool-input-error is pinned at 0 in all four affected tests, preserving the issue's hard requirement of exactly one failure event on the wire.
  • The new maxSteps test fails on the pre-fix runtime with 0 announces.

Regression evidence: full ag-ui suite 29 passed (158 steps), refresh.test.ts 2 passed (56 steps), internal-agents/ag-ui-sse 3 passed (72 steps).

Two defects found during review, both fixed

Unclosed tool-input lifecycle (Codex P2). tool-input-available and tool-input-error close the input via completeToolInput; tool-output-error only emitted ToolCallResult. Clients got ToolCallStartToolCallArgsToolCallResult with no ToolCallEnd. Fixed in the encoder rather than by emitting an extra runtime event, so the lifecycle closes for any producer. This predates the PR — the #3735 path already produced it — but widening the announce takes it from one rare case to all of them, so it belonged here.

That fix then surfaced a third thing: src/internal-agents/ag-ui-sse.test.ts was asserting the defecttool-input-start then tool-output-error, expecting the result alone. Updated with the reason inline. I checked every suite in src/ referencing tool-output-error together (31 passed, 298 steps) rather than discovering stale expectations one CI round at a time; that was the only one.

Required field on a public type (Codex P2). openToolCallIds was required on AgUiBrowserEncoderState, which is re-exported from veryfront/agent (src/agent/index.ts:752). A consumer holding a state object built against the older shape would crash on .add of undefined at the first tool-input-start. Now optional and lazily initialized — the same reason reasoningSpanIndex directly above it is optional, as its own comment says. Covered by a test that deletes the field from a fresh state and drives the same sequence.

Deductions

−4, issue point 3 left undecided. The issue asks whether maxSteps exhaustion deserves its own message rather than a per-tool error. This PR surfaces it as a per-tool error and does not settle the question. Defensible — the asymmetry the issue calls "the one outcome that should not persist" is gone either way — but it is a listed point, and it is not answered.

−3, blast radius beyond the issue. The encoder change affects every tool-output-error producer, not just truncated local calls. That is the right fix and the tests cover the no-duplicate case, but it is broader than the issue's title suggests and worth a reviewer's attention.

Points 1 and 2 of the issue are settled explicitly in the commit message: placeholders now surface (history and wire no longer disagree), and reasoning-vs-budget-vs-sibling no longer decides visibility.

Verdict

Green at head (4ae6408da, 28/28), reviewed at head with no findings, every review finding addressed with red-green coverage. Merging.

@kwakayama
kwakayama added this pull request to the merge queue Aug 16, 2026
Merged via the queue into main with commit d07010a Aug 16, 2026
34 checks passed
@kwakayama
kwakayama deleted the fix/issue-3737 branch August 16, 2026 20:58
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.

Surface interrupted local tool calls on every terminal path, not just declined reasoning recovery

1 participant