Skip to content

Correlate tool approvals by call occurrence instead of transcript-wide result IDs - #115

Merged
shibayan merged 2 commits into
masterfrom
issue-98
Aug 31, 2026
Merged

Correlate tool approvals by call occurrence instead of transcript-wide result IDs#115
shibayan merged 2 commits into
masterfrom
issue-98

Conversation

@shibayan

Copy link
Copy Markdown
Member

What this changes

Fixes #98. withToolApproval correlated inbound decisions against a transcript-wide set of
answered function_result.callIds, so an approval granted for a call id that an earlier completed
call had already used was silently discarded — a permanent approve → re-ask loop, or a lost
decision — and a turn carrying no decision deleted the stored request for the same reason.

Decisions now bind per call occurrence, derived from the order of the run's own messages: a result
closes only the occurrence before it, and a request appearing after the latest result for its call
opens a new one. Request ids cannot stand in for occurrence identity, because a local id is derived
from the call id and repeats whenever the call id does. Nothing positional is persisted — the
boundaries are recomputed every run, so serialized sessions keep their current shape.

Parity

  • Reference checked: .NET ApprovalResponseBindingChatClient, which binds the requests it issued
    and never purges pending requests from a transcript-wide result-id set; Python, which derives
    logical occurrences from transcript order and has regression coverage for call-id reuse. The rule
    matches the one isActionableResponse already applies in the invocation loop underneath.
  • Wire format affected: no
  • Public API affected: no
  • Breaking change: no

Checklist

  • pnpm check passes (lint, typecheck, build, test)
  • Behaviour changes are covered by a test that fails without the change

…e result IDs

`withToolApproval` collected every historical `function_result.callId` into one
set, deleted the pending approval requests whose wrapped call used any of those
ids, and then dropped the decisions those requests would have bound. When a
provider reused a call id from an occurrence that had already completed, the
approval a human had just granted was silently discarded: the composition
`withToolApproval(withFunctionInvocation(...))` either re-asked forever or lost
the decision outright, and a turn carrying no decision at all deleted the stored
request for the same reason.

The transcript-wide purge is replaced by occurrence-aware correlation. One
ordered pass over the messages of the run derives the logical occurrences: a
request opens one, a `function_result` naming its call closes every occurrence
for that call, and a decision settles the one still open under its id. A request
that appears after the latest result for its call is therefore a new occurrence
that neither the older result nor the decision that closed the older occurrence
can reach — the same rule `isActionableResponse` already applies in the
invocation loop underneath, and the correlation .NET's
`ApprovalResponseBindingChatClient` and Python's transcript-order occurrences
use. Request ids cannot stand in for occurrence identity, because a local id is
derived as `ficc_${callId}` and repeats whenever a call id does.

Nothing positional is persisted: the boundaries are recomputed from the input
transcript on every run, so a serialized session still carries only the request
snapshots. Stored requests stay the authoritative record of what a human was
shown — they now bind to the newest occurrence still open under their id rather
than to whichever copy the caller replayed — and the destructive store read is
written back with every request that remains unanswered. Replayed copies of one
still-open request coalesce into a single occurrence whose first copy stays
canonical, so a doctored replay can no longer displace the request a decision
binds against. Partial-batch behavior is unchanged: answering part of a batch
re-surfaces only the remainder.

Fixes #98

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 30, 2026 08:13
@shibayan shibayan added the bug Usage: [PRs], Target: bug fixes and regressions; issues use the Bug issue type label Aug 30, 2026
@github-actions github-actions Bot added documentation Usage: [Issues, PRs], Target: documentation changes core Usage: [Issues, PRs], Target: packages/core labels Aug 30, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This pull request fixes tool-approval decision correlation when providers reuse callIds by binding approvals to per-occurrence requests derived from transcript order (instead of a transcript-wide “answered callIds” set). This aligns withToolApproval behavior with the underlying invocation loop and reference implementations, preventing approve→re-ask loops and unintended loss of pending approvals.

Changes:

  • Reworked inbound approval binding to scan “approval occurrences” from message order, merge in stored pending requests, and bind decisions to the correct open occurrence.
  • Added regression tests covering call-id reuse, duplicate/replayed requests, destructive store semantics, batch ordering, and session serialization/resume.
  • Documented the behavioral fix in CHANGELOG.md.
File summaries
File Description
packages/core/src/client/tool-approval.ts Implements occurrence-aware approval scanning/binding and correct pending-request write-back semantics.
packages/core/src/client/tool-approval.test.ts Adds regression coverage for call-id reuse and occurrence correlation across replay/store/serialization scenarios.
CHANGELOG.md Records the behavior change and its user-visible impact under “Unreleased”.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

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

Comment thread packages/core/src/client/tool-approval.ts
Both layers still let something other than a `function_result` end an
occurrence, so a request copy replayed after the decision that answered it
was read as a second, unanswered ask.

`scanOccurrences` coalesced a replayed copy only while its occurrence was
unanswered, and the invocation loop's `lastRequestPosition` always moved to
the newest copy, which put the request after the decision meant for it.
Together they dropped the decision and asked the human again for a call they
had just approved: for `request → decision → request` with no result between,
the gated tool ran zero times.

An occurrence now ends only where the rule says it does. A replayed copy is
coalesced whether or not a decision has settled it, and the recorded request
position moves only when a result for that call has closed the previous
occurrence — so a genuinely reused call id after a result still opens a new
occurrence a stale decision cannot reach.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 31, 2026 02:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@shibayan
shibayan merged commit 2d36005 into master Aug 31, 2026
9 checks passed
@shibayan
shibayan deleted the issue-98 branch August 31, 2026 02:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Usage: [PRs], Target: bug fixes and regressions; issues use the Bug issue type core Usage: [Issues, PRs], Target: packages/core documentation Usage: [Issues, PRs], Target: documentation changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Approval for a reused call ID is silently dropped after an earlier call with that ID completed

2 participants