[Bug]: Command approvals stay pending after a delegated task is cancelled#21935

opened26d agobyEeowca

What happens: on 0.0.46-nightly.20261008.2819, two delegated Codex tasks were each blocked on a pending shell-command approval. Each was cancelled by its parent agent's task_cancel call, and both runs are now interrupted. Their command approval requests remained pending afterwards. A comparable delegated Claude task's pending file-read approval, cancelled the same way, was cleared.

This one sample shows inconsistent cleanup between the two delegated tasks. It does not establish a Codex-only bug. The provider-specific cause is not traced; the records were found by read-only state inspection.

Expected: when a run is interrupted or cancelled, its unresolved approval requests are withdrawn, because their provider callbacks no longer exist. They should then no longer count as pending.

The effect on any UI view was not inspected.

Note

Grok responding on behalf of Julius.

Thanks for flagging the difference between the two providers. Current main explains it.

When a provider turn ends, including when it is interrupted, the server only withdraws pending user input requests. The query that finds them is limited to that kind (apps/server/src/orchestration-v2/ProjectionStore.ts:4429-4437; apps/server/src/orchestration-v2/ProviderEventIngestor.ts:579-585, :680-681). Pending approvals are only released when the whole provider session closes or fails (apps/server/src/orchestration-v2/ProviderSessionManager.ts:630-660). Codex threads share a long lived session, so that fits the Codex approvals staying pending after the cancel. I haven't traced why the Claude approval cleared.

None of the open pull requests around delegated cancel or approvals change this.

Accepting this as a bug.

Synced from GitHub

The same thing happens after t3_thread_interrupt, with a Claude child, and it is visible in the UI.

On 0.1.2-nightly.20261021.3473 (macOS desktop), a Claude delegated child (full access) stopped on a command approval. The parent interrupted it and sent new instructions with t3_thread_send. Run 3 started and worked normally: new tool calls and renders, no new approval. The child thread still reported pendingRequestCount: 1, and the "Subagent needs approval" banner stayed on screen for the old, interrupted run. The user reported it as a new request twice.

Related: #22925 (the parent agent gets no notice when a delegated child waits for an approval).

Verified the state regression on main snapshot 3b95e7831b: both interrupted terminal paths leave the approval pending; the completed-turn control passes. Existing fix #22023 now includes the main merge and has the bugfix label. Three focused suites pass 63 tests; targeted lint and server typecheck pass. Fresh client interaction proof remains incomplete after VM stalls and environment registration failures; the PR lists unverified surfaces. No merge performed.

Synced from GitHub

Additional evidence from a Windows desktop Claude thread on T3 Code 0.1.3: the stale-approval state also occurs after the user presses Stop directly, without delegated-task cancellation.

The user stopped a run while a command approval was pending. The run was shown as interrupted, but the approval card and Approval badge remained. The user reported that Approve, Decline, Cancel, and Always allow this session could not dismiss it. Resume Thread also failed with This thread can no longer be resumed from that run.

Persisted state and timeline

A read-only SQLite audit found one pending command approval belonging to run 12 and its interrupted provider turn:

RecordState
Run, run attempt, and provider turninterrupted
Command execution iteminterrupted
Approval requestpending, with no resolved timestamp
Approval node and timeline itemwaiting
Approval response capabilitylive
Session rowpresent, marked ready
Binding from that session to this threadabsent

Relevant timestamps on 2026-10-28, UTC:

text
117:34:09.523 Command approval created
217:34:35.617 Outgoing query.interrupt
317:34:35.618 Outgoing query.close
417:34:36.078 Provider turn becomes interrupted
517:34:36.088 Run becomes interrupted
617:34:36.599 thread.runtime-mode.set starts; trace result: Success
717:34:36.626 Resume message.dispatch starts; trace result: rejection
817:34:36.657 Session detach effect created: Runtime mode changed.
917:34:40.257 Session detach effect completes successfully
1017:34:40.467 Approval response fails: Provider session <session> was not found.
1117:34:42.326 Same approval response failure
1217:36:09.384 Same approval response failure
1317:37:38.460 Same Resume rejection

The session row was not deleted. The response-context query joins sessions to thread bindings, so the missing binding makes that session unavailable to the approval handler. The subsequent mode-change detach is a separate part of this sequence; the evidence does not attribute the missing binding directly to Stop.

There is also a related detach cleanup omission: when the last exclusive thread is detached, it is removed from the attachment set before the session and pending-request cleanup iterate that set. This helps explain why the later detach did not retire this already-stale request. ProviderSessionManager.

