diff options
| author | Adam Malczewski <[email protected]> | 2026-06-21 02:08:04 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-21 02:08:04 +0900 |
| commit | 75032313a96856a932c109efbbe6b6a7eb782222 (patch) | |
| tree | 290c368cf1526280418705563a0f61a2c8d94b2b | |
| parent | 980de470c29d9cae475ada77284025305eaf5474 (diff) | |
| download | dispatch-75032313a96856a932c109efbbe6b6a7eb782222.tar.gz dispatch-75032313a96856a932c109efbbe6b6a7eb782222.zip | |
fix(history): harden loadSince sinceSeq lower bound (forgiving, like beforeSeq/limit)
Coerce sinceSeq to a non-negative integer lower bound in loadSince (omitted/0/
non-positive/non-integer/NaN/Infinity -> 0; valid as-is). The transport layer
400s these upstream, but loadSince stays total for direct callers. Byte-identical
to the prior ?? 0 for the only values any caller ever passed.
58 bun tests pass.
| -rw-r--r-- | packages/conversation-store/src/store.test.ts | 23 | ||||
| -rw-r--r-- | packages/conversation-store/src/store.ts | 17 |
2 files changed, 39 insertions, 1 deletions
diff --git a/packages/conversation-store/src/store.test.ts b/packages/conversation-store/src/store.test.ts index 65c6aed..b119e2a 100644 --- a/packages/conversation-store/src/store.test.ts +++ b/packages/conversation-store/src/store.test.ts @@ -301,6 +301,29 @@ describe("ConversationStore", () => { expect(chunks[0]?.chunk).toEqual({ type: "text", text: "c" }); }); + it("loadSince treats a non-positive / non-integer sinceSeq as 0 (from the start), honoring the contract", async () => { + const store = createConversationStore(storage); + const messages: ChatMessage[] = [ + { role: "user", chunks: [{ type: "text", text: "a" }] }, + { role: "assistant", chunks: [{ type: "text", text: "b" }] }, + { role: "user", chunks: [{ type: "text", text: "c" }] }, + ]; + await store.append("conv1", messages); + const all = [1, 2, 3]; + // Non-positive integers → from the start (already worked; now codified). + expect((await store.loadSince("conv1", 0)).map((c) => c.seq)).toEqual(all); + expect((await store.loadSince("conv1", -2)).map((c) => c.seq)).toEqual(all); + // Non-integer values → from the start (the contract lie this fixes: + // a positive non-integer like 2.5 used to filter like sinceSeq=2). + expect((await store.loadSince("conv1", 2.5)).map((c) => c.seq)).toEqual(all); + expect((await store.loadSince("conv1", 2.7)).map((c) => c.seq)).toEqual(all); + expect((await store.loadSince("conv1", -2.5)).map((c) => c.seq)).toEqual(all); + expect((await store.loadSince("conv1", Number.POSITIVE_INFINITY)).map((c) => c.seq)).toEqual( + all, + ); + expect((await store.loadSince("conv1", Number.NaN)).map((c) => c.seq)).toEqual(all); + }); + it("load() round-trips the exact ChatMessage[] that was appended", async () => { const store = createConversationStore(storage); const messages: ChatMessage[] = [ diff --git a/packages/conversation-store/src/store.ts b/packages/conversation-store/src/store.ts index fdbb2fb..ef5654b 100644 --- a/packages/conversation-store/src/store.ts +++ b/packages/conversation-store/src/store.ts @@ -77,6 +77,21 @@ function positiveInt(value: number | undefined): number | undefined { return value; } +/** + * Coerce `sinceSeq` to a non-negative integer lower bound, honoring the + * contract's stated forgivingness for DIRECT callers: omitted / `0` / + * non-positive / non-integer (incl. `NaN`/`Infinity`) all → `0` (= "from the + * start"). A valid non-negative integer is returned as-is. The transport layer + * 400s these upstream, but `loadSince` stays total on its own. Keeps `loadSince` + * byte-identical to the prior `?? 0` behavior for the only values any caller + * ever passed (omitted / `0` / non-negative integers). + */ +function sinceSeqBase(value: number | undefined): number { + if (value === undefined) return 0; + if (!Number.isInteger(value) || value < 0) return 0; + return value; +} + interface PersistedChunkEntry { readonly chunk: Chunk; readonly role: Role; @@ -164,7 +179,7 @@ export function createConversationStore( const sorted = [...keys].sort(); const result: StoredChunk[] = []; - const minSeq = sinceSeq ?? 0; + const minSeq = sinceSeqBase(sinceSeq); // Forgiving: a non-positive / non-integer bound is treated as ABSENT. const beforeSeq = positiveInt(window?.beforeSeq); const limit = positiveInt(window?.limit); |
