Skip to content

Permission prompt hangs when viewing a subagent #56907

Open

Description

@AILIFE4798

Description

Opening a subagent session hides its permission prompt. Auto-accept uses that same empty list, so the subagent stays blocked with nothing to approve or deny.

Plugins

None

OpenCode version

2.0.27 / current dev

Steps to reproduce

  1. Start a subagent that needs a shell permission.
  2. Open that subagent session.
  3. Try to answer the prompt, or turn auto-accept on.

The prompt does not appear, and auto-accept does not reply.

Screenshot and/or share link

Operating System

Linux

Terminal

gnome-terminal

Activity

  1. opencode-agent commented on Oct 19, 2026

    @opencode-agent
    Contributor

    Thanks for the clear report. I reproduced this on 2.0.27, and it still happens on the latest dev (501b15a).

    Setup: shell permission set to ask, and the parent agent starts the general subagent, which runs echo hello-from-child.

    • Prompt mode: the parent view shows "Permission required". After opening the subagent session (↓, then Enter), the prompt is gone and the shell call just spins with nothing to approve or reject.
    • Auto-accept (session.permissions: "autoaccept"): if the subagent asks for permission while its session is open, the request is never approved and the subagent stays "Running".

    The recording below shows the auto-accept case. The screenshots show the prompt on the parent view, the same request with no prompt in the subagent view, and the stuck auto-accept run. The script below reproduces both modes.

    The likely cause is in packages/tui/src/routes/session/index.tsx: the permission list is empty whenever the viewed session has a parent, and both the prompt and auto-accept read that list. Your PR #56906 changes this. Related but different: #51815 (the request is never recorded as pending) and #52411 (v1, attaching from a second instance).

    recording.mp4

    Screenshot

    Screenshot

    Screenshot

    Reproduction script (drive.ts)
    // Reproduces anomalyco/opencode#56907: while a subagent (child) session is open,
    // its pending permission prompt is not shown, and auto-accept does not answer it.
    //   default : prompt mode  -> prompt is visible on the parent, hidden in the child view
    //   AUTO=1  : autoaccept   -> request raised while viewing the child is never approved
    import { Effect } from "effect"
    import { Llm, OpenCodeDriver } from "opencode-drive"
    
    const auto = !!process.env.AUTO
    
    export default OpenCodeDriver.use(
      {
        ...(process.env.OC_DEV ? { opencode: { dev: process.env.OC_DEV } } : {}),
        tui: { recording: true, keypressOverlay: true },
        project: { git: true, files: { "README.md": "hello\n" } },
        // Drive's default config allows everything; make shell ask.
        config: {
          permissions: [
            { action: "*", resource: "*", effect: "allow" },
            { action: "shell", resource: "*", effect: "ask" },
          ],
        } as any,
        ...(auto ? { tuiConfig: { session: { permissions: "autoaccept" } } as any } : {}),
      },
      ({ ui, llm }) =>
        Effect.gen(function* () {
          // Parent: spawn a subagent.
          yield* llm.queue(
            Llm.toolCall({
              index: 0,
              id: "call_sub",
              name: "subagent",
              input: { agent: "general", description: "Run a shell command", prompt: "Run `echo hello-from-child`" },
            }),
            Llm.finish("tool-calls"),
          )
          yield* ui.submit("Delegate a shell command to a subagent")
          yield* ui.waitFor("1 subagent")
          if (!auto) {
            // Child asks for shell permission right away; parent view shows the prompt.
            yield* llm.queue(
              Llm.toolCall({ index: 0, id: "call_shell", name: "shell", input: { command: "echo hello-from-child", description: "Echo" } }),
              Llm.finish("tool-calls"),
            )
            yield* ui.waitFor("Permission required")
            yield* ui.screenshot("1-parent-view-prompt-visible")
          }
          // Open the subagent session (down opens the picker, enter opens the child).
          yield* ui.press("down")
          yield* Effect.sleep(1000)
          yield* ui.press("return")
          yield* Effect.sleep(2000)
          if (auto) {
            // Child asks for shell permission while its session is the one being viewed.
            yield* llm.queue(
              Llm.toolCall({ index: 0, id: "call_shell", name: "shell", input: { command: "echo hello-from-child", description: "Echo" } }),
              Llm.finish("tool-calls"),
            )
          }
          yield* Effect.sleep(3000)
          yield* ui.screenshot("2-child-view")
          yield* Effect.sleep(5000)
          const frame = yield* ui.capture()
          const text = JSON.stringify(frame)
          yield* ui.screenshot("3-child-view-later")
          yield* Effect.log(
            `permission prompt visible in child view: ${text.includes("Permission required")}; ` +
              `command ran: ${text.includes("hello-from-child\\n") || /\$ echo hello-from-child/.test(text)}`,
          )
        }),
    )

    Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.

  2. liaochzh commented on Oct 23, 2026

    @liaochzh

    Adding server-side evidence for the subagent permission-prompt hang (reported in #56907; PR #56906 proposes keeping prompts in subagent views). The behaviour I see is not only "the prompt is hidden in the subagent view" — the server never answers the child's permission endpoints with anything but 404, so even a client that polls the child session gets nothing, and the tool call can only end when the engine aborts it after 3600 s.

    Summary

    Permission requests raised inside a child session (subagent) are never surfaced to the client. Both permission endpoints of a child session answer 404 Not Found:

    • GET /api/session/<child>/permission — the pending-request list the client polls
    • POST /api/session/<child>/permission/<requestID>/reply — the reply

    Consequently the tool call stays pending for exactly 3600 s and is then aborted by the engine with Tool execution interrupted (outcome="interrupted"), with no actionable message for the user.

    Environment

    • opencode version: v2.1.2
    • OS: macOS 27.0.0 (darwin arm64)
    • Terminal: TERM=xterm-256color (TERM_PROGRAM / COLORTERM unset)
    • Shell: /bin/zsh
    • Install/channel: latest (binary at ~/.opencode/bin/opencode)
    • Active plugins: none found in config (no plugin key in ~/.config/opencode/opencode.jsonc, no ~/.config/opencode/plugin/ or plugins/ directory)

    Reproduction

    1. Give a subagent a rule that raises an approval request, e.g. in ~/.config/opencode/opencode.jsonc:
      { "action": "glob", "resource": "**/*.md", "effect": "ask" }
    2. From a primary session, dispatch any subagent and let it run a tool that hits that rule (a single glob call is enough).
    3. No approval UI appears; the primary session just waits.
    4. After 3600 s the tool part ends with
      error={"type":"aborted","message":"Tool execution interrupted"}
      step outcome="interrupted"
      
    5. In the server log, every permission request for the child session is answered 404:
      url=/api/session/ses_<child>/permission http.status=404
      url=/api/session/ses_<child>/permission/per_<requestID>/reply http.status=404
      

    Expected Behavior

    Either (a) the child's permission request is surfaced to the user — directly, or forwarded to the parent session — and can be answered, or (b) if child-session approval is intentionally unsupported, the call must fail fast with an explicit error instead of hanging for 60 minutes.

    A 404 from the reply endpoint is also ambiguous for the client: 404 currently means "no such pending request", but here the request is pending and simply is not reachable through that endpoint, so the caller cannot tell "wrong request id" from "request exists in another scope".

    Actual Behavior

    • GET /api/session/<child>/permission → 404 — the child's pending requests are never listed.
    • POST /api/session/<child>/permission/<requestID>/reply → 404 — the reply never reaches the pending request.
    • The tool call remains pending for 3600 s and is then aborted by the engine.

    In packages/server/src/handlers/permission.ts (v2.1.2) the two endpoints are scoped differently:

    • session.permission.list resolves the session first (sessionInfo(sessions, ctx.params.sessionID)) and only then calls permission.forSession(...) with instances.provide(session). For a child session the session lookup itself fails — in my log the child sessions also answer 404 for the plain GET /api/session/<id> (17 such responses) — so the handler returns "session not found" → 404 and the pending list is never consulted.
    • session.permission.reply goes through requireOwnedRequest(...), which calls permission.get(requestID) without providing the location instance, and converts a miss into PermissionNotFoundError (→ 404) when the request is not in that instance's in-memory pending map, or when its sessionID does not match the URL.
    • Permission is a location-scoped node while SessionStore / PermissionSaved are global nodes, so the two endpoints resolve their dependencies differently.

    Net effect for a child session: the client can neither list the pending requests nor apply a reply to one, so the request stays unanswered until the engine aborts the call — the 3600 s timeout is the only exit.

    Additional Context

    • Scope is the session type, not the tool. The same ask rule is answered normally in the parent session (approving it stores a saved/"always allow" row); in a child session it never is. The hang also covers shell and external_directory faces, not only glob.
    • Observed volume. In a local database sweep over 1391 child sessions I counted 47 aborted tool parts in 31 sessions; 36 of them ran longer than 3000 s, i.e. they hit the same timeout. Face distribution of those 47: shell 24, glob 12, workspace-external read/grep 9, subagent 1, question 1. (The question case is a different class of hang — the tool itself waits for an answer, not a permission decision; question is not even exposed to subagents in the current tool set.)
    • Reproduction commands (local):
      opencode --version
      grep -o 'url=/api/session/ses_[A-Za-z0-9]*/permission[^ ]* http.status=[0-9]*' \
        ~/.local/share/opencode/log/opencode.log | sort | uniq -c
      In my log this yields only 404 — 93 permission-path requests (90 × reply, 3 × list) and never a 2xx.
      The plain session endpoint fails the same way for those child sessions:
      grep -o 'http.method=GET http.url=/api/session/ses_[A-Za-z0-9]* http.status=[0-9]*' \
        ~/.local/share/opencode/log/opencode.log | sed -E 's/.*(http.status=[0-9]+)/\1/' | sort | uniq -c
      #   17 http.status=404
    • Related reports (same family, different vantage point): Permission prompt hangs when viewing a subagent #56907 (prompt hidden in the subagent view; PR fix(session): keep permission prompts in subagent views #56906), permissions: subagent tool call blocked indefinitely on unanswered external_directory request, reported as running #51815 (subagent blocked indefinitely on an unanswered external_directory request), desktop: permission dialog stuck after tool interrupt/timeout; Deny/Allow replies 404 forever, composer blocked #55678 (permission dialog stuck after interrupt/timeout; replies 404), permissions: ask effect is fail-open for shell commands (command runs, client reply gets 404) #56470 (ask fail-open for shell; client reply 404). What I could not find covered elsewhere is the server-side endpoint behaviour (list and reply both 404, with the location/global scope asymmetry) and the exact 3600 s abort.
    • Workaround on the user side: avoid ask rules inside subagents (use allow/deny), so the 60-minute hang cannot be triggered. I am not aware of a server-side workaround.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions