fix(services): resume a run when its approval is answered outside the playground (#5593) - #5598
fix(services): resume a run when its approval is answered outside the playground (#5593)#5598mmabrouk wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
… 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
6144bf9 to
4c6fd34
Compare
9b37ab5 to
7189a20
Compare
Railway Preview Environment
|
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
buildRunPlanrefused 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.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:
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
buildRunPlanstill 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
resolvedcarrying the real tool call id.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
resolvedand carries atool_call_id.Closes #5593