feat(serve): turn_started wire frame for client-side card reconciliation - #180
Merged
Conversation
…potency, forged-initiated rejection)
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.
Deploying with
|
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the bodek task spec (2026-09-02): a dedicated, order-stable
turn_startedframe at the start of every turn, so clients no longer depend on catching the singlesessionframe (stampedsystem_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
handlePromptemitsturn_started(turn_id/session_id/initiated/model) immediately after thesessionframe, before the first streamed frame. Wake turns carryinitiated: "system", operator turns"operator"— computed server-side via thewakeInitiatedtype gate; client input cannot influence it. The session frame's legacysystem_initiatedstamp stays for old clients.turn_idist_+ 128-bit crypto/rand hex, unique per turn (pinned by tests).thinking/token/tool_call/tool_result/done/errorcarryturn_idwhile the turn is active, via a per-connectionwsTurnAnnotatorshared by the agent's live callbacks and the processor loop. Lifecycle, sub-agent, approval, and*_deltaframes are untouched.omitemptysemantics). Old clients ignore the unknown frame type (UI switch behavior, verified); new client ↔ old server degrades to today's behavior. Compatibility matrix documented indocs/WEBUI.md.turn_started→ stream), id uniqueness across turns, forged-system_initiatedrejection at the wire level and the unit seam (mirrorsTestWakeInitiated_TypeGated), annotator exclusion set + lifecycle +-racehammer.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
handleWS): wake dispatch, operator dispatch, agent live callbacks — one annotated sender each.serve_runs.go): one wrappedrecordSendbacks both the agent callbacks andhandlePrompt'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 viaturn_startedwithinitiated: "system".Adversarial review (pre-release ritual)
2 diff-only reviewers (correctness/security + spec/docs), iterate-until-clean:
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 greengo test -raceon the new tests + wake-gate tests — greengo vet ./cmd/odek/— cleangolangci-lint run ./cmd/odek/— 0 issues