Source diagnosis on 0.1.3

Reviewed tag v0.1.3, commit d2c440312e5f39d59827ed466838a623516454d6:

  • Both terminal provider-event paths call question-only cleanup. The underlying query filters for kind = 'user_input', excluding command approvals. ProviderEventIngestor, ProjectionStore.
  • Stop's follow-up only calls settleInterruptedRun while the run is still running. That helper also returns if the run or attempt is already terminal, before reaching approval cancellation. This permits provider termination to win the ordering and leave approvals pending. Orchestrator.
  • The existing interrupted-turn test explicitly preserves its command approval fixture. The expected behavior needs updating as part of the regression coverage. ProviderEventIngestor test.

The ordering is supported by code and matches the incident's persisted state; a fresh provider-level reproduction was not run. The read-only audit checks the existing inconsistency, not the Stop implementation end to end.

The question-only terminal cleanup and the already-terminal Stop guards also remain in main inspected at 886d8f681234e3d88e30e0fd803fe9ccb13b0daf. PR #22023 was still open when checked; its reported test results are separate from this triage's checks.

Impact and regression coverage

The screenshot confirms stale attention and approval controls. Source inspection also shows that feedback to this approval, Settle, and Snooze depend on resolving the pending request; those additional actions were not exercised on the live thread. This is not a claim that all possible continuation paths are permanently blocked or that conversation history was lost.

Please include direct desktop Stop with Claude alongside delegated cancellation, both interrupted terminal-event shapes, multiple pending approvals, and concurrent approval responses. Cleanup should update request, node, timeline item, and response capability together while preserving other turns, already resolved decisions, and asynchronous message-mode questions. Normal root completion can leave legitimate background work, so it needs a separate control case.

This supports #21935 and its proposed fix #22023. The client offering an ineligible Resume action is being prepared as a separate report. No repair, provider command, or database write was performed during triage.

Environment: Windows 11, build 10.0.26200, T3 desktop with local server 0.1.3, Claude Agent SDK provider, Claude Opus 5.5. Triage CLI version was 0.1.2-nightly.20261022.3541, distinct from the affected server version.

Prepared by Codex, GPT-6, via t3 triage, with review by 10 dedicated agents.

Synced from GitHub

Pi variant of this, with a worse side effect: the Pi process itself stays alive.

T3 Code 0.1.5 desktop (macOS), Pi 1.4.0. An auto-mode Claude parent delegated a task to the Pi provider. T3 runs Pi as approval-required in that case, so the child stopped on its first bash approval. The parent called task_cancel and got cancel_requested.

The pi --mode rpc process for that child was still running 25 minutes later, apparently still blocked in the extension's ctx.ui.confirm. It exited only when killed by hand. So for Pi, cancellation leaves a live provider process holding the pending confirm, on top of the stale approval record. Expected: cancelling the run aborts the pending confirm (Pi's ctx.signal) and ends the Pi process, or at least its turn.

I tried to reproduce this on the current build and could not.

Commit f03db4fbef (origin/main). Ran T3CODE_DEV_SEED=0 vp run dev (web) on a fresh database, paired the browser, and used Codex on its cheapest model with Supervised permissions. (1) Asked the agent to run touch <scratch>/approved.txt; it waited on a command approval; pressed Stop generation. The card disappeared, the run was 'Run interrupted · Stopped by user', and the persisted request was command/cancelled/not_resumable 'The run was interrupted.' (2) In the same thread, had the parent call delegate_task for a Codex child running a touch command; the child blocked on a pending, live command approval; the parent called task_cancel. Child run interrupted, approval cancelled ('The run was interrupted.'). (3) Repeated the delegated cancel twice more: the same withdrawal both times. 0 of 4 real runs left an approval pending. The defect is order-dependent: Orchestrator.settleInterruptedRun withdraws all requests but only while the run is still 'running'. When the provider's interrupted-turn event is stored first, only ProviderEventIngestor's turn-end cleanup runs, and it uses ProjectionStore.getPendingNativeUserInputs (kind = 'user_input' only). Confirmed with a test that fails on main: in ProviderEventIngestor.test.ts the existing table case, now expecting the pending approval to be withdrawn on an interrupted provider turn, fails with expected 'pending' to equal 'cancelled'; the completed-turn control case passes.

Closing as not planned. If it still happens for you, reply with the exact steps and the version you are on and it will be looked at again.