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
Claude runs as a persistent --input-format/--output-format stream-json process. handle() parses real events (assistant text, tool_use, tool_result, result), so we get: clean isolated messages, exact turn-end, per-turn usage records, and consequential-act attribution (PR/merge).
The result: for any non-Claude engine the console/timeline shows noisy, truncated output; spend is unmetered (#556); acts are unattributed (#594); and the handoff/last-line is scraped from prose (#621). This is not a per-engine bug — it's that mode and handle() are hardcoded to Claude's schema, so a structured-capable CLI (Codex exec --json, Gemini, etc.) has nowhere to plug its events in.
Proposed direction
Replace the claude/raw switch with a small per-engine adapter interface. Each engine declares:
how to build its structured-output launch argv (Claude: the stream-json flags in buildClaudeArgs; Codex: exec --json; …), and
how to map ONE event line → a normalized event: { assistantText | toolUse | toolResult | usage | turnEnd | error }.
The existing Claude code becomes the Claude adapter (behaviour-identical — pin it with the current headless.test.ts). Raw stdout stays as the generic fallback adapter for any CLI with no structured mode. HeadlessSession keeps its public surface (start/snapshot/runState/ready/input/interrupt/stop/alive, takeUsage/takeActs) — only the guts behind handle()/mode move.
Problem
The coding runner (
packages/browser-runner/src/coding/headless.ts) picks the engine transcript mode with a single binary switch:--input-format/--output-format stream-jsonprocess.handle()parses real events (assistanttext,tool_use,tool_result,result), so we get: clean isolated messages, exact turn-end, per-turn usage records, and consequential-act attribution (PR/merge).runOneShot()and its stdout is captured raw — the assistant's answer is interleaved with everyexec/curl/tool line, then clipped by the transcript-line cap, the 8000-char snapshot, and the 400-char timeline tail. Raw engines also return empty usage and empty acts by construction (takeUsage()/takeActs()— see A Codex coding session is completely invisible — unmetered like a tmux drive, but unlike a tmux drive the absence is never recorded, and classifyEngineMetering has zero production callers #556).The result: for any non-Claude engine the console/timeline shows noisy, truncated output; spend is unmetered (#556); acts are unattributed (#594); and the handoff/last-line is scraped from prose (#621). This is not a per-engine bug — it's that
modeandhandle()are hardcoded to Claude's schema, so a structured-capable CLI (Codexexec --json, Gemini, etc.) has nowhere to plug its events in.Proposed direction
Replace the claude/raw switch with a small per-engine adapter interface. Each engine declares:
buildClaudeArgs; Codex:exec --json; …), and{ assistantText | toolUse | toolResult | usage | turnEnd | error }.The existing Claude code becomes the Claude adapter (behaviour-identical — pin it with the current
headless.test.ts). Raw stdout stays as thegenericfallback adapter for any CLI with no structured mode.HeadlessSessionkeeps its public surface (start/snapshot/runState/ready/input/interrupt/stop/alive,takeUsage/takeActs) — only the guts behindhandle()/modemove.Why this is the right shape
Acceptance
EngineAdapter(or equivalent) abstraction with: Claude adapter (structured), generic adapter (raw stdout fallback).headless.test.tsstays green with the Claude adapter — byte-identical transcript for a Claude turn.Refs: #556, #674, #693, #621, #594