Skip to content

fix(code-execution): deliver inner-call results only to the interpreter that asked - #242

Merged
antra-tess merged 2 commits into
mainfrom
fix/code-exec-stale-inner-results
Oct 7, 2026
Merged

antra-tess merged 2 commits into
mainfrom
fix/code-exec-stale-inner-results

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Why

A code_execution script could receive a tool result that belonged to a different script. A stopped script's late call could also end the next script's turn.

Inner tool-call ids (t1, t2, …) and wake ids restart in every interpreter (runtime-py.ts). PyRunner sends each answer with this.send, which writes to whichever interpreter is current when the answer arrives (py-runner.ts).

A script can stop while one of its calls is still running: it is aborted (abortAgentScript when a stream errors or a turn is abandoned), killed at its time limit, or its interpreter crashes. The call keeps running, as documented: "stopping a script does not stop tools it already called." Suppose the agent then starts another script, which gets a fresh interpreter. The old call's late result arrives as tool_result t1 and resolves the new script's first call. That script goes on with the wrong data, and its own result is later dropped as a duplicate.

The new tests reproduce this on unmodified main for both the abort path and the crash path. The second script prints the stopped script's result.

The same thing happened with a request to end the turn. scriptDeferredEndTurn recorded an end-turn request per agent and applied it to whichever foreground script finished next. If a script was stopped while a call that ends the turn was still running, the call's late result marked the agent's next script, and that script's result ended the turn. This path doesn't need a new interpreter: a script cancelled at its time limit keeps its interpreter. A background script's request could likewise end a foreground script's turn.

What this does

  • py-runner.ts: each child's line reader passes that child to handleLine. tool_call and wake are answered through a new reply(child, …) that writes only if that child is still the current interpreter. An answer whose interpreter is gone is dropped and logged as [pytc:<label>] dropped late result of <tool> call <id>: the interpreter that asked for it is gone. Before this change, such an answer went to the next interpreter, or nowhere if there was none.
  • framework.ts: each foreground exec gets its own end-turn mark, unless the runner is busy and will refuse the exec. The runner's onToolCall passes the mark of the script that is running when the call arrives, and handleScriptToolCall sets that mark. A stopped script's late result therefore marks only that script, which nothing reads any more. Background scripts' calls carry no mark, so their end-turn requests stay with them.
  • Unchanged: answers inside one interpreter. Ids keep counting up there, and a cancelled script's late results already fell through the runtime's pending-future lookup.
  • Changelog fragment: changelog.d/fix-code-exec-stale-inner-results.fixed.md.

Tests

  • New: test/code-execution.test.ts has a new describe block, "code_execution inner-call results after the interpreter is replaced", with 2 tests. Each test holds the first script's tool call open, stops that script, starts a second script and lets it make its own first call. It then resolves the stale call first and the new one after it. The second script must print its own result, and the drop must be logged.

    • One test stops the first script with abort().
    • The other stops it by crashing its interpreter (os._exit while the call is pending).

    Both fail on unmodified main: the second script prints stale result of the stopped script.

  • New: a framework test in the same file, "a stopped script's late end-turn request does not end the next script's turn". A script is stopped at a 1s limit while its finish call (which ends the turn) is still running. The next script runs, and the call finishes during it. On unmodified main, the next script's result carries endTurn; here it doesn't.

  • Full suite: npm run typecheck and npm run build are clean. On the final commit, npm test reported 1232 tests: 1223 passed, 5 failed, 4 skipped. All five failures are mcpl-awareness-barrier startup-gating tests. That file doesn't use code_execution, and it passes 25/25 when run alone. It's the known full-suite-load timing flake that also shows up on main.

This PR doesn't depend on #240 (blocking code and the time limit). The two branches merge cleanly.

This PR overlaps #237 in framework.ts. Both change the foreground exec block in runCodeExecution, the runner's onToolCall closure and handleScriptToolCall's parameters, using the same busy-guarded per-agent pattern as #237's foregroundScriptOrigins. Whichever merges second gets a small, mechanical conflict. Once #237 claims busy synchronously, the busy guard here also covers two code_execution calls in one response.

Refs #235, #203.

🤖 Generated with Claude Code

antra-tess and others added 2 commits October 6, 2026 16:49
…er that asked

Inner tool-call ids (t1, t2, ...) and wake ids restart in every interpreter,
and a late result was sent to whichever interpreter was current when it
arrived. After a script was aborted, killed at its time limit, or crashed
while a call was in flight, and the next script started in a fresh
interpreter, the old call's result resolved the new script's t1: the new
script went on with the wrong data, and its own result was dropped.

Each tool_call / wake is now answered on the child process that sent it;
an answer whose interpreter is gone is dropped and logged.

Refs #235, #203.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t that made it

