#3332·Archon

fix(providers/opencode): workflow node hangs forever when OpenCode raises permission.updated

Author: tbrandenburgCreated Sep 15, 2026Updated Sep 17, 2026

Problem

A workflow AI node using the opencode provider can get stuck permanently in the running state after the assistant has already produced its final answer. The model's response streams through correctly and is visible in the run transcript, but the workflow node never transitions to completed. The run has to be manually cancelled/abandoned; nothing in Archon surfaces an error or a timeout during the hang.

Root cause: OpenCode's event stream includes permission.updated (EventPermissionUpdated) and permission.replied (EventPermissionReplied) events, used whenever the embedded OpenCode server needs an allow/deny decision for a permission-gated action (the SDK's Permission config supports ask for categories such as external_directory, doom_loop, bash, edit, etc). Archon's OpenCode provider never handles either event type and never calls the SDK's permission-reply endpoint. When the server emits permission.updated and blocks the session waiting for a reply, Archon's event loop just falls through every if branch without matching it, loops back to for await, and waits forever — session.idle (EventSessionIdle) never arrives because the session never leaves its permission-blocked state.

Both the single-agent (session.ts) and multi-agent (multi-agent.ts) OpenCode adapters have this gap, and neither agent-config.ts (which only builds the legacy tools: {name: boolean} map) nor runtime.ts's buildEmbeddedServerConfig (which only sets server: {hostname, port, password}) ever configures the SDK's separate permission: {...} policy for the embedded, non-interactive runtime — so there is no way to pre-authorize actions and no code path to answer a pending prompt on behalf of a headless run.

Why

Workflows are meant to run unattended. A silent, indefinite hang with no error and no operator prompt defeats that: the run occupies a worktree and a database row forever, the CLI's workflow wait eventually reports owner_lost instead of a clear terminal state, and nothing tells the operator why the node is stuck — they only see a correct-looking assistant answer in the transcript and a run that never finishes.

Desired outcome

An OpenCode-provider node reaches a terminal state (completed, or a clear failed with an actionable error) even when the OpenCode server raises a permission.updated event during the turn. There is no code path where an unanswered permission request causes an unbounded hang.

Acceptance

  • A scripted OpenCode event sequence containing a permission.updated event (with no accompanying session.idle until the permission is resolved) causes the node to either auto-resolve and finish, or fail fast with an error that names the pending permission — never hang past a bounded timeout.
  • Both the single-agent (session.ts) and multi-agent (multi-agent.ts) OpenCode adapters are covered by the fix (or the multi-agent path is confirmed unaffected with evidence).
  • A new test scripts permission.updated directly (the existing provider.test.ts only scripts session.idle and never exercises the permission-pending path).

Evidence

Reproduction

  1. Run any workflow with provider: opencode and a github-copilot/* or openai/* model against this repo checkout, e.g.:
    yaml
    name: my-opencode-smoke
    provider: opencode
    model: github-copilot/gpt-5.6-luna
    nodes:
      - id: simple
        prompt: "What is 2+2? Answer with just the number, nothing else."
  2. archon workflow run my-opencode-smoke --detach "run smoke test"
  3. Watch the transcript (~/.archon/workspaces/<project>/logs/<runId>.jsonl): the assistant event with the correct answer ("4") appears, but no further node_start/completion event ever follows. archon workflow get <runId> --json keeps reporting "status": "running" indefinitely; archon workflow wait <runId> eventually reports "result": "owner_lost" instead of a terminal outcome.

Reproduced 4 times across github-copilot/gpt-4.1, github-copilot/gpt-5-mini, and github-copilot/gpt-5.6-luna.

Confirmed event lifecycle

Verified against the pinned @opencode-ai/[email protected] (node_modules/.bun/@[email protected]/.../dist/gen/types.gen.d.ts):

typescript
export type EventPermissionUpdated = {
    type: "permission.updated";
    properties: Permission;
};
export type EventPermissionReplied = {
    type: "permission.replied";
    properties: { sessionID: string; permissionID: string; response: string };
};
export type Event = ... | EventPermissionUpdated | EventPermissionReplied | ... | EventSessionIdle | ...;

Archon's single-agent event loop only branches on four event types and silently drops everything else, including permission.updated:

https://github.com/coleam00/Archon/blob/dev/packages/providers/src/community/opencode/session.ts#L161

typescript
if (event.type === 'message.updated') { ... continue; }
if (event.type === 'message.part.updated') { ... continue; }
if (event.type === 'session.error') { ... throw err; }
if (event.type === 'session.idle') { ... return; }
// no branch for 'permission.updated' or 'permission.replied' — falls through
// to the next for-await iteration and waits forever if the server is blocked
// on that permission.

Neither of these ever configures a permission policy for the embedded, non-interactive server:

The multi-agent adapter follows the same pattern and has the same gap: https://github.com/coleam00/Archon/blob/dev/packages/providers/src/community/opencode/multi-agent.ts

Environment

  • Archon version or commit: v0.10.1, commit c15da516 (dev)
  • Platform or adapter: CLI, provider: opencode
  • Database: SQLite
  • OS: Linux x64
  • OpenCode: 1.18.30 CLI / @opencode-ai/[email protected]

Logs or screenshots

Transcript excerpt for one affected run (4919bba99779207f0dbafa518c63f971):

json
{"type":"node_start","step":"simple","workflow_id":"4919bba9...","ts":"...T06:45:08.417Z"}
{"type":"watchdog_reset","step":"simple","chunk_type":"assistant","ts":"...T06:45:30.906Z"}
{"type":"assistant","content":"What is 2+2? Answer with just the number, nothing else.","ts":"...T06:45:30.914Z"}
{"type":"watchdog_reset","step":"simple","chunk_type":"assistant","ts":"...T06:45:42.296Z"}
{"type":"assistant","content":"4","ts":"...T06:45:42.298Z"}

No further events. archon workflow get <runId> still reports "status": "running" and "terminal_record": null more than 10 minutes later.

Constraints and related work

  • Must remain true: a tool/action whose permission is legitimately "allow" (the common case) must keep working exactly as today — this is not a regression fix for the happy path, only for the previously-unhandled permission-pending path.
  • Known prerequisites or blockers: none identified.
  • Related issues or PRs: #3321 ("OpenCode workflow tool calls lose arguments after a pending event") touches the same event-loop area of session.ts/multi-agent.ts but is a distinct defect (tool-input loss on the pendingrunning tool-part lifecycle, not a permission.updated hang). The two may be worth fixing in the same pass since they share code, but neither's fix addresses the other's symptom.
  • Solution steering: Hint — either (a) auto-answer permission.updated via the SDK's permission-reply endpoint under an explicit, documented auto-approve policy for the embedded/headless runtime (mirroring OpenCode's own --auto semantics, since no human is present to approve a prompt in a workflow node), or (b) set an explicit permission: "allow" (or a narrower allow-list) override in buildEmbeddedServerConfig so the server never emits ask in the first place. Whichever is chosen, the event loop should still handle permission.updated/permission.replied defensively (e.g. fail fast with a named error) so a future permission category defaulting to ask cannot reintroduce the hang silently.

Additional notes

The generic per-node idle_timeout/watchdog does not save this case: the watchdog resets on assistant/tool/tool_result chunk types, but once the final assistant text has streamed and the server is blocked on a permission event that never yields any further MessageChunk, no further watchdog resets happen — the node relies entirely on session.idle to end the loop, so it is only ever bounded by the top-level workflow timeout, not the node's own idle_timeout.