refactor(opencode): cut per-step turn prep

Merged
PI
2mo ago
+178-14

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:

  1. prep_ms + ttft_step on turn.done — the derived overhead (ttft − provider_ttft) silently absorbed tool wall-clock on tool-first turns (firstTokenAt only 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_step flags cross-step pairs. Additive; no existing field changes.
  2. Phase breakdown — history_ms / tools_ms / context_ms / snapshot_ms, so each following commit has a fleet-wide before/after.
  3. History once per turn — runLoop reloaded 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 (new MessageV2.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.
  4. Snapshot overlap — the pre-stream snapshot.track() git chain forks and runs during provider TTFT; an idempotent awaitSnapshot joins at the real constraint points (tool execute wrapper + step-start). track is mutex-locked so the fiber serializes safely; orDie + forkIn(scope) keep failure/interruption semantics.
  5. 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 an awaitSnapshot barrier. Uses a fresh snapshot per step after the first join and serializes awaitSnapshot so 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 with after(), pre-reminder cache, timing aggregation and turn.done fields, per-turn instruction resolution, TTFT step capture.
  • packages/opencode/src/session/processor.ts: snapshot overlap and serialized awaitSnapshot barrier (KeyedMutex); fresh per-step snapshots; first-request stamping to exclude retry backoff; exposes snapshotMs.
  • packages/opencode/src/session/message-v2.ts: new after(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.