1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
# Backend handoff — CR-5: history windowing for the FE chat limit (courier doc)
> **From:** dispatch-web · **To:** arch-rewrite · **Courier:** the user.
> Companion to the living `backend-handoff.md` (§2 CR-5). 2026-06-12.
## Context — what the FE is building (no backend blocker)
The FE is adding a **chat limit**: in very long conversations the transcript unloads old
chunks from memory/DOM so the browser stays fast. Policy (already decided with the user):
- Limit `L` counts **chunks** (default 256, localStorage-configurable).
- When the loaded count exceeds `L`, the FE unloads the oldest `ceil(L/4)` chunks in ONE
bulk pass (e.g. `L=100`: at 101 chunks it unloads 25 → 76 remain). Bulk-on-threshold —
NOT one-per-delta like old Dispatch — to kill the scroll-jump-per-step failure mode.
- A fresh page load shows only the newest `floor(0.75 × L)` chunks (192 for the default).
- A "Show earlier messages" affordance pages older history back in (today: from the FE's
IndexedDB cache, which still holds it).
**This works TODAY with no backend change** — the FE fetches everything and windows in
memory. The ask below makes the *fresh-browser* case cheap: with an empty IndexedDB cache,
`GET /conversations/:id?sinceSeq=0` currently returns the ENTIRE conversation, so a
10k-chunk chat downloads + parses megabytes only for the FE to display 192 chunks.
## The ask (additive, `transport-contract` bump)
Extend `GET /conversations/:id` with two OPTIONAL query params:
1. **`limit=<n>`** — return only the **newest** `n` chunks of the selection (still
ascending seq order in the response). Selection semantics otherwise unchanged
(`seq > sinceSeq`).
- **If the selection has ≤ `n` chunks, return everything** — the FE will routinely send
a largish number (e.g. `limit=192`) against short conversations and expects the
normal full response (that flow must stay cheap and exact).
- `limit` absent → exactly today's behavior (full selection). Existing FE versions keep
working unchanged.
2. **`beforeSeq=<s>`** — restrict the selection to `seq < s` (combined with `limit`: the
newest `n` chunks below `s`, ascending). This is the "Show earlier messages" page-in
path for history the FE's local cache doesn't have (e.g. a fresh browser that
initial-loaded with `limit`). `beforeSeq` + `sinceSeq` together = `sinceSeq < seq < s`
(we only ever send one of them, but defined semantics beat undefined).
And one additive response field on `ConversationHistoryResponse`:
3. **`earliestSeq?: number`** (or `hasOlder: boolean` — your pick, flag your choice in the
reply) — the conversation's overall lowest seq (or whether chunks exist below the
returned window). The FE needs to know whether to OFFER "Show earlier messages" when
its local cache is exhausted. Without it the FE can only guess (seq 1 = start works if
seqs are guaranteed to start at 1 and be gap-free — if you'd rather just CONFIRM that
invariant in writing, the FE can derive `hasOlder` from `chunks[0].seq > 1` and we skip
the new field entirely; cheapest option, totally fine).
## How the FE will consume it
- Fresh load (empty cache): `GET /conversations/:id?sinceSeq=0&limit=<floor(0.75×L)>`.
- Incremental tail sync (cache warm): unchanged `?sinceSeq=<maxCachedSeq>` (no limit — the
tail since last sync is small by construction).
- Show-earlier beyond local cache: `GET /conversations/:id?beforeSeq=<oldestLoadedSeq>&limit=<ceil(L/4)>`.
- The FE's IndexedDB cache is seq-keyed + dedup-by-seq and already tolerates a
non-contiguous prefix (a windowed suffix), so no cache-format change is needed FE-side.
## Priority / sequencing
Not a blocker — the FE ships the limit feature against the current contract (full fetch +
in-memory windowing) and lights up the `limit`/`beforeSeq` params when you ship. Ship
whenever convenient; please bump `transport-contract` and note the params in the reply
handoff so the FE re-pins + re-mirrors.
|