Skip to content

Web/Desktop stale permission prompt returns Permission request not found after interrupted approval #29422

Closed as not planned

Description

@MingyooLee

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:

  1. Permission.ask creates a pending request and publishes permission.asked.
  2. The request is cleaned up before approval, either by instance reload/cleanup or by interrupting the ask fiber.
  3. permission.list() is empty after cleanup.
  4. Replying later with the old request ID fails with Permission.NotFoundError.
  5. 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.

Activity

  1. github-actions commented on May 26, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. MingyooLee commented on May 26, 2026

    @MingyooLee
    Author

    Clarifying the duplicate suggestions from the bot:

    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.

  3. MingyooLee commented on May 26, 2026

    @MingyooLee
    Author

    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.

  4. code-yeongyu commented on May 28, 2026

    @code-yeongyu
    Contributor

    [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 in Permission.layer skip event emission:

    • The instance-state finalizer (~L158-165) fails each pending Deferred and clears state.pending. No Event.Replied ever fires.
    • Permission.ask's Effect.ensuring (~L205-209) deletes the entry on interrupt. Same — no event.

    Meanwhile packages/app/src/context/global-sync/event-reducer.ts only drops prompts on permission.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.Replied with reply: "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.

  5. yale-Liyao commented on Jun 4, 2026

    @yale-Liyao

    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 the task tool. 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.

  6. github-actions commented on Aug 11, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

  7. Shehab-Hegab commented on Sep 22, 2026

    @Shehab-Hegab

    Fresh PR with the server-side fix + regression tests: #50595 (revives #40100, which was closed by automated cleanup rather than review).

    Verified locally:

    • \�un test test/permission/next.test.ts\ — 80 pass including new \�sk - publishes rejected reply event when interrupted\ (fails with timeout without the fix)
    • \�un typecheck\ — exit 0

    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.

  8. added a commit that references this issue on Sep 23, 2026
    2ad589a
  9. 1fanwang commented on Sep 27, 2026

    @1fanwang

    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 found and left the session blocked with the prompt still visible. I could not clear it from the UI.

  10. InCerryGit commented on Sep 29, 2026

    @InCerryGit

    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_reply tool failed with:

    Task reply transport failed: Permission request not found: per_...
    

    (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.ts L254–L258: the ask cleanup path deletes the pending entry in Effect.ensuring (pending.delete(item.request.id)) and does not publish permission.replied. Permission.reply (L269) returns NotFoundError when the entry is missing, so an event-driven client has no removal signal.
    • packages/server/src/handlers/permission.ts L13–L28: requireOwnedRequest returns the same PermissionNotFoundError for a missing request and for request.sessionID !== sessionID. "Already cleaned up" and "wrong session owner" are indistinguishable to the caller.
    • In the bundled plugin, createTaskReplyTool resolves the tracked child wait, calls client.permission.reply(...), and clears the wait only after a successful reply; any non-timeout error is rethrown as Task reply transport failed: .... So a not-found reply leaves the tracked wait registered, and each retry hits the same failure.

    Limitations:

    • Consistent hypothesis, not a confirmed reproduction. The retained local logs only show the child permission wait being opened; they do not retain the failure timestamp, and the failed transcript is no longer available locally.
    • The same child session later reached a terminal completed/succeeded state, so the impact here stayed on the parent-side reply path instead of leaving the session permanently stuck.
    • Cleanup and session-ownership mismatch both produce the same typed error, so I cannot confirm which one applied.

    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.

  11. pieczorx commented on Oct 13, 2026

    @pieczorx

    same here, UI blocked

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions