You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Bug: Changing approval policy while escalation is pending leaves agent stuck working
#5147
When a user changes the approval policy from "ask" to "never" while an agent has a pending sandbox escalation approval request, the agent remains stuck in the "working" state indefinitely. The pending approval request is never resolved, the tool result never returns, and the turn never closes. The UI continues to show the agent as "working" with no indication anything is wrong.
Reproduction
Session with approval/policy: ask, agent calls edit with sandbox_permissions + justification
UI shows approval prompt; agent shows awaiting approval -> working
User changes permission preset (e.g. /permission danger-full-access) or approval policy to "never"
Pending approval prompt disappears, but agent remains "working" indefinitely
Only way to recover: send a new user message (starts new turn, original tool call never resolved)
Root cause
ApprovalService.setPolicy() in packages/interaction/user-approval/src/index.ts appends the policy change and injects a user message, but does NOT abort in-flight approval/request() promises that are already executing inside decide(). Those promises sit on an await in the escalate -> request -> decide chain, waiting for the waterfall/answerer to resolve.
decide() already handles signal abort at line ~187 (if (signal?.aborted) return "cancelled"), but setPolicy() never calls signal.abort().
Fix (1 line)
setPolicy(agent: Agent, policy: ApprovalPolicy): void {
const previous = this.effectivePolicy(agent.session)
if (previous === policy) return
setApprovalPolicy(agent.session, policy)
+ // Abort any in-flight approval request so the pending tool call+ // resolves as cancelled and the agents turn can close.+ agent.signal?.abort()
agent.inject(createUserMessage({
content: [{
type: text,
text: `The approval policy changed from ${previous} to ${policy} (changed by the user).`,
}],
source: { kind: plugin, plugin: user-approval },
}))
}
The existing signal path in decide() does the rest: signal.aborted -> cancelled -> audit pair closes -> tool result propagates -> turn closes -> UI updates to idle.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
When a user changes the approval policy from "ask" to "never" while an agent has a pending sandbox escalation approval request, the agent remains stuck in the "working" state indefinitely. The pending approval request is never resolved, the tool result never returns, and the turn never closes. The UI continues to show the agent as "working" with no indication anything is wrong.
Reproduction
approval/policy: ask, agent callseditwithsandbox_permissions+justificationRoot cause
ApprovalService.setPolicy()inpackages/interaction/user-approval/src/index.tsappends the policy change and injects a user message, but does NOT abort in-flightapproval/request()promises that are already executing insidedecide(). Those promises sit on anawaitin the escalate -> request -> decide chain, waiting for the waterfall/answerer to resolve.decide()already handles signal abort at line ~187 (if (signal?.aborted) return "cancelled"), butsetPolicy()never callssignal.abort().Fix (1 line)
setPolicy(agent: Agent, policy: ApprovalPolicy): void { const previous = this.effectivePolicy(agent.session) if (previous === policy) return setApprovalPolicy(agent.session, policy) + // Abort any in-flight approval request so the pending tool call + // resolves as cancelled and the agents turn can close. + agent.signal?.abort() agent.inject(createUserMessage({ content: [{ type: text, text: `The approval policy changed from ${previous} to ${policy} (changed by the user).`, }], source: { kind: plugin, plugin: user-approval }, })) }The existing signal path in
decide()does the rest:signal.aborted -> cancelled-> audit pair closes -> tool result propagates -> turn closes -> UI updates to idle.All reactions