Repository navigation
Web/Desktop stale permission prompt returns Permission request not found after interrupted approval #29422
Description
What happened
Web/Desktop can keep showing permission prompts after the underlying tool/subagent request has already been interrupted or cleaned up. When I approve those prompts later, they all fail with:
Permission request not found
This leaves the session stuck. The original tool/subagent never continues, approving the prompt does not unblock anything, and I cannot continue my work from that session without recovering/restarting/refreshing around the stale state.
This is not tied to a specific command. I saw it for harmless commands and file operations, including date, gh --version, gh search discussions --help, GIT_MASTER=1 git status --short, reading GEMINI.md, and reading/editing packages/opencode/test/permission/next.test.ts.
Expected behavior
If the underlying permission request is no longer pending, Web/Desktop should not keep an approve-able prompt around. It should either remove the prompt when the request is cleaned up, or recover from PermissionNotFoundError by clearing/refetching local permission state.
Confirmed behavior
Local service-level tests reproduce the stale flow directly:
Permission.askcreates a pending request and publishespermission.asked.- The request is cleaned up before approval, either by instance reload/cleanup or by interrupting the ask fiber.
permission.list()is empty after cleanup.- Replying later with the old request ID fails with
Permission.NotFoundError. - No
permission.repliedevent is emitted for that stale reply, so a client that still has the prompt has no event-driven removal path.
Focused tests:
reply - stale request after cleanup fails without replied event
reply - stale request after ask interrupt fails without replied event
Verification:
bun test test/permission/next.test.ts -t "stale request"
# 2 pass, 0 fail
bun test test/permission/next.test.ts
# 81 pass, 0 fail
bun typecheck
# passed
bun test test/server/httpapi-instance.test.ts -t "returns typed not found bodies for missing permission"
# 1 pass, 0 fail
Root cause narrowed down
In packages/opencode/src/permission/index.ts, Permission.ask stores the request in the instance-local pending map and removes it in the cleanup/finalizer path. Permission.reply checks pending.get(input.requestID) and returns Permission.NotFoundError before publishing permission.replied if the request is no longer pending.
That service behavior is understandable, but Web/Desktop can still retain the old prompt from the earlier permission.asked event. Once that happens, every later click on the stale prompt is guaranteed to fail with Permission request not found, and the blocked tool/subagent never runs.
Likely fix direction
A fix probably needs one or more of these:
- emit a cleanup/removal signal when a pending permission is rejected by interruption/finalization
- make Web/Desktop treat
PermissionNotFoundErroras a stale local prompt and remove/refetch permissions immediately - reconcile Web/Desktop local permission state with
permission.list()on reconnect/bootstrap/focus
Related issues
This may share the same stale-client-state class as #28312, but #28312 is the TUI attach flow. This report is specifically Web/Desktop and uses the typed Permission request not found failure after clicking a stale prompt.
#15386 overlaps with stale/non-existent permission IDs, but that issue was about the reply endpoint silently returning 200 true. The current behavior now exposes the stale request as a typed 404; the remaining bug is that Web/Desktop can still keep and offer a prompt whose server-side pending request is already gone.
#26907 and #28651 are related but cover a different case: a child-session prompt remaining after a successful/live reply path. This issue is the earlier/staler case where approval never reaches a live pending request because the underlying ask was interrupted or cleaned up first.
Activity
github-actions commentedon May 26, 2026
This issue might be a duplicate of existing issues. Please check:
POST /permission/{requestID}/replyreturns 200/true for non-existent permission IDs #15386:POST /permission/{requestID}/replyreturns 200/true for non-existent permission IDs (covers the silent-success reply behavior; your report notes the endpoint now returns a typed 404, so this may be partially addressed upstream, but the underlying lifecycle issue overlaps)MingyooLee commentedon May 26, 2026
Clarifying the duplicate suggestions from the bot:
Permission request not found.POST /permission/{requestID}/replyreturns 200/true for non-existent permission IDs #15386 covered stale/non-existent permission IDs returning200 true. That part now appears to return a typed 404, but the user-visible bug remains: Web/Desktop can still keep an approval prompt after the server-side pending permission is already gone.The main impact is that the session is stuck: the original tool/subagent never continues, approving the prompt cannot unblock it, and work cannot proceed from that session until the stale state is recovered/reset.
MingyooLee commentedon May 26, 2026
One more bit of context: I notice this more often in OMO / heavy subagent workflows, probably because they create and interrupt more nested tool permission requests. But this does not seem OMO-specific: the stale reply behavior is reproduced at the core permission lifecycle level, and I also saw it with simple commands and file operations.
code-yeongyu commentedon May 28, 2026
[jobdori] hey — took a closer look on our side. Your root cause is spot-on.
The gap is in
packages/opencode/src/permission/index.ts. Both cleanup paths inPermission.layerskip event emission:Deferredand clearsstate.pending. NoEvent.Repliedever fires.Permission.ask'sEffect.ensuring(~L205-209) deletes the entry on interrupt. Same — no event.Meanwhile
packages/app/src/context/global-sync/event-reducer.tsonly drops prompts onpermission.replied, so Web/Desktop holds the stale prompt. Server has no record, the click returns exactly the typed 404 you described.Minimum-touch fix: in both cleanup paths, publish
Event.Repliedwithreply: "reject"for each entry before deleting. The reducer already handles that event correctly — no client change needed for this class.The reconcile-on-reconnect idea you mentioned is still nice as belt-and-suspenders for #28312-style cases, but it's not strictly needed for this one.
We notice this disproportionately in OMO (oh-my-openagent) because heavy subagent fan-out + aborts slam those cleanup paths, but as you confirmed it reproduces at the core service level. Not OMO-specific.
Happy to validate a patch if someone pushes one. Thanks for the clean writeup — the focused tests made tracing this much faster.
yale-Liyao commentedon Jun 4, 2026
I'm hitting this same issue on v1.15.13 (desktop app, Windows 11) with the following specifics:
Plugin context: Using
oh-my-openagent@next— a multi-agent orchestration plugin (Sisyphus, Atlas, Prometheus) that extensively spawns subagents via thetasktool. Multiple parallel subagent sessions create competing permission requests, which likely exacerbates the stale prompt problem.Source code observation: The error
Permission request not found: ${requestID}fires at two locations in the bundled app.asar (lines 1405413 and 1405985). The permission request lifecycle uses an in-memory RcMap with setTimeout-based TTL cleanup, which explains why requests get garbage-collected while the dialog is still visible.Workaround: Setting
"allow"for frequently-triggered permissions reduces the frequency, but doesn't fix the root cause.github-actions commentedon Aug 11, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.
Shehab-Hegab commentedon Sep 22, 2026
Fresh PR with the server-side fix + regression tests: #50595 (revives #40100, which was closed by automated cleanup rather than review).
Verified locally:
Repro timeline from a local session log confirms the root cause:
\
09:03:19.699 asking id=per_0c85af... permission=doom_loop
09:03:19.709 cancel session=...
09:03:19.713 AbortError
\
Cleanup deleted the pending entry without \permission.replied, so the Desktop prompt stayed and later Allow returned \Permission request not found.
fix(permission): publish replied event on cleanup paths
1fanwang commentedon Sep 27, 2026
I hit this in the app UI after leaving a permission prompt unanswered for a while. When I tried to respond, the app showed
Permission request not foundand left the session blocked with the prompt still visible. I could not clear it from the UI.InCerryGit commentedon Sep 29, 2026
Plugin-side / child-session observation (OpenCode v2.0.19, oh-my-opencode-slim 2.2.25)
The server-side root cause is already covered above; this adds an integration data point from a plugin that answers child-session permission requests.
The parent-side
task_replytool failed with:(request ID truncated; the "transport failed" prefix is the plugin's wrapper around the host error.)
From the v2.0.19 sources:
packages/core/src/permission.tsL254–L258: the ask cleanup path deletes the pending entry inEffect.ensuring(pending.delete(item.request.id)) and does not publishpermission.replied.Permission.reply(L269) returnsNotFoundErrorwhen the entry is missing, so an event-driven client has no removal signal.packages/server/src/handlers/permission.tsL13–L28:requireOwnedRequestreturns the samePermissionNotFoundErrorfor a missing request and forrequest.sessionID !== sessionID. "Already cleaned up" and "wrong session owner" are indistinguishable to the caller.createTaskReplyToolresolves the tracked child wait, callsclient.permission.reply(...), and clears the wait only after a successful reply; any non-timeout error is rethrown asTask reply transport failed: .... So a not-found reply leaves the tracked wait registered, and each retry hits the same failure.Limitations:
For plugin-side handling, the practical direction seems to be: on a not-found reply, reconcile against the host's pending permissions for the owning session and only then clear the stale wait. Treating every not-found/transport error as "already answered" would be wrong — it does not prove the reply reached a live request.
PR #50595 targets the server-side event gap and is open as of this writing; this comment only adds the plugin/child-session angle and does not claim the fix is in a release.
pieczorx commentedon Oct 13, 2026
same here, UI blocked