summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-21 02:08:04 +0900
committerAdam Malczewski <[email protected]>2026-06-21 02:08:04 +0900
commit75032313a96856a932c109efbbe6b6a7eb782222 (patch)
tree290c368cf1526280418705563a0f61a2c8d94b2b
parent980de470c29d9cae475ada77284025305eaf5474 (diff)
downloaddispatch-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.ts23
-rw-r--r--packages/conversation-store/src/store.ts17
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);