diff options
| author | Adam Malczewski <[email protected]> | 2026-06-12 19:00:29 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-12 19:00:29 +0900 |
| commit | d66585333ee5764700c67a81eaec015b0026f8f1 (patch) | |
| tree | 6e1ac455c2ecbf3c442fce9f73fdaed8fb71fade /.dispatch | |
| parent | 1764e3e5dff836255d121a933dd92542368346f9 (diff) | |
| download | dispatch-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.md | 71 | ||||
| -rw-r--r-- | .dispatch/wire.reference.md | 26 |
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 |
