Skip to content

[5357] fix(runner): recognize client-tool settles as a resume in cold replay#5364

Merged
mmabrouk merged 1 commit into
release/v0.105.4from
runner-fix/elicitation-resume-loop
Jul 17, 2026
Merged

[5357] fix(runner): recognize client-tool settles as a resume in cold replay#5364
mmabrouk merged 1 commit into
release/v0.105.4from
runner-fix/elicitation-resume-loop

Conversation

@ardaerzin

Copy link
Copy Markdown
Contributor

Fixes #5357 (AGE-3964).

Problem

After an agent calls request_input and the user submits the form, the immediate resume turn behaved as if the answer was never given — the agent re-asked, sometimes looping. The value only became usable on a later turn.

Root cause

A request_input pause tears down the sandbox (client tools are non-parkable), so the resume is a cold /run and buildTurnText (services/runner/src/engines/sandbox_agent/transcript.ts) rebuilds the prompt from history.

It had two frames: an approval-resume frame (detected via {approved} envelopes) and a normal-turn frame. A request_input answer is a non-approval tool_result, so isApprovalResume returned false and it fell into the normal-turn path. On a resume there is no new user message, so resolvePromptText fell back to the stale original prompt and re-presented it as "The user now says: <prompt>" — the model read that as a fresh request and re-asked. The answer was in the transcript (why it "knows" a turn later), but the closing frame overrode it.

This is the same class of bug the approval-resume frame already fixed for approvals (turn 6d34b1ea); it just never covered client-tool settles.

Fix

Generalize resume detection: isApprovalResumeresumeKindFor(...) → "approval" | "client" | null.

  • A client resume = a non-approval tool_result in an assistant-role message positioned after the last user-text message. The Vercel adapter keeps the settled tool_result inside the assistant turn it paused on, so an executed server-tool result in a tool/user turn is not mistaken for a resume (preserves the existing "approved call already executed" behavior).
  • New CLIENT_RESUME_CLOSING frame: continue from the pause using the provided result; do not restart or re-ask.
  • Approval wins when both are present. Covers request_connection and decline/cancel settles too.
  • Log field resumeFrame= now emits none|approval|client.

Tests

Not in this PR / notes

  • Runner-side only — needs a runner rebuild to take effect.
  • Live end-to-end confirmation needs a running stack + real LLM (non-deterministic); the unit tests pin the exact prompt frame that was the root cause.

… replay

buildTurnText only detected an approval resume, so a request_input answer
(a non-approval tool_result) fell into the normal-turn frame that re-presents
the stale original prompt as "The user now says: <prompt>". The model treated
it as a fresh request and re-asked, looping.

Generalize resume detection to resumeKindFor -> "approval" | "client" | null:
a client resume is a non-approval tool_result in an assistant-role message
after the last user-text message. Add a client-resume closing frame that tells
the model to continue from the pause using the provided result. Also covers
request_connection and decline/cancel settles.
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jul 17, 2026 4:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address #5357 by making request_input answers available on the immediate resume turn and preventing re-ask loops.
Out of Scope Changes check ✅ Passed The modified code and tests stay focused on transcript resume handling and logging, with no clear unrelated scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: treating client-tool settles as resume cases during cold replay.
Description check ✅ Passed The description matches the implemented fix and tests for client-resume cold replay behavior.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch runner-fix/elicitation-resume-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@mmabrouk
mmabrouk self-requested a review July 17, 2026 20:18
@mmabrouk
mmabrouk changed the base branch from main to release/v0.105.4 July 17, 2026 22:01
@mmabrouk
mmabrouk marked this pull request as ready for review July 17, 2026 22:21
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. bug Something isn't working labels Jul 17, 2026
@mmabrouk
mmabrouk merged commit 63682f3 into release/v0.105.4 Jul 17, 2026
37 of 38 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Image tag pr-5364-4025d53
Status Failed
Railway logs Open logs
Logs View workflow run
Updated at 2026-07-17T22:36:37.906Z

mmabrouk added a commit that referenced this pull request Jul 18, 2026
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
mmabrouk added a commit that referenced this pull request Jul 18, 2026
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
mmabrouk added a commit that referenced this pull request Jul 18, 2026
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
mmabrouk added a commit that referenced this pull request Jul 19, 2026
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
mmabrouk added a commit that referenced this pull request Jul 19, 2026
Rebases the runner third of JP's feat/sessions-extensions (PR #5363) onto
current main, ported into the post-decomposition module layout (PR #5369).

JP wrote these against the old monolithic sandbox_agent.ts; they are re-homed
by region into the decomposed modules:
- The SandboxAgentDeps interface change (syncHarnessSessionDurable renamed to
  appendSessionTurn; the write/clear sandbox-pointer deps dropped) goes into
  runtime-contracts.ts.
- The two acquireEnvironment branches (the reconnect-failure path and the
  post-hydrate pointer write, both now append-only with no pre-turn pointer PUT)
  go into environment.ts.
- The turn-completion sync (syncHarnessSessionDurable replaced by appendSessionTurn
  with the streamId gate, workflow references, sandbox id, and trace id) goes into
  run-turn.ts.
The sandbox-reconnect.ts and session-continuity-durable.ts rewrites apply as-is.
protocol.ts, sessions/alive.ts, sessions/persist.ts, version.ts, and the tests
are JP's versions verbatim.

Ported from feat/sessions-extensions@1532ec7fe5 (JP).

Reconciliations with the release main:
- server.ts: kept main's session-identity / session-pool import split from the
  decomposition and applied JP's watchdog changes (startAliveWatchdog is now
  awaited and takes an onInterrupted callback that aborts the controller,
  request.streamId is threaded from the heartbeat, and buildPersistingEmitter
  gets turnId and span_id).
- tracing/otel.ts, package.json, pnpm-lock.yaml: kept main's. The OTel 2.x bump
  JP carried is already in main, and main additionally has the #5364
  trace-id-on-done feature that JP's branch predates.

Verified: pnpm run typecheck clean, pnpm test 1202/1202 pass, build:extension ok.

Claude-Session: https://claude.ai/code/session_01DnWRxU3dCJ11hgDidm26vW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] request_input answer is unavailable during resume turn and can cause loops

2 participants