summaryrefslogtreecommitdiffhomepage
path: root/.dispatch
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 19:00:29 +0900
committerAdam Malczewski <[email protected]>2026-06-12 19:00:29 +0900
commitd66585333ee5764700c67a81eaec015b0026f8f1 (patch)
tree6e1ac455c2ecbf3c442fce9f73fdaed8fb71fade /.dispatch
parent1764e3e5dff836255d121a933dd92542368346f9 (diff)
downloaddispatch-web-d66585333ee5764700c67a81eaec015b0026f8f1.tar.gz
dispatch-web-d66585333ee5764700c67a81eaec015b0026f8f1.zip
feat(chat): consume CR-5 history windowing — server-windowed cold loads + show-earlier backfill
Re-pinned [email protected]>0.10.0 + [email protected]>0.6.1 (reply frontend-history-windowing-handoff.md); re-mirrored both .dispatch references. - HistorySync port gains optional { limit?, beforeSeq? } (CR-5 params); the app's createHistorySync appends them to GET /conversations/:id. - COLD-cache fresh load now fetches ?sinceSeq=0&limit=<floor(0.75xL)> — a huge conversation no longer ships whole to show 192 chunks. A warm-cache tail sync stays unwindowed (windowing a tail that outgrew the limit would leave a silent seq gap behind the cache). - hasEarlier now derives from the [email protected] CONTRACT (1-based gap-free seqs): loaded window starting above seq 1 => older history exists — covering both locally-trimmed AND server-windowed transcripts (the watermark stays as the merge floor only). - showEarlier(): local cache first; when the cache doesn't reach far enough back, backfills the missing older run via ?beforeSeq=<oldestKnown>&limit= and persists it (next page-in is local). latestSeq windowed-read caveat is satisfied structurally (tail cursor derives from the cache's max seq). - live-probe: +6 CR-5 checks (seq origin, newest-k ascending, short-chat exactness, beforeSeq paging, 400 validation x2). NOT yet run live — backend was down at commit time; run pending. - backend-handoff.md: CR-5 RESOLVED, pins/mirrors current. 602 tests green x2.
Diffstat (limited to '.dispatch')
-rw-r--r--.dispatch/transport-contract.reference.md71
-rw-r--r--.dispatch/wire.reference.md26
2 files changed, 75 insertions, 22 deletions
diff --git a/.dispatch/transport-contract.reference.md b/.dispatch/transport-contract.reference.md
index e6ab799..774cfb0 100644
--- a/.dispatch/transport-contract.reference.md
+++ b/.dispatch/transport-contract.reference.md
@@ -5,10 +5,23 @@
> hangs on a permission prompt). Your CODE still imports `@dispatch/transport-contract` normally —
> this file is for READING only.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]` (CR-4 cache-warming lifecycle shipped).
-> Depends on `@dispatch/[email protected]` (see `wire.reference.md`) + `@dispatch/[email protected]` (see
+> **Orchestrator:** SNAPSHOT of `[email protected]` (CR-5 history windowing shipped).
+> Depends on `@dispatch/[email protected]` (see `wire.reference.md`) + `@dispatch/[email protected]` (see
> `ui-contract.reference.md`).
>
+> **2026-06-12 delta (CR-5 history windowing — package bumped `0.9.0` → `0.10.0`):** NO type-shape
+> change — `GET /conversations/:id` gains two OPTIONAL query params alongside `sinceSeq`:
+> **`limit=<k>`** (the NEWEST `k` chunks of the selection, still ASCENDING; a selection with ≤ `k`
+> chunks is returned whole; omitted = full selection, byte-identical to the old behavior) and
+> **`beforeSeq=<s>`** (exclusive upper bound `seq < s`; combined: `sinceSeq < seq < beforeSeq`).
+> `limit`/`beforeSeq` must be POSITIVE integers (`sinceSeq` may still be 0); malformed/zero/negative
+> → HTTP 400 `{ error }` naming the param. Seq numbering is now a WRITTEN CONTRACT: 1-based,
+> monotonic, gap-free (see `[email protected]` `StoredChunk`), so `hasOlder = oldestLoaded.seq > 1` — there
+> is deliberately NO `earliestSeq`/`hasOlder` field. CAVEAT: on a windowed read, `latestSeq`
+> describes the returned WINDOW; never regress a tail cursor from a `beforeSeq` backfill page.
+> Intended flows: fresh load `?sinceSeq=0&limit=<k>` · tail sync `?sinceSeq=<cursor>` (no limit) ·
+> page older in `?beforeSeq=<oldestLoadedSeq>&limit=<k>`.
+>
> **2026-06-12 delta (CR-4 cache-warming lifecycle — package bumped `0.8.0` → `0.9.0`):** adds
> `POST /conversations/:id/close` (`CloseConversationResponse`) — the EXPLICIT "user closed this
> conversation's tab" affordance, distinct from a socket disconnect / `chat.unsubscribe` (which
@@ -92,9 +105,11 @@
- `POST /chat` — body `ChatRequest` (JSON); response NDJSON stream, one `AgentEvent` per line;
resolved id also in `X-Conversation-Id` header.
- `GET /models` — `ModelsResponse`.
-- `GET /conversations/:id?sinceSeq=<n>` — `ConversationHistoryResponse`: RAW, append-order,
- seq-ordered slice with `seq > n` (NOT reconciled — dangling tool-calls returned as-is).
- `latestSeq` = last chunk's `seq`, or the requested `sinceSeq` when caught up (empty `chunks`).
+- `GET /conversations/:id?sinceSeq=<n>&beforeSeq=<s>&limit=<k>` — `ConversationHistoryResponse`:
+ RAW, append-order, seq-ordered slice with `n < seq < s`, windowed to the NEWEST `k` (all params
+ optional; NOT reconciled — dangling tool-calls returned as-is). `latestSeq` = last chunk's `seq`,
+ or the requested `sinceSeq` when caught up (empty `chunks`) — a TAIL cursor only; do not regress
+ a cursor from a windowed/backfill read. `limit`/`beforeSeq` must be positive ints → else 400.
- `GET /conversations/:id/metrics` — `ConversationMetricsResponse`: every SEALED turn's `TurnMetrics`
in turn order (per-turn token + timing; NOT seq-filtered). IMPLEMENTED + LIVE-VERIFIED (probe 17/17).
- `POST /chat/warm` — body `WarmRequest` (JSON) → `200 WarmResponse` (cache-warm usage incl.
@@ -182,23 +197,49 @@ export interface ModelsResponse {
}
/**
- * Response body for `GET /conversations/:id?sinceSeq=<n>` — the incremental
- * read-side history endpoint a long-lived client uses to (re)hydrate a
- * conversation cheaply.
+ * Response body for
+ * `GET /conversations/:id?sinceSeq=<n>&beforeSeq=<s>&limit=<k>` — the
+ * incremental read-side history endpoint a long-lived client uses to
+ * (re)hydrate a conversation cheaply. All three query params are OPTIONAL and
+ * combine as one SELECTION + one WINDOW:
+ *
+ * - **Selection** — `sinceSeq` (exclusive lower bound, `seq > n`; omitted/0 =
+ * from the start) and `beforeSeq` (exclusive upper bound, `seq < s`; omitted
+ * = to the end). Together: `n < seq < s`.
+ * - **Window** — `limit=<k>` returns only the NEWEST `k` chunks of the
+ * selection (the response stays ASCENDING by seq). A selection with ≤ `k`
+ * chunks is returned whole. `limit` omitted = the full selection — exactly
+ * the pre-windowing behavior, so existing clients are unchanged.
+ * - `limit` and `beforeSeq` must be POSITIVE integers (`sinceSeq` may be 0);
+ * malformed, zero, or negative values → HTTP 400 `{ error }`.
+ *
+ * Intended client flows: fresh load = `?sinceSeq=0&limit=<k>` (newest window);
+ * 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.
*
* `chunks` is the RAW, append-order, seq-ordered slice of the conversation log
- * with `seq > sinceSeq` (or the whole log when `sinceSeq` is omitted/0). 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).
+ * 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).
*
* `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`
* (0 for a full read of an empty conversation). So after applying the response a
* client's new cursor is always `latestSeq`, and an empty `chunks` means
- * "nothing new past your cursor".
+ * "nothing new past your cursor". CAVEAT (windowed reads): `latestSeq` is a
+ * TAIL-sync cursor — on a `beforeSeq` backfill page (or any `limit`ed read that
+ * did not reach the log's true tail) it describes the returned window, NOT the
+ * conversation's high-water mark, so a client must not regress its sync cursor
+ * from a backfill response. (A true server-side high-water mark independent of
+ * the filter is deferred until a consumer needs it — it would require widening
+ * the store contract.)
*/
export interface ConversationHistoryResponse {
readonly chunks: readonly StoredChunk[];
diff --git a/.dispatch/wire.reference.md b/.dispatch/wire.reference.md
index 40f94cf..1d761bf 100644
--- a/.dispatch/wire.reference.md
+++ b/.dispatch/wire.reference.md
@@ -4,8 +4,14 @@
> 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.
>
-> **Orchestrator:** SNAPSHOT of `[email protected]` (the metrics types below + the `user-message` turn event
-> shipped + version-bumped). Regenerate whenever `@dispatch/wire` changes.
+> **Orchestrator:** SNAPSHOT of `[email protected]` (doc-only bump: the 1-based gap-free seq guarantee
+> codified on `StoredChunk`). Regenerate whenever `@dispatch/wire` changes.
+>
+> **2026-06-12 delta (CR-5 history windowing — package bumped `0.6.0` → `0.6.1`, DOC-ONLY):** the
+> per-conversation `seq` numbering is now a WRITTEN CONTRACTUAL GUARANTEE on `StoredChunk`:
+> **1-based, monotonic, gap-free** — a conversation's first chunk is always `seq === 1` and
+> numbering never skips. A client holding only a windowed suffix of the log derives "older chunks
+> exist server-side" purely from `oldestLoaded.seq > 1` (no `earliestSeq`/`hasOlder` field exists).
>
> **2026-06-12 delta (CR-3 user-message handoff — package bumped `0.5.0` → `0.6.0`, ADDITIVE):** adds a
> new `AgentEvent` union member `TurnInputEvent` (`{ type: "user-message"; conversationId; turnId; text }`)
@@ -168,11 +174,17 @@ export interface ChatMessage {
/**
* A persisted chunk plus its sync metadata. The append-only conversation log
- * stamps every chunk with a monotonic, gap-free, 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. `chunk` is the content unit — `Chunk` carries no storage/sync cursor
+ * stamps every chunk with a **1-based**, monotonic, gap-free, 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` 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