refactor(opencode): cut per-step turn prep
Issue for this PR
Issues are disabled on this fork. Consolidates #23, #25, #26, #27, #28 (closed in favor of this) — one commit per concern, individually revertable.
Type of change
- Bug fix
- Refactor / code improvement
What does this PR do?
Five commits attacking per-step turn prep, in order:
prep_ms+ttft_steponturn.done— the derived overhead (ttft − provider_ttft) silently absorbed tool wall-clock on tool-first turns (firstTokenAtonly set on text/reasoning, handle resets per step): prod sample showed a 192s "overhead" on a 217.9s turn.prep_ms(turnStart → first provider request, first-attempt stamped so retry backoff is excluded — cubic review, addressed) is correct by construction;ttft_stepflags cross-step pairs. Additive; no existing field changes.- Phase breakdown —
history_ms/tools_ms/context_ms/snapshot_ms, so each following commit has a fleet-wide before/after. - History once per turn —
runLoopreloaded and re-hydrated the whole session from SQLite every step (O(history × steps)). The runner lock makes the turn the single writer, so: load once, then fetch only rows with id > last-seen (newMessageV2.after, ids are monotonic) — which also preserves mid-turn queued user messages exactly as today — with a full reload whenever the tail contains compaction/summary rows. - Snapshot overlap — the pre-stream
snapshot.track()git chain forks and runs during provider TTFT; an idempotentawaitSnapshotjoins at the real constraint points (tool execute wrapper + step-start).trackis mutex-locked so the fiber serializes safely;orDie+forkIn(scope)keep failure/interruption semantics. - Instructions once per turn — the AGENTS.md findUp walk (and any URL instruction fetch with its 5s timeout) ran per step; now per turn. Deliberate: a turn's own instruction edits apply next turn, like skills/config.
How did you verify your code works?
Full packages/opencode suite over the stack: 3129 pass; only failures are the pre-existing Truncate date bug (fixed in #24) and a pty test that passes 9/9 in isolation (full-suite load flake). Purpose-built coverage: new turn.done fields test (scripted tool-first two-step turn: ttft_step === 2, prep_ms ≤ ttft_ms, phase fields ≥ 0); snapshot-tool-race.test.ts guards the barrier; multi-step/subtask/question/compaction suites guard history threading. bun run typecheck clean, bun run lint 0 errors.
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
🤖 Generated with Claude Code
Summary by cubic
Cuts per-step turn prep and makes turn.done timing accurate. Previously we reloaded history and instructions every step and misattributed prep/TTFT on tool-first turns; now we cache per turn, overlap the snapshot with provider TTFT, use fresh per-step snapshots, serialize the snapshot barrier so concurrent tools share one baseline, and emit honest, additive timing fields.
- Adds
prep_ms(turn start → first provider request),ttft_step(step index for TTFT), and per-phase timings:history_ms,tools_ms,context_ms,snapshot_ms. Stamps the first request once so retry backoff is excluded. - Loads session history once per turn and appends new rows via
MessageV2.after(sessionID, time, id)(index-aligned on(time_created, id)); falls back to full recompute if the tail contains compaction/summary rows. - Overlaps the pre-turn
snapshot.track()with the provider request; tools (including MCP) and step-start events block on anawaitSnapshotbarrier. Uses a fresh snapshot per step after the first join and serializesawaitSnapshotso concurrent tools share one baseline. - Resolves instruction files once per turn; instruction edits during a turn take effect next turn.
- Caches before reminders to avoid duplicating synthetic reminder parts across steps.
Review focus
packages/opencode/src/session/prompt.ts: per-turn history cache withafter(), pre-reminder cache, timing aggregation andturn.donefields, per-turn instruction resolution, TTFT step capture.packages/opencode/src/session/processor.ts: snapshot overlap and serializedawaitSnapshotbarrier (KeyedMutex); fresh per-step snapshots; first-request stamping to exclude retry backoff; exposessnapshotMs.packages/opencode/src/session/message-v2.ts: newafter(sessionID, time, id)query using(time_created, id)for index alignment.packages/opencode/src/session/tools.ts: built-in and MCP tools await the snapshot before execution.
Rollout notes
- No migrations. Event fields are additive; update dashboards to consume
prep_ms,ttft_step, and phase metrics. - Behavior change: per-turn instructions and a serialized snapshot barrier. Edits made during a turn apply next turn; tools wait for the snapshot before mutating files.