Skip to content

Permissions from interactive TUI turns can't be answered via the HTTP API (separate pending stores) #36835

Closed as not planned

Description

@henrikekblad

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

  1. Start opencode (TUI) with a permission set to ask.
  2. In the TUI, send a prompt that triggers a permission (e.g. a bash command).
  3. From another client, hit the HTTP API on the TUI's port:
    • GET pending permissions → empty list, even though the TUI is visibly showing the prompt.
    • POST a reply for the shown request id → HTTP 200, but the TUI prompt stays and the agent remains blocked.
  4. 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.ts keeps pending permissions in an in-process Map (const pending = new Map(...)), and reply() looks up pending.get(input.requestID), throwing NotFoundError when the id isn't present.
  • There are two parallel permission systems — v1 (permission.asked/permission.replied, POST /session/:id/permissions/:permissionID with {response, remember?}) and v2 (permission.v2.*, .../permission/:id/reply with {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, no permission.replied published → TUI never dismisses. ❌

Related

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

  1. github-actions commented on Sep 13, 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.

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