You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The opencode-http adapter is the only adapter whose finished runs are opaque.
The claude and generic tmux adapters get a full replay for free: the pane scrollback is captured to <task>.log, so after a run you can read exactly what the agent said and did. The HTTP adapter has no pane to scrape — it drives the session over the API and observes GET /event — and <task>.log currently receives only the spawned opencode serve process's own stdout: startup INFO lines, "loading instance", request noise. None of the agent's actual work appears anywhere on disk.
The practical cost: when a run stalls, times out, or lands a wrong verdict, there's nothing to read back. Every other adapter supports "look at the log and see what happened"; this one does not, which makes exactly the failures that most need diagnosis the ones you can't diagnose.
@jackmcintyre proposed and implemented the fix in #279, and we're adopting that PR. The shape:
Split the sinks per task. <task>.log becomes a curated, role-coloured, human-readable transcript; the server's own stdout moves to a separate <task>.server.out so diagnostics stop mixing into the transcript.
Render the transcript off the existing SSE dispatch in adapters/opencode_http.py — the frames are already being read and filtered by sessionID, so this is a second consumer of a stream we already have. No new connection, no polling, no change to the deterministic control loop.
Keep the complete structured SSE trace in a per-task JSONL sink for post-hoc replay.
Resolve the speaker by messageID (from message.updated.info.{id,role}) rather than "last role seen", because message.updated frames arrive out of order and a re-emit mid-turn would otherwise mislabel the assistant's reply.
Revisions on adoption
We're pushing these directly to the contributor's branch (maintainer edits), so authorship stays with them:
Repoint the spawn-failure error. It still cites <task>.log, but the server's stdout now lands in <task>.server.out — as written, a failed spawn points at the file that no longer holds the diagnostics.
Best-effort guard around the new render/emit calls in _dispatch_sse. An unexpected frame shape must not be able to take out the SSE reader thread upstream of the idle / error queue puts — the control loop's liveness cannot depend on the logging path.
Rename the trace to <task>.sse.jsonl, knob-gate it with an off-switch, and exclude per-token message.part.delta records. The deltas dominate the file and re-carry text that message.part.updated already delivers complete, so they cost a lot of bytes for little replay value.
Render tool activity. Live probe (see PR feat(opencode-http): readable run logs + structured event trace #279) shows agent tool use surfaces as message.part.updated with part.type == "tool" (part.tool, state.status pending→running→completed, state.input/state.output) — notcommand.executed, which never fires for tool use. The current render reads part.text, absent on tool parts, so the transcript shows no tool activity at all.
Correct the permission frame names and fields. 1.18.2 has no permission.updated; it is permission.asked (permission/patterns/metadata/always/tool), and permission.replied carries reply, not response.
Let session-less frames through the dispatch filter.file.edited carries only {"file": ...} with no sessionID, so it is dropped before the render runs. Safe to allowlist because bmad-loop runs one server per session.
Extend the module-docstring API pin list with the frame types this newly consumes: message.part.updated (complete-once semantics), message.part.delta, tool-typed parts, file.edited, permission.asked, permission.replied. That docstring is where the adapter's opencode contract is pinned (/event frame semantics, epoch-ms times, basic-auth shape), so anything newly read off the stream has to be named there with live-probe evidence behind it.
The gap
The
opencode-httpadapter is the only adapter whose finished runs are opaque.The
claudeand generic tmux adapters get a full replay for free: the pane scrollback is captured to<task>.log, so after a run you can read exactly what the agent said and did. The HTTP adapter has no pane to scrape — it drives the session over the API and observesGET /event— and<task>.logcurrently receives only the spawnedopencode serveprocess's own stdout: startup INFO lines, "loading instance", request noise. None of the agent's actual work appears anywhere on disk.The practical cost: when a run stalls, times out, or lands a wrong verdict, there's nothing to read back. Every other adapter supports "look at the log and see what happened"; this one does not, which makes exactly the failures that most need diagnosis the ones you can't diagnose.
Adopted approach (PR #279)
@jackmcintyre proposed and implemented the fix in #279, and we're adopting that PR. The shape:
<task>.logbecomes a curated, role-coloured, human-readable transcript; the server's own stdout moves to a separate<task>.server.outso diagnostics stop mixing into the transcript.adapters/opencode_http.py— the frames are already being read and filtered bysessionID, so this is a second consumer of a stream we already have. No new connection, no polling, no change to the deterministic control loop.messageID(frommessage.updated.info.{id,role}) rather than "last role seen", becausemessage.updatedframes arrive out of order and a re-emit mid-turn would otherwise mislabel the assistant's reply.Revisions on adoption
We're pushing these directly to the contributor's branch (maintainer edits), so authorship stays with them:
<task>.log, but the server's stdout now lands in<task>.server.out— as written, a failed spawn points at the file that no longer holds the diagnostics._dispatch_sse. An unexpected frame shape must not be able to take out the SSE reader thread upstream of theidle/errorqueue puts — the control loop's liveness cannot depend on the logging path.<task>.sse.jsonl, knob-gate it with an off-switch, and exclude per-tokenmessage.part.deltarecords. The deltas dominate the file and re-carry text thatmessage.part.updatedalready delivers complete, so they cost a lot of bytes for little replay value.message.part.updatedwithpart.type == "tool"(part.tool,state.statuspending→running→completed,state.input/state.output) — notcommand.executed, which never fires for tool use. The current render readspart.text, absent on tool parts, so the transcript shows no tool activity at all.permission.updated; it ispermission.asked(permission/patterns/metadata/always/tool), andpermission.repliedcarriesreply, notresponse.file.editedcarries only{"file": ...}with nosessionID, so it is dropped before the render runs. Safe to allowlist because bmad-loop runs one server per session.message.part.updated(complete-once semantics),message.part.delta, tool-typed parts,file.edited,permission.asked,permission.replied. That docstring is where the adapter's opencode contract is pinned (/eventframe semantics, epoch-ms times, basic-auth shape), so anything newly read off the stream has to be named there with live-probe evidence behind it.