summaryrefslogtreecommitdiffhomepage
path: root/scripts
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 /scripts
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 'scripts')
-rw-r--r--scripts/live-probe.ts57
1 files changed, 55 insertions, 2 deletions
diff --git a/scripts/live-probe.ts b/scripts/live-probe.ts
index 7099b44..f44a136 100644
--- a/scripts/live-probe.ts
+++ b/scripts/live-probe.ts
@@ -75,13 +75,27 @@ function fail(msg: string): never {
process.exit(1);
}
-async function historySync(id: string, sinceSeq: number): Promise<ConversationHistoryResponse> {
- const url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}?sinceSeq=${sinceSeq}`;
+async function historySync(
+ id: string,
+ sinceSeq: number,
+ window?: { limit?: number; beforeSeq?: number },
+): Promise<ConversationHistoryResponse> {
+ let url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}?sinceSeq=${sinceSeq}`;
+ if (window?.limit !== undefined) url += `&limit=${window.limit}`;
+ if (window?.beforeSeq !== undefined) url += `&beforeSeq=${window.beforeSeq}`;
const res = await fetch(url, { headers: { Origin: "http://localhost:24204" } });
if (!res.ok) fail(`history fetch ${res.status} for ${url}`);
return (await res.json()) as ConversationHistoryResponse;
}
+/** Raw history GET that returns the status (for the CR-5 validation checks). */
+async function historyStatus(id: string, query: string): Promise<number> {
+ const url = `${HTTP_BASE}/conversations/${encodeURIComponent(id)}?${query}`;
+ const res = await fetch(url, { headers: { Origin: "http://localhost:24204" } });
+ await res.arrayBuffer(); // drain
+ return res.status;
+}
+
/** Durable metrics fetch — returns the response, or the HTTP status when not OK
* (the endpoint is being implemented backend-side; the FE tolerates a 404). */
async function metricsSync(id: string): Promise<ConversationMetricsResponse | { status: number }> {
@@ -203,6 +217,45 @@ async function main() {
.join("");
record("turn 1 committed transcript has assistant text", committedText.length > 0);
+ // ─── CR-5: history windowing (?limit= / ?beforeSeq=, [email protected]) ───────
+ const logLen = hist.chunks.length;
+ record(
+ "CR-5 seq origin: first chunk is seq 1 (1-based gap-free contract)",
+ hist.chunks[0]?.seq === 1,
+ `first seq=${hist.chunks[0]?.seq}`,
+ );
+ const win = await historySync(textConv, 0, { limit: 2 });
+ record(
+ "CR-5 ?limit=2 returns the NEWEST 2, ascending, latestSeq = window tail",
+ win.chunks.length === Math.min(2, logLen) &&
+ win.chunks[0]?.seq === Math.max(1, logLen - 1) &&
+ win.chunks[win.chunks.length - 1]?.seq === logLen &&
+ win.latestSeq === logLen,
+ `seqs=[${win.chunks.map((c) => c.seq).join(",")}] latestSeq=${win.latestSeq}`,
+ );
+ const whole = await historySync(textConv, 0, { limit: 200 });
+ record(
+ "CR-5 ?limit= larger than the log returns everything (short-chat flow exact)",
+ whole.chunks.length === logLen,
+ `${whole.chunks.length}/${logLen} chunks`,
+ );
+ const oldestLoaded = win.chunks[0]?.seq ?? 0;
+ if (oldestLoaded > 1) {
+ const back = await historySync(textConv, 0, { beforeSeq: oldestLoaded, limit: 50 });
+ record(
+ "CR-5 ?beforeSeq= pages the older run (seq < bound, ascending from 1)",
+ back.chunks.length === oldestLoaded - 1 &&
+ back.chunks[0]?.seq === 1 &&
+ back.chunks.every((c) => c.seq < oldestLoaded),
+ `seqs=[${back.chunks.map((c) => c.seq).join(",")}]`,
+ );
+ }
+ record("CR-5 limit=0 rejected with 400", (await historyStatus(textConv, "limit=0")) === 400);
+ record(
+ "CR-5 beforeSeq=-1 rejected with 400",
+ (await historyStatus(textConv, "beforeSeq=-1")) === 400,
+ );
+
// ─── Metrics: LIVE token + timing ([email protected] usage/step-complete/done) ──────
// (TurnMetricsEntry is `{ turnId, steps, total }` — the turn aggregate lives on
// `total`, present once the live `done` folded.)