fix(providers/opencode): workflow node hangs forever when OpenCode raises permission.updated
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.updatedevent (with no accompanyingsession.idleuntil 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.updateddirectly (the existingprovider.test.tsonly scriptssession.idleand never exercises the permission-pending path).
Evidence
Reproduction
- Run any workflow with
provider: opencodeand agithub-copilot/*oropenai/*model against this repo checkout, e.g.: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." archon workflow run my-opencode-smoke --detach "run smoke test"- Watch the transcript (
~/.archon/workspaces/<project>/logs/<runId>.jsonl): theassistantevent with the correct answer ("4") appears, but no furthernode_start/completion event ever follows.archon workflow get <runId> --jsonkeeps 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):
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:
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:
- https://github.com/coleam00/Archon/blob/dev/packages/providers/src/community/opencode/runtime.ts#L12-L18
(
buildEmbeddedServerConfig— only setsserver: {hostname, port, password}) agent-config.ts'sbuildToolsPermissionsMap— only builds the legacytools: {name: boolean}enable/disable map, never the separatepermission: {ask/allow/deny}policy the SDK'sPermissiontype supports.
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, commitc15da516(dev) - Platform or adapter: CLI,
provider: opencode - Database: SQLite
- OS: Linux x64
- OpenCode:
1.18.30CLI /@opencode-ai/[email protected]
Logs or screenshots
Transcript excerpt for one affected run (4919bba99779207f0dbafa518c63f971):
{"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.tsbut is a distinct defect (tool-input loss on thepending→runningtool-part lifecycle, not apermission.updatedhang). 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.updatedvia the SDK's permission-reply endpoint under an explicit, documented auto-approve policy for the embedded/headless runtime (mirroring OpenCode's own--autosemantics, since no human is present to approve a prompt in a workflow node), or (b) set an explicitpermission: "allow"(or a narrower allow-list) override inbuildEmbeddedServerConfigso the server never emitsaskin the first place. Whichever is chosen, the event loop should still handlepermission.updated/permission.replieddefensively (e.g. fail fast with a named error) so a future permission category defaulting toaskcannot 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.
Source: coleam00/Archon