diff --git a/services/runner/src/engines/sandbox_agent/acp-interactions.ts b/services/runner/src/engines/sandbox_agent/acp-interactions.ts index 3f04a0eecb..b0f8a3d5fd 100644 --- a/services/runner/src/engines/sandbox_agent/acp-interactions.ts +++ b/services/runner/src/engines/sandbox_agent/acp-interactions.ts @@ -35,6 +35,7 @@ export interface PiToolSpecMeta { } export interface AttachPermissionResponderInput { + onPermissionRequestStarted?: (toolCallId: string) => void; session: any; run: { emitEvent: (event: AgentEvent) => void; @@ -125,6 +126,7 @@ export function attachPermissionResponder({ onPause, log, onPausedToolCall, + onPermissionRequestStarted, onAllowedExecution, onAnsweredDeny, onNonParkablePause, @@ -367,6 +369,9 @@ export function attachPermissionResponder({ }), ); } + if (envelope.toolCallId) { + onPermissionRequestStarted?.(envelope.toolCallId); + } const verdict = await responder.onPermission({ id, availableReplies, @@ -462,6 +467,11 @@ export function attachPermissionResponder({ return; } + const toolCallId = stringValue(toolCall?.toolCallId); + if (toolCallId) { + onPermissionRequestStarted?.(toolCallId); + } + const verdict = await responder.onPermission({ id, availableReplies, @@ -476,7 +486,7 @@ export function attachPermissionResponder({ id, verdict.kind, availableReplies, - stringValue(toolCall?.toolCallId), + toolCallId, verdict.interactionToken, ); } @@ -635,4 +645,4 @@ function clientToolReply( function errorMessage(err: unknown): string { return err instanceof Error ? err.message : String(err); -} +} \ No newline at end of file diff --git a/services/runner/src/engines/sandbox_agent/pause.ts b/services/runner/src/engines/sandbox_agent/pause.ts index fe7b2dc23e..9f18bf6f62 100644 --- a/services/runner/src/engines/sandbox_agent/pause.ts +++ b/services/runner/src/engines/sandbox_agent/pause.ts @@ -13,6 +13,7 @@ const EVENT_DRAIN_MAX_TICKS = 6; export class PendingApprovalPauseController { private pendingApproval = false; + private readonly inFlightPermissionToolCallIds = new Set(); private readonly pausedToolCallIds = new Set(); private readonly allowedExecutionToolCallIds = new Set(); private readonly answeredDenyToolCallIds = new Set(); @@ -72,23 +73,40 @@ export class PendingApprovalPauseController { * last word for the call this turn. Later harness frames for the same id are teardown artifacts * from cancellation/session disposal and must not reach the event stream. */ - markPausedToolCall(toolCallId: string): void { - if (!toolCallId) return; - const added = !this.pausedToolCallIds.has(toolCallId); - this.pausedToolCallIds.add(toolCallId); - // A late gate must extend terminalization long enough for its paused classification to land. - if (this.pendingApproval && added) this.gateClassificationVersion += 1; + markPausedToolCall(toolCallId: string): void { + if (!toolCallId) return; + + this.inFlightPermissionToolCallIds.delete(toolCallId); + + const added = !this.pausedToolCallIds.has(toolCallId); + this.pausedToolCallIds.add(toolCallId); + + if (this.pendingApproval && added) { + this.gateClassificationVersion += 1; + } +} + + markPermissionRequestStarted(toolCallId: string): void { + if (toolCallId) { + this.inFlightPermissionToolCallIds.add(toolCallId); + } } + isInFlightPermission(toolCallId: string | undefined): boolean { + return toolCallId !== undefined && this.inFlightPermissionToolCallIds.has(toolCallId); + } isPausedToolCall(toolCallId: string | undefined): boolean { return toolCallId !== undefined && this.pausedToolCallIds.has(toolCallId); } /** Record that this turn answered allow for the call, so pause cleanup preserves its result. */ markAllowedExecution(toolCallId: string): void { - if (!toolCallId) return; - this.allowedExecutionToolCallIds.add(toolCallId); - } + if (!toolCallId) return; + + this.inFlightPermissionToolCallIds.delete(toolCallId); + + this.allowedExecutionToolCallIds.add(toolCallId); +} /** Record an answered deny so its authoritative failed frame survives a sibling pause. */ markAnsweredDeny(toolCallId: string): void { diff --git a/services/runner/src/engines/sandbox_agent/run-turn.ts b/services/runner/src/engines/sandbox_agent/run-turn.ts index d9b328f841..047e60e075 100644 --- a/services/runner/src/engines/sandbox_agent/run-turn.ts +++ b/services/runner/src/engines/sandbox_agent/run-turn.ts @@ -414,7 +414,9 @@ export async function runTurn( if (pause.active) { run.settleOpenToolCalls( (id) => - pause.isPausedToolCall(id) || pause.isAllowedExecution(id), + pause.isPausedToolCall(id) || + pause.isAllowedExecution(id) || + pause.isInFlightPermission(id), TOOL_NOT_EXECUTED_PAUSED, ); } @@ -582,6 +584,7 @@ export async function runTurn( serverPermissions, log: logger, onPause: () => pause.pause(), + onPermissionRequestStarted: (id) => pause.markPermissionRequestStarted(id), onPausedToolCall: (id) => pause.markPausedToolCall(id), onAllowedExecution: (id) => pause.markAllowedExecution(id), onAnsweredDeny: (id) => pause.markAnsweredDeny(id),