Skip to content

feat(serve): turn_started wire frame for client-side card reconciliation - #180

Merged
jkyberneees merged 3 commits into
mainfrom
feat/turn-started-frame
Sep 3, 2026
Merged

feat(serve): turn_started wire frame for client-side card reconciliation#180
jkyberneees merged 3 commits into
mainfrom
feat/turn-started-frame

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Implements the bodek task spec (2026-09-02): a dedicated, order-stable turn_started frame at the start of every turn, so clients no longer depend on catching the single session frame (stamped system_initiated) to open the streaming card. Field-observed failure (bodek v0.20.0 ↔ odek v1.40.0, 2026-09-02): a missed session frame left the whole turn streaming into the void.

Spec conformance

  • R1 — every turn: handlePrompt emits turn_started (turn_id / session_id / initiated / model) immediately after the session frame, before the first streamed frame. Wake turns carry initiated: "system", operator turns "operator" — computed server-side via the wakeInitiated type gate; client input cannot influence it. The session frame's legacy system_initiated stamp stays for old clients.
  • R2 — idempotency substrate: exactly one emission per turn; turn_id is t_ + 128-bit crypto/rand hex, unique per turn (pinned by tests).
  • R3 — turn attribution: thinking / token / tool_call / tool_result / done / error carry turn_id while the turn is active, via a per-connection wsTurnAnnotator shared by the agent's live callbacks and the processor loop. Lifecycle, sub-agent, approval, and *_delta frames are untouched.
  • R4 — versioning: all additions are additive map fields (absent = omitempty semantics). Old clients ignore the unknown frame type (UI switch behavior, verified); new client ↔ old server degrades to today's behavior. Compatibility matrix documented in docs/WEBUI.md.
  • R5 — tests (RED-first, commit 2b0eec3): frame order (session → turn_started → stream), id uniqueness across turns, forged-system_initiated rejection at the wire level and the unit seam (mirrors TestWakeInitiated_TypeGated), annotator exclusion set + lifecycle + -race hammer.

Interpretation accepted explicitly (spec R5 "idempotency case for duplicated turn_started"): the server emits exactly once per turn — a duplicated frame only arises from client-side replay (e.g. REST run-tail replay), and tolerance for duplicates is the client's upsert-by-turn_id, which is bodek-side (out of scope here). The server-side analog is pinned by the uniqueness + single-emission tests.

Surfaces covered

  • WS (handleWS): wake dispatch, operator dispatch, agent live callbacks — one annotated sender each.
  • REST runs (serve_runs.go): one wrapped recordSend backs both the agent callbacks and handlePrompt's send (a first-draft gap caught in review: live tool frames were recorded untagged).

Docs (same PR)

docs/WEBUI.md: frame-table row, tagged-frame paragraph with exclusion list, compatibility matrix, updated example sequence. docs/CONFIG.md: wake paragraph now notes wake turns announce via turn_started with initiated: "system".

Adversarial review (pre-release ritual)

2 diff-only reviewers (correctness/security + spec/docs), iterate-until-clean:

  • 1 P2 found (REST live-callback annotator bypass) → fixed in 9b62fab → dedicated verifier pass: PASS on all 5 points (no untagged leak, no new race, typed approval frames untouched, non-map fallback inert, no ordering change).
  • Clean: races, frame order, call-site coverage, cross-turn leaks, forged-initiated, backward compat.

Follow-up noted, non-blocking: a dedicated REST-run integration test asserting live tool frames carry turn_id (wiring is line-for-line analogous to the pinned WS path).

Test plan

  • go test -count=1 ./cmd/odek/ — full package green
  • go test -race on the new tests + wake-gate tests — green
  • go vet ./cmd/odek/ — clean
  • golangci-lint run ./cmd/odek/ — 0 issues

Implements the bodek task spec (2026-09-02): a dedicated, order-stable
frame at turn start so clients never depend on catching the single
session frame to open the streaming card.

- R1: handlePrompt emits turn_started (turn_id/session_id/initiated/model)
  immediately after the session frame, before the first streamed frame,
  for wake and operator turns alike. initiated is computed server-side
  via the wakeInitiated type gate; the session frame's legacy
  system_initiated stamp is kept for old clients.
- R2: one emission per turn; ids are t_+128-bit random hex, unique per turn.
- R3: streamed frames (thinking/token/tool_call/tool_result/done/error)
  carry turn_id while the turn is active, via a per-connection
  wsTurnAnnotator shared by the agent's live callbacks and the processor
  loop; lifecycle/sub-agent/delta frames are untouched.
- R5: tests pin frame order, id uniqueness, forged-initiated rejection
  (wire level + unit seam), and annotator exclusions/lifecycle (-race).
- Docs: WEBUI.md protocol table + compatibility matrix + example sequence;
  CONFIG.md wake-frame paragraph.

RED commit 2b0eec3 (tests failing: turn_started frames = 0).
Adversarial review finding: the run's newServeAgent sendFn recorded
tool_call/tool_result/iteration frames via run.record directly, bypassing
the turn annotator — the recorded tail mixed tagged (bulk/done) and
untagged (live tool) frames. One wrapped recordSend closure now backs
both the agent callbacks and handlePrompt's send.
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 9b62fab Commit Preview URL

Branch Preview URL
Sep 03 2026, 05:09 PM

@jkyberneees
jkyberneees merged commit 29044e1 into main Sep 3, 2026
10 checks passed
@jkyberneees
jkyberneees deleted the feat/turn-started-frame branch September 3, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant