summaryrefslogtreecommitdiffhomepage
path: root/packages/transport-http/src/app.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 18:37:09 +0900
committerAdam Malczewski <[email protected]>2026-06-12 18:37:09 +0900
commitdbf77ba78ff840e0ed5f6294030523fe3ab121fa (patch)
treee768aef3edd0c126212058c3d1433355594c49be /packages/transport-http/src/app.ts
parent6689eb51b467d8e370f31495840d88661f978168 (diff)
downloaddispatch-dbf77ba78ff840e0ed5f6294030523fe3ab121fa.tar.gz
dispatch-dbf77ba78ff840e0ed5f6294030523fe3ab121fa.zip
feat(history): CR-5 windowed reads — ?limit= / ?beforeSeq= on GET /conversations/:id
Selection sinceSeq < seq < beforeSeq; newest-limit window, ascending; positive- integer validation (400, store never sees an invalid window); 1-based gap-free seq codified as the contractual has-older mechanism (no earliestSeq field). transport-contract 0.9.0->0.10.0, wire 0.6.0->0.6.1 (doc-only). conversation-store +8 tests, transport-http +20; 935 vitest + 112 bun green. Live-verified: 6/6 probe checks OK. FE courier: frontend-history-windowing-handoff.md
Diffstat (limited to 'packages/transport-http/src/app.ts')
-rw-r--r--packages/transport-http/src/app.ts39
1 files changed, 38 insertions, 1 deletions
diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts
index 11d2850..ae24922 100644
--- a/packages/transport-http/src/app.ts
+++ b/packages/transport-http/src/app.ts
@@ -17,9 +17,11 @@ import {
computeExpectedCacheRate,
isParseError,
isSinceSeqError,
+ isWindowParamError,
parseChatBody,
parseSinceSeq,
parseWarmBody,
+ parseWindowParam,
serializeEventLine,
} from "./logic.js";
import {
@@ -147,8 +149,43 @@ export function createApp(opts: CreateServerOptions): Hono {
return c.json({ error: sinceSeqResult.error }, 400);
}
+ // `limit` / `beforeSeq` are optional positive-integer history-window
+ // params. The store is deliberately forgiving (a 0/negative bound is
+ // treated as ABSENT), so we MUST reject malformed values here and never
+ // forward an invalid window.
+ const beforeSeqResult = parseWindowParam(c.req.query("beforeSeq"), "beforeSeq");
+ if (isWindowParamError(beforeSeqResult)) {
+ log.warn("conversations: invalid beforeSeq", {
+ conversationId,
+ error: beforeSeqResult.error,
+ });
+ return c.json({ error: beforeSeqResult.error }, 400);
+ }
+ const limitResult = parseWindowParam(c.req.query("limit"), "limit");
+ if (isWindowParamError(limitResult)) {
+ log.warn("conversations: invalid limit", {
+ conversationId,
+ error: limitResult.error,
+ });
+ return c.json({ error: limitResult.error }, 400);
+ }
+
+ // Include only the fields actually provided (exactOptionalPropertyTypes),
+ // and omit the window argument entirely when neither was given — keeping
+ // the pre-windowing call shape byte-identical for existing callers.
+ const window: { readonly beforeSeq?: number; readonly limit?: number } | undefined =
+ beforeSeqResult !== undefined || limitResult !== undefined
+ ? {
+ ...(beforeSeqResult !== undefined ? { beforeSeq: beforeSeqResult } : {}),
+ ...(limitResult !== undefined ? { limit: limitResult } : {}),
+ }
+ : undefined;
+
try {
- const chunks = await opts.conversationStore.loadSince(conversationId, sinceSeqResult);
+ const chunks =
+ window !== undefined
+ ? await opts.conversationStore.loadSince(conversationId, sinceSeqResult, window)
+ : await opts.conversationStore.loadSince(conversationId, sinceSeqResult);
const latestSeq =
chunks.length > 0 ? (chunks[chunks.length - 1]?.seq ?? sinceSeqResult) : sinceSeqResult;
log.info("conversations: read", {