Repository navigation
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Summary
#5454 covers a pending user-input request becoming permanently stuck once its provider session dies. The same hole exists for pending approvals, and open PR #10586 (which fixes #5454) does not close it: that PR resolves native questions only, on both the respond path and session.exited.
Nothing ever resolves a pending approval when its session goes away.
approval.resolved has exactly one non-test producer, and it is the live provider reporting request.resolved:
// ProviderRuntimeIngestion.ts
case "request.resolved": {
...
kind: "approval.resolved",On session.exited the ingestion clears turn state and plan progress, and with #10586 also dismisses native user-input requests. Approvals are not touched in any of those paths. So once the provider session is gone, the only remaining route is the user pressing the button, which fails:
// ProviderCommandReactor.ts - processApprovalResponseRequested
const hasSession = thread.session && thread.session.status !== "stopped";
if (!hasSession) {
return yield* appendProviderFailureActivity({
kind: "provider.approval.respond.failed",
detail: "No active provider session is bound to this thread.",
...
});
}That detail is not in any of the three approval stale-failure matchers, so all three keep the request open:
packages/client-runtime/src/pendingRequests.ts—staleRequestFailureDetails["provider.approval.respond.failed"]holds onlystale pending approval request,unknown pending approval request,unknown pending permission request,unknown pending codex approval request.derivePendingRequeststherefore never deletes the approval, so the card stays rendered in web, desktop, and mobile.apps/server/src/orchestration/Layers/ProjectionPipeline.ts—isStalePendingApprovalFailureDetailholds three of those fragments, so theprojection_pending_approvalsrow stayspendingandpending_approval_countstays above zero.apps/server/src/orchestration/decider.ts—isStaleRequestFailureDetailkeeps the request inopenRequests, so settle is rejected on a thread whose shell flags read as blocked.
The client will reach this branch, because the approval handler has no session-status gate — unlike the main stop button, which is gated on session.status === "running":
// ChatView.tsx - onRespondToApproval
if (!activeThreadId) return;So every Approve/Deny press appends another provider.approval.respond.failed and the card never closes.
Steps to reproduce
- Get a provider to request an approval (command execution, file change, or MCP elicitation) so the approval card renders.
- While it is pending, make the provider session go away: restart the server, let the session reaper take it, or let the provider child exit.
- Press Approve or Deny.
- A
provider.approval.respond.failedactivity is appended with"No active provider session is bound to this thread.", the card stays open, and the thread keeps a non-zero pending-approval count. Repeat step 3 for as many failure activities as you like.
Expected behavior
Once the provider session is gone the approval can never be accepted, so it should be treated as terminal: the card closes, pending_approval_count drops to zero, and the thread can settle — the same outcome #10586 produces for native questions.
Actual behavior
The approval stays pending indefinitely on both the client and the server projection, every response attempt appends another failure activity, and the thread cannot settle.
Impact
Major degradation or frequent failure
Version or commit
main @ 9d4bb55.
Environment
Found by code inspection while investigating a related stop failure (#11796) on desktop 0.0.40, Windows 11, provider Claude Code.
Logs or stack traces
provider.approval.respond.failed
summary: "Provider approval response failed"
detail: "No active provider session is bound to this thread."Workaround
None from inside the app for the approval card itself. Restarting the server re-runs reconcileProviderSessions, which settles the orphaned session, but that does not emit approval.resolved, so the pending-approval accounting is not obviously repaired by it either.
Two possible fixes
- Mirror fix(server): dismiss orphaned provider questions #10586 for approvals (structurally consistent, larger): record a durable
approval.resolveddismissal when a response arrives against a missing or stopped session, and dismiss pending approvals onsession.exited. This needs an approval analogue ofprojectionSnapshotQuery.getUserInputActivity, which does not exist yet. - Add the detail to the three approval matchers (smallest, follows the established pattern): treat
"no active provider session is bound to this thread"as a stale approval failure in the three places listed above. This is what the closed-unmerged fix: clear pending user-input requests when the provider session is gone #5453 and Clear pending requests whose provider session is gone #4598 did, for user-input as well as approvals.
Happy to open a PR for whichever direction you prefer.
What I verified and what I did not
Verified by reading main @ 9d4bb55: the single approval.resolved producer, the absence of approvals from every session.exited path, the exact contents of all three matcher lists, that #10586's diff touches user-input only, and that onRespondToApproval has no session-status gate.
Not verified: a live runtime repro. This one is a code-path finding, unlike #11796 where I confirmed the failure with a test against unmodified main.
Related
- [Bug]: Pending user-input popup can never be dismissed after the provider session dies (e.g. server restart) #5454 and open PR fix(server): dismiss orphaned provider questions #10586 — the user-input half. fix(server): dismiss orphaned provider questions #10586 does not cover approvals.
- [Bug]: Turn interrupt on a stopped session settles nothing, leaving a dangling active turn and a permanently failing stop #11796 — the interrupt/stop path of the same "session died underneath a pending interaction" family.
- [Bug]: OpenCodeAdapter leaks pending permission requests on session teardown — thread becomes permanently unsettleable #7113 — an OpenCode-adapter-specific leak of pending permission requests, already completed. This report is provider-agnostic and sits in the shared orchestration layer.
- fix: clear pending user-input requests when the provider session is gone #5453 and Clear pending requests whose provider session is gone #4598 — both closed unmerged; both would have covered the approval detail.
Activity
juliusmarminge commentedon Sep 14, 2026
Triage
Confirmed as a distinct orchestration bug: a pending approval is never made terminal when its provider session disappears. This is the approval half of #5454. Open PR #10586 is questions-only and does not close this issue.
What we verified
Checked on current
main(549d182aa); the paths still match the report against9d4bb550.approval.resolvedhas a single non-test producer: liverequest.resolvedinProviderRuntimeIngestion. There is no approval analogue ofgetUserInputActivity/listUserInputLifecycleByThreadId.session.exitedclears turn state, plan progress, and background liveness. Currentmainalready dismisses native user-input on a terminal turn. fix(server): dismiss orphaned provider questions #10586 (still open) adds session-exit + dead-session respond dismissal for questions only. Approvals are untouched in all of those paths.processApprovalResponseRequestedon a missing/stopped session appendsprovider.approval.respond.failedwith"No active provider session is bound to this thread."That string is not in any of the three approval stale-failure matchers (packages/client-runtime/src/pendingRequests.ts,ProjectionPipeline.isStalePendingApprovalFailureDetail,decider.isStaleRequestFailureDetail).stalePendingRequestDetail("approval", …)would match, but it is only used for unknown-request errors from a live session.onRespondToApprovalgate on thread identity only, so Approve/Deny still reaches that branch after the session is gone.reconcileProviderSessionsmarks orphaned sessions stopped and does not emitapproval.resolved, so a restart does not repair the pending-approval count.hasPendingApprovals/openRequeststhen block settle and snooze.Graceful
stopSessionon Claude, Cursor, Grok, Codex, and OpenCode (after #9653) already emitsrequest.resolved. The remaining hole is abrupt death (server restart, crash, or any stopped session that never went through adapter teardown).Not live-reproduced here. The cited code paths are enough to accept this without a runtime repro.
Not a duplicate
runtimeMode.activeTurnId; same family, different path.No open PR currently
Fixes/Closes#11799.Recommended fix
Keep this issue open. Do not expand #10586 (already approved and scoped to questions).
Open a new PR that mirrors #10586 for approvals:
approval.resolved(or equivalent dismissal) onsession.exited/ reconcile so the card andpending_approval_countclear without a click."no active provider session is bound to this thread"to the three approval matchers as defense in depth.Matcher-only (the #5453 / #4598 approach) is not enough for the stated expected outcome: settle stays blocked until someone presses Approve/Deny.
Severity
Bug / major. The approval card stays open, every response attempt appends another failure, and the thread cannot settle or snooze.