summaryrefslogtreecommitdiffhomepage
path: root/.dispatch
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-10-21 02:31:38 +0900
committerAdam Malczewski <[email protected]>2026-10-21 02:31:38 +0900
commit2686cf6bcd247d17528922e36aa380a521c964e0 (patch)
treed49a0a59e0025e6b3dad9dc40d691a7a4a728c04 /.dispatch
parentde421a9720bc00334c2cac2913067079a410f8b4 (diff)
downloaddispatch-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.md41
-rw-r--r--.dispatch/wire.reference.md31
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 ───────────────────────────────────────────────────────