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.ask creates a pending request and publishes permission.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.replied event 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
PermissionNotFoundError as 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.
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:
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, readingGEMINI.md, and reading/editingpackages/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
PermissionNotFoundErrorby clearing/refetching local permission state.Confirmed behavior
Local service-level tests reproduce the stale flow directly:
Permission.askcreates a pending request and publishespermission.asked.permission.list()is empty after cleanup.Permission.NotFoundError.permission.repliedevent is emitted for that stale reply, so a client that still has the prompt has no event-driven removal path.Focused tests:
Verification:
Root cause narrowed down
In
packages/opencode/src/permission/index.ts,Permission.askstores the request in the instance-local pending map and removes it in the cleanup/finalizer path.Permission.replycheckspending.get(input.requestID)and returnsPermission.NotFoundErrorbefore publishingpermission.repliedif the request is no longer pending.That service behavior is understandable, but Web/Desktop can still retain the old prompt from the earlier
permission.askedevent. Once that happens, every later click on the stale prompt is guaranteed to fail withPermission request not found, and the blocked tool/subagent never runs.Likely fix direction
A fix probably needs one or more of these:
PermissionNotFoundErroras a stale local prompt and remove/refetch permissions immediatelypermission.list()on reconnect/bootstrap/focusRelated 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 foundfailure 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.