diff options
| author | Adam Malczewski <[email protected]> | 2026-10-21 02:31:38 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-10-21 02:31:38 +0900 |
| commit | 2686cf6bcd247d17528922e36aa380a521c964e0 (patch) | |
| tree | d49a0a59e0025e6b3dad9dc40d691a7a4a728c04 /.dispatch | |
| parent | de421a9720bc00334c2cac2913067079a410f8b4 (diff) | |
| download | dispatch-web-2686cf6bcd247d17528922e36aa380a521c964e0.tar.gz dispatch-web-2686cf6bcd247d17528922e36aa380a521c964e0.zip | |
fix(review-3.6): invalidate history caches by epoch and coalesce syncs
Diffstat (limited to '.dispatch')
| -rw-r--r-- | .dispatch/transport-contract.reference.md | 41 | ||||
| -rw-r--r-- | .dispatch/wire.reference.md | 31 |
2 files changed, 49 insertions, 23 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md index 95f968e..e798fa7 100644 --- a/.dispatch/transport-contract.reference.md +++ b/.dispatch/transport-contract.reference.md @@ -1,5 +1,7 @@ # `@dispatch/transport-contract` — in-repo reference (read THIS, not node_modules) +> **2026-10-20 review 3.5:** history replacements preserve monotonic seqs (retained history need not start at 1). HTTP history and WS compaction carry required `historyEpoch`; unknown cached epochs must be reloaded. Consumed by review 3.6. +> > MIRRORS the backend's `@dispatch/transport-contract` package source so headless FE agents can read > the transport types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a > permission prompt). Your CODE still imports `@dispatch/transport-contract` normally — this file is for @@ -318,17 +320,24 @@ export interface ModelMetadata { * tail sync = `?sinceSeq=<cursor>` (no limit); page older history in = * `?beforeSeq=<oldestLoadedSeq>&limit=<k>`. * - * Seq numbering is **1-based and gap-free** (a CONTRACTUAL GUARANTEE — see - * `StoredChunk` in `@dispatch/wire`): a client can derive "older chunks exist" - * purely from `oldestLoaded.seq > 1`; there is deliberately no - * `earliestSeq`/`hasOlder` response field. + * Seq is positive and never reused within a conversation, even when history is + * replaced. The retained log need not start at 1, so `oldestLoaded.seq > 1` + * does not prove older chunks exist. An empty/short backfill page can establish + * that no earlier chunks remain. + * + * `historyEpoch` is the persisted replacement count (missing storage value = 0). + * Every successful replacement, including an empty one, increments it once. + * On a different epoch, clients invalidate their conversation cache and refetch + * with `sinceSeq=0` (optionally windowed), even if this response has no chunks. * * `chunks` is the RAW, append-order, seq-ordered slice of the conversation log * selected + windowed as above. It is NOT reconciled: a dangling tool-call is * returned as-is (rendered as an interrupted call). Reconciliation is a * turn-path concern — the server repairs history only when it feeds a provider, * never on this read path — which is what preserves the per-chunk `seq` cursor - * invariant (a synthesized repair chunk would have no seq). + * invariant (a synthesized repair chunk would have no seq). Provider-opaque + * thinking replay fields (`signature` and `redactedData`) are omitted from + * this client-facing response; chunk presence, text, order, and seq are intact. * * `latestSeq` is the `seq` of the LAST chunk in this response, or — when the * slice is empty (the client is already caught up) — the requested `sinceSeq` @@ -343,8 +352,9 @@ export interface ModelMetadata { * the store contract.) */ export interface ConversationHistoryResponse { - readonly chunks: readonly StoredChunk[]; - readonly latestSeq: number; + readonly chunks: readonly StoredChunk[]; + readonly latestSeq: number; + readonly historyEpoch: number; } /** @@ -945,15 +955,18 @@ export interface ConversationStatusChangedMessage { /** * Broadcast to all connected WS clients when a conversation's history has been - * compacted (summarized). The frontend should reload the conversation history - * via `GET /conversations/:id` to reflect the compacted state. + * compacted (summarized). `historyEpoch` is the persisted replacement count: + * missing storage value = 0; every successful replacement increments it once, + * including empty replacements. On a different epoch, clients invalidate their + * conversation cache and refetch `GET /conversations/:id?sinceSeq=0`. */ export interface ConversationCompactedMessage { - readonly type: "conversation.compacted"; - readonly conversationId: string; - readonly newConversationId: string; - readonly messagesSummarized: number; - readonly messagesKept: number; + readonly type: "conversation.compacted"; + readonly conversationId: string; + readonly newConversationId: string; + readonly messagesSummarized: number; + readonly messagesKept: number; + readonly historyEpoch: number; } /** diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md index 6e8daff..3743bd5 100644 --- a/.dispatch/wire.reference.md +++ b/.dispatch/wire.reference.md @@ -1,5 +1,7 @@ # `@dispatch/wire` — in-repo reference (read THIS, not node_modules) +> **2026-10-20 review 3.5:** history replacements preserve monotonic seqs (retained history need not start at 1). HTTP history and WS compaction carry required `historyEpoch`; unknown cached epochs must be reloaded. Consumed by review 3.6. +> > MIRRORS the backend's `@dispatch/wire` package source so headless FE agents can read the wire > types WITHOUT following the `file:` dep symlink out of this repo (which hangs on a permission > prompt). Your CODE still imports `@dispatch/wire` normally — this file is for READING only. @@ -258,26 +260,37 @@ export interface ChatMessage { /** * A persisted chunk plus its sync metadata. The append-only conversation log - * stamps every chunk with a **1-based**, monotonic, gap-free, per-conversation + * stamps every chunk with a positive, monotonic, per-conversation * `seq` (the sync cursor, assigned in append order) and records the `role` of * the message it belongs to. This makes a flat seq-ordered stream both * incrementally syncable ("give me chunks after seq N") and regroupable into * messages by the client. * - * The 1-based start is a CONTRACTUAL GUARANTEE (not an implementation detail): - * a conversation's first chunk is always `seq === 1` and numbering never skips, - * so a client holding only a windowed suffix of the log can derive "older - * chunks exist server-side" purely from `oldestLoaded.seq > 1` — no separate - * has-older flag is needed (or provided). `chunk` is the content unit — `Chunk` carries no storage/sync cursor + * Seq is never reused within a conversation, including after compaction replaces + * its history. The retained log need not start at 1 or be gap-free; a client + * cannot infer that older chunks exist solely from `oldestLoaded.seq > 1`. + * `chunk` is the content unit — `Chunk` carries no storage/sync cursor * (`seq` lives here on the envelope, not on the chunk, since it is assigned by * the store and the provider has no use for it). A chunk MAY still carry * generation provenance assigned at production time (e.g. a tool chunk's * `stepId`), which is intrinsic to the content and so travels with it. */ export interface StoredChunk { - readonly seq: number; - readonly role: Role; - readonly chunk: Chunk; + readonly seq: number; + readonly role: Role; + readonly chunk: Chunk; + /** + * The turn that produced this chunk — the same `turnId` carried on every + * live `AgentEvent` of that turn (the turn's opening user message, any + * mid-turn steering message, and every assistant/tool chunk the turn + * generated). The join key between transcript content and the turn's + * `TurnMetrics`, so a client places per-turn metrics EXACTLY (no positional + * guessing) — including while a turn is in flight and when older history has + * been unloaded. Optional: absent on rows persisted before this field + * existed and on chunks written outside any turn (e.g. a compaction summary). + * A consumer must tolerate its absence. + */ + readonly turnId?: string; } // ─── Reasoning effort ─────────────────────────────────────────────────────── |
