Repository navigation
Permissions from interactive TUI turns can't be answered via the HTTP API (separate pending stores) #36835
Description
Summary
When a permission is raised by a turn started in the interactive TUI, answering it through the HTTP API (permission.reply / POST /session/:id/permissions/:permissionID) does not resolve the prompt: the TUI keeps showing it and the agent stays blocked. The same API call does work for turns started over the API (e.g. POST /session/:id/prompt). This makes it impossible for an external client (dashboard, phone, remote approver) to answer a permission that a user triggered locally in the TUI.
Environment
- opencode
1.17.18, single process (TUI also exposes its local HTTP server). - Permission mode:
ask.
Steps to reproduce
- Start
opencode(TUI) with a permission set toask. - In the TUI, send a prompt that triggers a permission (e.g. a
bashcommand). - From another client, hit the HTTP API on the TUI's port:
GETpending permissions → empty list, even though the TUI is visibly showing the prompt.POSTa reply for the shown request id → HTTP 200, but the TUI prompt stays and the agent remains blocked.
- For contrast: start a turn via
POST /session/:id/prompt, trigger a permission, and reply over the API → works, prompt is resolved.
Expected
An externally submitted reply for a pending permission resolves it and dismisses the TUI prompt, regardless of whether the turn was started in the TUI or over the API.
Root cause (from source, current tree)
The TUI is designed to accept external answers — packages/tui/src/context/sync.tsx has case "permission.replied": that splices the request out of its store when a reply event arrives on the bus. So the display side is fine.
The break is below it:
packages/core/src/permission.tskeeps pending permissions in an in-processMap(const pending = new Map(...)), andreply()looks uppending.get(input.requestID), throwingNotFoundErrorwhen the id isn't present.- There are two parallel permission systems — v1 (
permission.asked/permission.replied,POST /session/:id/permissions/:permissionIDwith{response, remember?}) and v2 (permission.v2.*,.../permission/:id/replywith{reply}).
The interactive TUI turn and the HTTP API appear to be backed by different permission pending stores (v1/v2 split and/or directory/workspace scoping — the TUI's own auto-reply at sync.tsx passes directory, workspace). Consequently:
- API-started turn → permission lives in the store the API resolves against → reply publishes
permission.replied→ TUI dismisses. ✅ - TUI-started turn → permission lives in the interactive store → the API's pending list is empty and its reply hits a different map →
NotFoundError/no-op, nopermission.repliedpublished → TUI never dismisses. ❌
Related
- Agent hangs indefinitely when using
opencode serve+opencode attachif any permission is set to "ask" #16367 (opencode serve+attachhangs when a permission is set toask) — same class of remote-permission plumbing. - Port ACP support to V2 core and APIs #35457 (Port ACP support to V2 core and APIs) — the v2 migration that would presumably unify this.
Impact
Any external approval UI can only answer permissions for turns it started itself, not for turns the user initiated locally in the TUI. (Found while building an external client that mirrors agent status and approves permissions remotely.)
Activity
github-actions commentedon Sep 13, 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.