Skip to content

fix(services): resume a run when its approval is answered outside the playground (#5593) - #5598

Open
mmabrouk wants to merge 1 commit into
fix/pi-default-builtinsfrom
fix/approval-resume
Open

fix(services): resume a run when its approval is answered outside the playground (#5593)#5598
mmabrouk wants to merge 1 commit into
fix/pi-default-builtinsfrom
fix/approval-resume

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Context

An agent on a schedule stops and waits when it hits a tool that needs approval. Answering that approval from anywhere other than a live playground session did nothing. The row was marked answered, the agent service returned 200 with an empty body, and the run stayed stopped forever.

That blocks every out-of-band approval surface equally: an inbox, a webhook, a Slack button, a script.

Three separate defects, each reproduced against the running runner.

The resumed run was refused for carrying no user message. An approval reply has no new user text, and buildRunPlan refused any request without it. The runner can rebuild the earlier turns from its own record log, but that happens after the plan is built, so the request died before the conversation could be supplied.

{"kind":"result","result":{"ok":false,"error":"No user message to send (prompt/messages empty)."}}

The agent service raises after the response headers are already committed, which is why the caller saw a successful status and nothing in the body, and then reported Workflow service closed the stream before emitting a started record.

Answering evicted the live session parked on the gate. The approval branch compares the conversation the request carries against the parked one. A caller answering from a durable row carries none, so the runner concluded the transcript had been edited and tore the session down:

[keepalive] approval-mismatch (history) key=<project>:<session>; evict + cold

The ordinary continuation branch already had an escape hatch for callers that send no history. The approval branch never got one.

The durable interaction row never stored the tool call id. A parked gate has two ids, the permission gate id and the tool call id, and only the first was persisted. The playground works because it receives both on the live event stream. So an answer built from the stored row alone named the wrong call, and the resume path matches strictly on tool call id.

Changes

buildRunPlan still refuses an empty prompt, except when the request is an approval reply and nothing else. A new predicate, carriesApprovalReplyOnly, decides that. History reconstruction accepts the same shape, so the earlier turns come from the record log.

The keep-alive approval branch now skips its history comparison for a caller that asserts no conversation, the same way the ordinary continuation branch already did. It needed its own condition rather than reusing the existing helper, because that one requires the last message to be a fresh user turn and an approval reply never is.

The tool call id is persisted when the gate is raised, so a caller can name the right call. Rows written before this still read back, and the frontend falls back to the old id for them.

Tests

Verified end to end on a running deployment. An approval was answered out of band on a session whose park had already expired, so it took the cold path. The runner rebuilt two prior turns from the record log, recovered the stored decision, allowed the gate, and finished the turn. The file the agent had asked to write appeared on the mount with the approved contents, and the row moved to resolved carrying the real tool call id.

[HITL] cold replay: ... resumeFrame=approval
[reconstruct] session=... records=10 prior=10 priorMessages=2 inbound=1
[HITL] gate toolName="Write" permission=ask outcome=allow
complete OK session=... turn=1

Runner tests 1308 passing, six of them new. API session tests 144 passing, three of them new.

Two reviewers went at this adversarially. Neither could construct a request that gains anything through the loosened checks: the live resume answers the parked gate with the gate's own original arguments and never shows the model the incoming messages, any user text at all re-arms the history comparison, and gate binding stays strict on tool call id.

Follow-ups this does not fix

An answer that matches no parked gate still evicts the live session instead of being refused. That policy is old, but it almost never fired while the playground was the only client answering approvals. A retry or a double click will now hit it. Filed as #5596.

Answering an approval marks the row as used before the resumed run is known to have started, so a failed resume loses the approval with no retry. Filed as #5592.

What to QA

  • Put an agent on a schedule so it stops for approval unattended. Answer the approval outside the playground. The run continues and the tool actually executes.
  • Check the interaction row afterwards. It reads resolved and carries a tool_call_id.
  • Regression: approve and deny in the playground as usual. Both still work, and denying still ends the call as declined rather than hanging.
  • Regression: send an ordinary message to an agent mid-conversation. It continues from the live session rather than rebuilding from scratch.
  • An approval created before this change carries no tool call id. Answering it still falls back to the old identifier.

Closes #5593

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 30, 2026
@vercel

vercel Bot commented Jul 30, 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 Jul 30, 2026 8:05pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8931170d-d986-4b78-b1c8-08d64751d529

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

… the run

Answering a parked approval from anywhere but a live playground session never
resumed anything. Three defects, each reproduced against the runner:

The resumed run was refused for carrying no user message. An approval reply has
no new user text, and the runner rebuilds the prior turns from the record log
only AFTER the plan is built, so the request died before the conversation could
be supplied. The check now spares an approval-reply-only request.

Answering also evicted the live session parked on that gate. The approval branch
compared the conversation the request carries against the parked one and found
nothing, so it treated the transcript as edited. It now has the same
no-history escape hatch the ordinary continuation branch already had.

The durable interaction row stored only the permission gate id, never the tool
call id, so a caller building an answer from the row named the wrong call. The
tool call id is now persisted when the gate is raised; rows written before this
still read back.

Closes #5593
@github-actions

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-4f08.up.railway.app/w
Project agenta-oss-pr-5598
Image tag pr-5598-825dde3
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-07-30T20:10:22.590Z

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant