Skip to content

docs(spec): session JSONL streaming over MoQ (design, pre-implementation) - #697

Draft
rsnodgrass wants to merge 2 commits into
mainfrom
ryan/session-streaming-spec
Draft

docs(spec): session JSONL streaming over MoQ (design, pre-implementation)#697
rsnodgrass wants to merge 2 commits into
mainfrom
ryan/session-streaming-spec

Conversation

@rsnodgrass

@rsnodgrass rsnodgrass commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands the design spec for streaming coding-session JSONL to the cloud live over MoQ, in parallel with the existing stop-time git upload. Spec only — no code ships here. It exists so the format, transport, and safety contract get reviewed before the two implementation PRs (2a transport, 2b daemon) build against it, and so teammates and the mono side can start the cloud contract in parallel.

The design was hardened by four adversarial principal-engineer review panels; their confirmed findings are baked into the requirements (they are the reason this is worth reviewing as a doc first).

Why a spec PR now

  • The load-bearing safety decisions — pause-window exclusion, honest at-least-once/ack semantics, the retroactive-redaction gap — are cheap to change on paper and expensive to change after internal/moq and sessionstream are written against them.
  • The cloud contract (coordinates endpoint, purge/re-redact API, narrow-scope archiver, server-side dedup) is a separate-repo (mono) dependency that can begin immediately off this doc.

What the design commits to

  • Tail the already-redacted raw.jsonl from a persisted cursor — no tee of the single-writer chokepoint; buffering is free and capture can't be blocked or corrupted by streaming.
  • Pause-aware publisher (CRITICAL): pause/resume is upload-time filtering today, so a naive tailer would stream exactly what the user paused to exclude. The publisher must skip suspended seq ranges at publish time.
  • Honest ack semantics: moq-lite has no object ack (per mono ADR-071); cursor advance = local send success, NOT durability. Git upload stays authoritative.
  • seq/eid stamping added to RawWriter — live entries carry none today; this is the stream's ordering/dedup key (session_id, seq).
  • moq-lite-04 over WebSocket/TLS, pure Go, coordinates minted cloud-side (secret sauce stays behind the API), fail-closed TLS, untrusted-input decoder.
  • Retroactive-redaction policy: dogfood-only until the mono purge API exists; customer enablement blocks on it.

See the ## Data flow Mermaid diagram and ## Implementation phases table in the doc for the shape and the PR split.

Test Plan

  • N/A — documentation only. docs/specs/session-streaming.md renders on GitHub (Mermaid diagram verified against the repo's strict-parse rules: quoted labels, no arrow substrings, no reserved IDs).
  • No code, no deps, no build/lint surface touched.

Checklist (expand if needed)
  • Tests added or N/A documented — N/A: spec doc, no code
  • CHANGELOG entry added (if user-facing) — N/A: internal design doc
  • Breaking change documented — N/A
  • Ran /security-review on the diff, or N/A documented — N/A: no code; the security content of the design was produced by an adversarial review panel and is captured in the doc

Summary by CodeRabbit

  • Documentation
    • Added a proposed design specification for streaming coding session updates over MoQ.
    • Documented session stream formats, lifecycle events, ordering, deduplication, pause behavior, cursor handling, and redaction safeguards.
    • Outlined transport, daemon, server integration, resilience, and phased implementation and testing requirements.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a proposed specification for best-effort redacted session JSONL streaming over MoQ, covering identity, envelopes, lifecycle and cursor semantics, daemon and transport responsibilities, server projection, cloud contracts, redaction policy, and phased testing.

Changes

Session streaming specification

Layer / File(s) Summary
Stream identity and envelope contract
docs/specs/session-streaming.md
Defines ses_/cnv_ identity binding, cursor storage, JSONL envelope kinds, sequence-based deduplication, and SessionEntry mapping.
Pause, finalization, and cursor safety
docs/specs/session-streaming.md
Specifies fail-closed pause lifecycle frames, send-success cursor advancement, session state transitions, provisional finalization, amendments, and tombstone enforcement.
Transport and daemon integration
docs/specs/session-streaming.md
Describes MoQ-over-WebSocket transport rules, coordinate retrieval, publisher lifecycle, tailing, backpressure, resilience, and active-recording rewrite safeguards.
Projection, cloud contracts, and rollout validation
docs/specs/session-streaming.md
Defines conversation-native server projection, cloud ingest and purge contracts, redaction gating, implementation phases, and validation coverage.

Estimated code review effort: 2 (Simple) | ~15 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the documentation-only design spec for session JSONL streaming over MoQ.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ryan/session-streaming-spec

Comment @coderabbitai help to get the list of available commands.

@rsnodgrass

Copy link
Copy Markdown
Contributor Author

Spec revised after a deep pass over the current conversation model (mono record-adr-important workspace: ADR-087 + the settled ADR-076/081 model). Load-bearing changes:

  • Envelope is now conversation-native (conversation_id = cnv_ twin of ses_ via prefix swap; kind: open|turn|lifecycle|close; turn vocabulary from ADR-076 TurnLine: role/author_kind/participant_kind/client_turn_id/redacted/tool_calls). The legacy turns.jsonl git layout is a server-side projection (ADR-081 principle), never the wire shape.
  • Pause is now an explicit lifecycle frame carrying the suspended seq range — silent skipping would read as missing_ranges gaps and trigger resend loops; fail-closed still holds (no tombstones for withheld content).
  • Finalization adopts ADR-087 D9: finalize_reason, provisional staleness-presumed finalize, stopped × draining two-axis late-drain, amendment-idempotent re-finalize, tombstone respect.
  • Ack section now cites ADR-087's confirmation the receipt plane is unbuilt; future receipt shape = {last_captured_seq, missing_ranges} self-healing pair from CaptureTurns.
  • Gap flagged for PR 2b: SessionEntry lacks tool_use_id; optional on the wire, adapters pass it through when the agent provides it.

Full mapping table (SessionEntryturn) is in the spec.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/specs/session-streaming.md`:
- Around line 86-94: Update the session-streaming specification around the frame
kind and seq definitions to assign unique, collision-free identities to open,
lifecycle, and close frames. Reserve seq for turn entries and define a separate
frame ID with allocation, retry, and deduplication semantics, or explicitly
document a non-colliding sequence namespace and cursor behavior; ensure frame
retries cannot be dropped due to collisions with turns.
- Around line 172-178: Define an amendment mechanism compatible with
`(conversation_id, seq)` first-writer-wins deduplication: specify whether
amendments receive new sequence numbers, use an explicit revision/version field,
or follow an authorized replacement path. Update the finalization and late-frame
semantics around “Finalization is provisional and amendable; the FSM never
reopens” so `client_turn_id` provides idempotency without reusing rejected
sequence numbers, and ensure retroactive redaction cannot retain stale content.
- Around line 127-140: Define an atomic publication fence for pause activation
in the session publisher: persist the effective pause sequence as part of the
pause transition, then re-check that fence immediately before sending each turn.
If the turn falls at or after the effective pause sequence, suppress it and
advance the read cursor; ensure the fence and send decision are synchronized so
no suspended-range entry can leave the process.
- Around line 64-66: Update the cursor design and load/validation flow described
in the session streaming specification to detect when raw.jsonl has been
rewritten after recording stops. Persist a file generation or content identity
alongside the last sequence and byte offset, compare it against the current
raw.jsonl before resuming, and reset the cursor to the beginning on mismatch so
draining cannot skip or resume mid-record.
- Around line 86-91: Update the session-streaming specification’s lifecycle and
close-frame rules to define abort-specific wire behavior: specify the close
frame’s distinct finalization reason, ensure the server treats it as discarded
rather than normally stopped, and state whether buffered frames are dropped
before closure. Keep explicit-stop behavior unchanged and apply the rule
wherever abort is described.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 20cfffb7-828a-46d2-80a6-4635ef2d9cab

📥 Commits

Reviewing files that changed from the base of the PR and between adc214a and a31eaf5.

📒 Files selected for processing (1)
  • docs/specs/session-streaming.md

Comment on lines +64 to +66
Cursor (`stream-cursor.json`, session cache dir, 0600, atomic temp+rename) records the last
fully-published entry seq + byte offset. Safe to lose: restart from 0 is correct under
at-least-once. Clamped to file size on load.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Invalidate byte-offset cursors when raw.jsonl is rewritten.

Post-finalize draining is supported, but rewriteRawJSONL is only prohibited during active recording. A rewrite after stop can change line lengths or ordering while leaving the persisted byte offset apparently valid, causing the streamer to skip data or resume mid-record. Add a file generation/content identity to the cursor and reset it on mismatch, or block rewrites until draining completes.

Also applies to: 215-215

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/session-streaming.md` around lines 64 - 66, Update the cursor
design and load/validation flow described in the session streaming specification
to detect when raw.jsonl has been rewritten after recording stops. Persist a
file generation or content identity alongside the last sequence and byte offset,
compare it against the current raw.jsonl before resuming, and reset the cursor
to the beginning on mismatch so draining cannot skip or resume mid-record.

Comment on lines +86 to +91
- `kind`: `open` | `turn` | `lifecycle` | `close`.
- `open` — first frame: `session_id` (`ses_` twin), `session_name`, `repo_id`, `team_id`,
agent/coworker identity, adapter name (from the raw.jsonl header / `StoreMeta`).
- `turn` — one redacted conversation entry (mapping below).
- `lifecycle` — `{event: pause|resume|stop|abort, reason, suspended_from_seq?, resumed_at_seq?}`.
- `close` — final frame: `{finalize_reason: "explicit-stop"}` (see Finalization).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Define the wire behavior for abort.

The FSM includes abort and says it discards the session, but close only carries finalize_reason: "explicit-stop". If an aborted session emits that close frame, the server can finalize and project it as a normal stopped session. Add an abort-specific finalization reason/close rule, including whether buffered frames must be dropped.

Also applies to: 163-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/session-streaming.md` around lines 86 - 91, Update the
session-streaming specification’s lifecycle and close-frame rules to define
abort-specific wire behavior: specify the close frame’s distinct finalization
reason, ensure the server treats it as discarded rather than normally stopped,
and state whether buffered frames are dropped before closure. Keep explicit-stop
behavior unchanged and apply the rule wherever abort is described.

Comment on lines +86 to +94
- `kind`: `open` | `turn` | `lifecycle` | `close`.
- `open` — first frame: `session_id` (`ses_` twin), `session_name`, `repo_id`, `team_id`,
agent/coworker identity, adapter name (from the raw.jsonl header / `StoreMeta`).
- `turn` — one redacted conversation entry (mapping below).
- `lifecycle` — `{event: pause|resume|stop|abort, reason, suspended_from_seq?, resumed_at_seq?}`.
- `close` — final frame: `{finalize_reason: "explicit-stop"}` (see Finalization).
- `seq` — client-allocated, 1-based, monotonically increasing, **never renumbered** (stamped
into `raw.jsonl` at write time; see below). Sole ordering/dedup authority; server dedup key
is `(conversation_id, seq)`, first-writer-wins — duplicates are expected (at-least-once).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define unique identities for non-turn frames.

seq is the deduplication key, but the specification only defines its allocation for raw session entries. open, lifecycle, and close frames currently have no non-colliding sequence or frame ID; reusing a turn sequence would cause first-writer-wins drops, while omitting it makes retry/deduplication undefined. Reserve seq for turns and add a separate frame ID, or explicitly define a collision-free sequence namespace and cursor behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/session-streaming.md` around lines 86 - 94, Update the
session-streaming specification around the frame kind and seq definitions to
assign unique, collision-free identities to open, lifecycle, and close frames.
Reserve seq for turn entries and define a separate frame ID with allocation,
retry, and deduplication semantics, or explicitly document a non-colliding
sequence namespace and cursor behavior; ensure frame retries cannot be dropped
due to collisions with turns.

Comment on lines +127 to +140
## Pause exclusion (publish-time, CRITICAL)

Pause/resume is upload-time filtering today (ADR-020): raw.jsonl keeps receiving entries
while paused. The session spine's rule is **fail-closed: paused means no content lands**
(off-the-record = `pause` + `reason:"off_the_record"`). The publisher therefore:

1. Consults `RecordingState.SuspendedAt` + lifecycle pause markers per entry.
2. **Never publishes** any `turn` whose seq lies in an open or closed suspended range — not
even a redacted tombstone (tombstones would still leak that something happened, and
fail-closed means nothing lands). The read cursor still advances.
3. Publishes `lifecycle` frames instead: `pause` with `suspended_from_seq`, `resume` with
`resumed_at_seq`. The server marks `[suspended_from_seq, resumed_at_seq)` as
**intentionally absent** — not missing — so gap accounting (`missing_ranges`) never
requests a resend of withheld content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Add an atomic pause publication fence.

Because raw.jsonl continues receiving entries while the publisher reads and sends them, a turn can be read before pause activation but sent after it. The current wording does not define a linearization point that guarantees fail-closed behavior. Persist an effective pause sequence atomically and re-check it immediately before send, or otherwise fence publication so no entry in the suspended range can leave the process.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/session-streaming.md` around lines 127 - 140, Define an atomic
publication fence for pause activation in the session publisher: persist the
effective pause sequence as part of the pause transition, then re-check that
fence immediately before sending each turn. If the turn falls at or after the
effective pause sequence, suppress it and advance the read cursor; ensure the
fence and send decision are synchronized so no suspended-range entry can leave
the process.

Comment on lines +172 to +178
- **Finalization is provisional and amendable; the FSM never reopens** (ADR-087 D9). A
daemon that restarts after a `staleness-presumed` finalize keeps draining from its cursor:
the client state is `stopped` (control) × `draining` (durability debt) — two axes, not a
reopened session. Late frames are an amendment trigger: the server re-finalizes
idempotently (keyed on the delivered seq set), superseding derived artifacts with lineage.
Amendment MUST respect deletion tombstones — a purged conversation is never resurrected,
and the publisher drops its backlog on a tombstone response.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make amendment semantics compatible with first-writer-wins deduplication.

A late amendment using an existing seq will be rejected by the stated (conversation_id, seq) first-writer-wins rule, so client_turn_id cannot provide the promised amendment idempotency. Define whether amendments use new sequence numbers, an explicit revision/version field, or an authorized replacement path; otherwise retroactive redaction and re-finalization can silently preserve stale content.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/session-streaming.md` around lines 172 - 178, Define an amendment
mechanism compatible with `(conversation_id, seq)` first-writer-wins
deduplication: specify whether amendments receive new sequence numbers, use an
explicit revision/version field, or follow an authorized replacement path.
Update the finalization and late-frame semantics around “Finalization is
provisional and amendable; the FSM never reopens” so `client_turn_id` provides
idempotency without reusing rejected sequence numbers, and ensure retroactive
redaction cannot retain stale content.

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