feat(core): session protocol — ClientMessage/ServerMessage + snapshot-then-live resync (card df3fb2ab)#2
Conversation
…-then-live resync (card df3fb2ab)
The conversation layer over the wire envelopes: one self-describing
union per direction, nothing stringly-routed.
- ClientMessage = Subscribe{kinds, layers, last_seen:[KindRevision]}
| Command(CommandEnvelope) | Observe(ObserverSpec)
- ServerMessage = State(StateEnvelope) — deliberately the same frame
for snapshot and live; consumers reconcile by kind+revision, never
by phase bookkeeping.
- The resync contract (module doc, normative): every Subscribe —
first connect and reconnect alike — gets current snapshots per kind
then live-from-now. Never history replay. A reconnect can neither
flood nor gap; airc card bf0b5790's lesson promoted to the UI
substrate contract. This is the structural property behind
continuum #793/#794/#773.
- Revision stays per-KIND (mirrors ViewState::revision — one counter
per state; layer classifies update cadence, not state identity).
Worst case of the MAY-skip optimization under ephemeral churn: one
redundant snapshot per kind per reconnect.
- last_seen is #[serde(default)]: a bare subscribe from an older
build decodes as send-everything, never an error.
Tests pin the tag layout ({type:'subscribe'|'command'|'observe'} /
{type:'state'}), round-trips, and the bare-subscribe default. ts-rs
exports KindRevision/ClientMessage/ServerMessage to @positron/core.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adversarial review — BLOCK MERGE (spec holes in the normative contract; code itself is clean)Mechanical quality is excellent — every gate passed in a fresh worktree of
The block is on the normative module doc, which is the actual deliverable of this PR. Three holes: R1 (blocking): revision reset breaks the "can never gap" claimThe contract says the substrate "MAY skip a snapshot whose revision the client already holds," but never defines holds. If a substrate restarts and its revision counter resets (wire.rs explicitly contemplates resets), a client reconnecting with R2 (blocking): re-subscribe replace-vs-merge unspecified"Re-declare what this client renders... always idempotent" covers identical payloads. A second R3 (blocking):
|
…f3fb2ab)
R1 — skip rule is now EXACT EQUALITY, normatively: never >=. A
substrate restart may reset its revision counter; under >= a client
at last_seen:500 vs a restarted substrate at 3 keeps stale state
forever — a permanent gap. Exact equality makes resets safe by
construction; skip stays purely an optimization.
R2 — Subscribe is declarative: REPLACES the connection's subscription
set. Clients always send their whole world; no add/remove bookkeeping
to drift. Idempotency falls out as the identical-replacement case.
R3 — Observers resync identically: Observe { spec, last_seen } is
declarative per observer_id, triggers the same snapshot-then-live
with the same skip rule. One resync contract for humans and AIs —
the AI-perception constituency was the one the prior shape gapped.
Also from the review: per-kind revision ordering rule (substrate
emits non-decreasing; consumers drop lower-than-rendered — reordering
degrades harmless, not corrupting); the missing ack/error frame is
documented as a deliberate v0 omission with the additive path named;
observe tag layout + bare-observe default pinned by tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re-verify of f410ff4 (sentinel re-review after prior BLOCK): BLOCK — R1's stale-forever scenario is not fully closed; it survives one layer over, via the new consumer drop rule. What's verified good:
The blocking hole (R1 residual): the exact-equality skip correctly kills the
Fix is one sentence in the Ordering paragraph: scope the drop-rule watermark per connection/subscription — e.g. "the rendered-revision watermark backing this drop rule MUST be reset whenever the consumer sends a new Non-blocking note: "Exact equality makes counter resets safe by construction" slightly overclaims — old-epoch rev N vs new-epoch rev N with different content (counter resets, kind mutates N times before the client reconnects; cheap to hit at small N) still skips wrongly, leaving the client silently stale until that kind's next mutation. Transient and self-healing, and not fixable at this layer without an epoch/generation id — worth one line in "Deliberate v0 omissions" (epoch id as the v0.x answer), or advice that reset-prone substrates simply never exercise the MAY-skip. One-sentence respin away; everything else is approval-ready. |
…nd 2, card df3fb2ab) The round-2 sentinel caught R1's failure mode migrating into the ordering rule: a consumer that rendered chat@500, reconnecting to a counter-reset substrate, would drop the snapshot@3 it just requested (3 < 500) and everything after — stale-forever, one layer up. The watermark is now normatively SUBSCRIPTION-SCOPED: resets on every Subscribe/Observe; pre-reconnect knowledge travels only in last_seen. Watermark resets; last_seen remembers; the two never share a counter. Also documents the residual ABA case (old-epoch rev N vs new-epoch rev N skips one snapshot, transient, heals on next mutation) in the v0 omissions with the epoch-id path named — 'safe by construction' no longer overclaims. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Final-round verification of e29e34d — APPROVE Sole round-2 blocker (watermark recreating the stale-forever gap): CLOSED. Delta f410ff4..e29e34d is doc-only, +18 lines in ABA residual: documented as a v0 omission, accurate (coincidental counter re-match wrongly skips one snapshot; transient — next mutation streams down; epoch/generation id named as the full fix; persisted counters never enter the case). No contradictions among skip rule / replace semantics / observer contract / ordering rule / new scoping — skipped snapshot + reset watermark composes fine (subsequent live frames are ≥ current by monotonicity), and identical re-subscribes stay observably idempotent. Gauntlet (temp worktree at e29e34d): Non-blocking notes for v0.x:
|
The conversation layer over the wire envelopes: one self-describing
union per direction, nothing stringly-routed.
| Command(CommandEnvelope) | Observe(ObserverSpec)
for snapshot and live; consumers reconcile by kind+revision, never
by phase bookkeeping.
first connect and reconnect alike — gets current snapshots per kind
then live-from-now. Never history replay. A reconnect can neither
flood nor gap; airc card bf0b5790's lesson promoted to the UI
substrate contract. This is the structural property behind
continuum #793/#794/#773.
per state; layer classifies update cadence, not state identity).
Worst case of the MAY-skip optimization under ephemeral churn: one
redundant snapshot per kind per reconnect.
build decodes as send-everything, never an error.
Tests pin the tag layout ({type:'subscribe'|'command'|'observe'} /
{type:'state'}), round-trips, and the bare-subscribe default. ts-rs
exports KindRevision/ClientMessage/ServerMessage to @positron/core.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com