Repository navigation
Description
Description
When a location instance is disposed (POST /global/dispose, any effective PATCH /global/config, SIGUSR2 TUI reload), pending permission requests are destroyed without emitting permission.replied or any revocation event, and the associated tool-call part is never finalized — it persists in the message store as status:"running", executed:false. Clients rebuild the dialog from that part on every load, so the user is stuck with a permission prompt that can never be answered: every Allow/Deny replies to an ID the server no longer knows → 404 Permission request not found: per_... → error toast, retried in a loop (observed: 178 × 404 in ~2.5 h, bursts of 109/min). In-flight tool calls in other windows die with {"type":"aborted","message":"Tool execution interrupted"}. Reload, closing/reopening the window, and even restarting the service do not clear the ghost dialog — the source is persisted data. This makes unattended/long-horizon multi-session work on a shared service structurally unsafe.
- Plugins: one local plugin watching the global config dir (not required for repro)
- OpenCode version: 2.0.18 (channel=latest)
- OS: macOS (Darwin 25.6.0, arm64)
- Terminal: shared background service (
opencode serve --service), web client in Safari
Steps to reproduce
- With the shared service running, open a web UI session and trigger a tool call that resolves to
ask(permission dialog waiting). Leave it unanswered. - From a terminal, force a disposal:
curl -X POST http://127.0.0.1:<port>/global/dispose(or make any effectivePATCH /global/configchange; orkill -USR2 <tui-pid>). - Click Allow (or Deny) on the still-visible dialog →
请求失败 / Permission request not found: per_...toast; server log showsPOST .../permission/per_.../reply→ 404. GET /api/session/:id/permissionreturns[]while the dialog persists.- Reload / reopen the window → dialog reappears, rebuilt from the persisted non-terminal tool part. No user action can clear it.
Expected
Disposal should be survivable or self-healing for clients: emit a terminal permission.replied (rejected/interrupted) per destroyed request, finalize the persisted tool part, and reconcile client dialogs against the pending list. A reply to an unknown request should not leave clients retrying forever (PermissionNotFoundError should be treated as terminal by the client).
Notes / code pointers (2.0.18)
permission/index.ts(~L50–59): the pendingMapis instance-scoped state; the scope finalizer fails each pending deferred withRejectedErrorand clears the map without emitting any event, unlike the normalreply()path.handlers/global.ts(~L80): config update withchanged→disposeAllInstancesAndEmitGlobalDisposed(); disposal interrupts all fibers without quiescing in-flight sessions, so the session runner never writes a terminal state to the awaiting tool part.- Web
event-reducerhandlesserver.instance.disposed(directory resync) but does not reconcile dialogs rebuilt from persisted non-terminal parts against the empty pending list. cli/cmd/run/stream.transport.ts(~L206, ~L1140):opencode runtreatsserver.instance.disposedas a hard stream failure ("instance disposed") — headless/looped callers see an undifferentiated run error; same non-terminal part outcome when disposal is triggered by provider errors (Failed to drain Session→ HTTP 400 / invalid key / rate limit).- Field timeline (sanitized): subagent permission
per_Acreated 09:09 UTC, live-pending confirmed via API 09:12, mass disposal at 09:31:42 (±2 s of concurrent CLI gate-test activity), user clicks at 12:59 and 14:01 → 404 ×3; an older dialog produced the 178 × 404 storm.session_pending/eventDB tables hold no trace of the destroyed requests — pending-permission state is memory-only while the tool part is persisted, and the two diverge permanently after disposal. - Screenshot: error dialog shows
请求失败 / Permission request not found: per_0fae....
Activity
github-actions commentedon Oct 2, 2026
This issue might be a duplicate of existing issues. Please check:
Permission request not foundbecause no terminal event clears the stale prompt.weixiongshang commentedon Oct 5, 2026
Adding a field repro for the automatic disposal path — no manual
/global/disposeor config change needed. The 60-minute location idle TTL evicts the location while a permission request is still pending, and the client is never told.Version 2.0.22, macOS arm64, shared background service, Desktop client:
T+0: a tool resolves toask(readof a path outside the worktree →external_directory). The run blocks; nobody answers.T+41m: last activity on the location (a queued shell-result message).T+101m: exactly 60 minutes after that last activity, the server log showslocation services evicted directory=<project>followed immediately byInterruptError: All fibers interrupted without error; the waiting tool part is finalized as aborted (Tool execution interrupted).POST /api/session/<ses>/permission/<per_>/reply → 404whileGET /api/session/<ses>/permissionreturns[]. The composer stays blocked because a visible prompt replaces it.So disposal does not need to be forced for this failure mode: an unanswered prompt older than the idle TTL reproduces the same silent drop + 404 loop. The 60-minute sweep is also what #52908 measures for running shells; this is the same eviction hitting a pending
PermissionV2wait, which nothing in the TTL logic counts as a borrower.I opened #53272 for the client half — treat
PermissionNotFoundErroras terminal (drop the stale prompt locally, stop the auto-respond retry loop), mirroring the TUI fix #40960. The server half (publishpermission.repliedon cleanup) is #50595.Bliird commentedon Oct 14, 2026
Reproduced on Windows, no plugins, no forced dispose.
Env: OpenCode 2.0.28, desktop app (OPENCODE_CLIENT=desktop), Windows_NT 10.0.26300 x64
Trigger: { "action": "shell", "resource": "*", "effect": "ask" }, prompt left unanswered several hours. No plugin, no subagent fan-out, no /global/dispose, no config change.
Symptom: all three buttons (reject / always allow / allow once) -> "Permission request not found: per_139bcabe200117QMXM8G3Esxwj". Dialog stays, session stays blocked, no in-UI escape.
Cleared by: full app quit only. Closing the window did not.
Still broken in: 2.0.28 (V2).
Matches the idle path @weixiongshang described - nothing forced it, time alone dropped the pending request.
Fix status: #50595 (server: publish permission.replied on cleanup) closed Oct 10 as V1, never merged. #53272 (client: treat PermissionNotFoundError as terminal) closed. TUI equivalent is #40960.
Question: is #50595 being ported to V2, or should it be re-opened against dev?
dskajita commentedon Oct 27, 2026
Another trigger, reproduced on a clean sandbox with a scripted fake model (no real provider), version 2.1.4, macOS arm64: interrupting a session that is waiting on a permission.
Steps:
opencode servewith an agent whoseexternal_directoryisask.readof a path outside the project.GET /api/session/<child>/permissionlists the pending request (action: external_directory). The parent's list is empty, andGET /api/session/activejust saysrunningfor both sessions.POST /api/session/<child>/interrupt.Result: the server drops the request (the child's permission list is empty, the tool call ends as
aborted, the parent's sub-agent call ends as cancelled), but the event stream (GET /api/event) never publishes apermission.repliedor any other permission event for that id. Order afterpermission.asked:session.tool.failed(aborted) ->session.step.failed->session.execution.interrupted. A client that tracks pending prompts from events keeps the dialog, and every reply then fails withPermission request not found: per_...(seen in the desktop/web UI, Deny and Allow alike).So interrupt is one more cleanup path with the same gap as disposal and idle eviction: the request is removed without a closing event. Publishing the existing replied/rejected event (or a dedicated revoked event) from every path that removes a pending request would cover all of them. The closed #50595 did this for the cleanup paths.
Side effect for headless use: a permission raised in a sub-agent session is not visible from the parent session or the active-sessions endpoint, so a run waits silently until someone opens the child session.
opencodejr commentedon Oct 27, 2026
Thanks for the detailed report. I tried to reproduce this on 2.0.18 and couldn't.
What I did: shared server, a simulated model, and a permission rule set to
ask. The agent made a shell tool call, so a "Permission required" prompt was waiting, andGET /api/session/:id/permissionlisted theper_…request. Then I triggered instance disposal.Findings:
POST /global/disposereturns 405. That route, anddisposeAllInstancesAndEmitGlobalDisposedin the notes, belong to the v1 server. The v2 equivalent isPOST /api/location/reload, which shuts down and rebuilds every loaded location.[].status: "error"withInteraction cancelled because the location shut down. It did not stayrunning.permission.replied(reject) for each pending request when it shuts down. The web client already treatsPermissionNotFoundErrorfrom a reply as final, and the web permission dialog is driven by the pending list, not by saved tool parts.If you still see this, it would help to have:
opencode --versionfrom the binary actually running the service, plus how it was installed. Please check that no v1 (opencode-ai) server is still running on that port.GET /api/session/<id>/message, while the prompt is still showing.opencodejr commentedon Oct 30, 2026
We haven't heard back on this, so I'm closing it as not planned for now.
On 2.0.18, reloading every location with
POST /api/location/reload(the v2 equivalent of/global/dispose, which returns 405 on v2) cleared the pending request. The tool part ended asstatus: "error"(Interaction cancelled because the location shut down), and the permission prompt went away. We couldn't get the 404 loop or the ghost prompt to happen.If it still happens, please comment here with:
opencode --versionfrom the binary running the serviceGET /api/session/<id>/messageWe'll reopen and look again. Thanks for the detailed write-up.