An inner tool result that ends the turn was recorded per agent and applied
to whichever foreground script finished next. A script stopped (time limit,
abort) while such a call was running left the call's late request to mark
the agent's next script, whose result then ended the turn. A background
script's request could likewise end a foreground script's turn.

Each foreground exec now gets its own mark, captured by the inner calls it
makes; a late result marks only the stopped script, which nothing reads.
Background scripts' inner calls carry no mark.

Refs #235.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5 Tier: apex

[High risk] Fixes message routing in concurrent code execution interpreters.

The reviewed changes appear safe to merge.

What we checked:

  • Stopped calls cannot end next turn: The callback keeps the original script object before waiting for the tool. The next script has a different object, so the late result cannot set its endTurn.
  • Background calls cannot end foreground turns: Background calls receive no script mark. Their private waiter returns before the server gives the result to the agent, and handleScriptToolCall changes endTurn only when a mark exists.

Summary

This PR keeps tool replies with the interpreter that asked and gives each foreground script its own endTurn flag.

  • Adds regression tests for interpreter replacement and a late turn-ending result after a script times out.
  • No new actionable issue was found in the changes since the previous review.
  • Known scope: antra-tess leaves simultaneous-start protection to fix(mcpl): report tool calls made by code_execution scripts (RFC-007) #237, which will claim busy before waiting.
  • Previous threads were not supplied and could not be retrieved because GitHub authentication was unavailable. No numbered previous findings were available to classify.
  • Checks were based on source reads; tests were not run during this review.

Reviews (2) · Last reviewed commit: "fix(code-execution): apply an inner call..." · Reviewed by Greptile

@antra-tess

Copy link
Copy Markdown
Collaborator Author

@greptileai please re-review at the current head (8fd9523).

@antra-tess
antra-tess merged commit 47465a3 into main Oct 7, 2026
6 checks passed
antra-tess added a commit that referenced this pull request Oct 7, 2026
…ool-calls

Resolves framework.ts against #242's per-script deferred end-turn and
#238's pattern check: a foreground script now carries both its RFC-007
origin and its end-turn mark, both cleared in one finally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	src/framework.ts
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 8, 2026
…onto current main

GitHub reported the anima-research#247 → anima-research#252 → anima-research#256 → anima-research#257 stack DIRTY against main
02adf27 (June-1784 and Basil-1786, room-203 #51425): the conflict sits at
the bottom, in anima-research#247. Integrated once here, by merge so every reviewed commit
keeps its hash, and propagated upward the same way (Tessa-974 #51438;
Basil-1007 reviews the resolutions, #51449).

main since 64c480b brings anima-research#236 (reject visibly-empty push and channel
content), anima-research#238 (zero-match pattern and feature-set diagnostics), anima-research#240 and
anima-research#242 (code-execution deadlines and inner results), anima-research#241 (silent heartbeat
turn rows) and anima-research#237 (lifecycle tool calls from code_execution scripts).

Two textual conflicts, both additive:
- src/mcpl/push-handler.ts imports: main's EmptyContentError and
  visible-content helpers beside anima-research#247's InboundSource type.
- src/framework.ts: main's new silentTurnRowMetadata() sits before
  recordProseSuppression(); that method keeps anima-research#247's doc comment ("suppressed
  without drafts (proseRouting=disabled)"), which is what its body does
  since anima-research#247 holds explicit-send-suppressed prose as drafts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 12, 2026
anima-research#240, anima-research#242, anima-research#237, anima-research#169, anima-research#221) into fix/workspace-truthful-listings

Brings the branch up to main 956c6d2. GitHub reported the PR as
conflicting after anima-research#169's merge (6c07b6a), so CI didn't run on 2ae3554;
git's merge is clean, with no file conflicts.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 13, 2026
Brings in main through c2f9bae: anima-research#236, anima-research#241, anima-research#238, anima-research#240, anima-research#242, anima-research#237,
anima-research#169, anima-research#221, anima-research#248, anima-research#308, anima-research#263 and anima-research#312.

One conflict, in src/framework.ts's four MCPL handlers (push/event,
inference/request, channels/incoming and host/command). anima-research#263 wraps each
handler in answerOnce and refuseServerItem, and refuses the item after
the barrier wait once the queue has closed. This branch replaced each
handler's single-barrier wait with awaitDiscordAwarenessBarriers(),
called only while a generation is current. Resolved by keeping anima-research#263's
structure and putting this branch's wait where it waited on the barrier:

  if (this.discordAwarenessBarrier) await this.awaitDiscordAwarenessBarriers();

So the queue-closed check runs once every generation armed while the
handler waits has been waited out, and with no gate up an event still
reaches its handler in the turn it arrives.

Everything else merged without conflict.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant