pi-mono/packages/durable/test/memory-storage.test.ts
Mario Zechner e5c6182bab feat(durable): group commit and batch Storage.commit
A Session no longer holds its line while a commit is stored. Commits that
arrive while one storage call is in flight go out together in the next call,
each with its own seq and outcome; a job runs until it touches a key an
unpublished commit wrote, then waits for that commit's publication. Outcomes
are delivered in line order; publication is in line order, one commit per
macrotask. The scheduler's decisions run as regions that record the keys they
read and rerun after a wait, with side effects only after a clean check.

Storage.commit(batches, receive, context) replaces commit(writes, adopt,
context): prefix semantics (Durable's own checks refuse a batch and keep the
ones before it; any other failure rejects the call), receipts per stored
batch, optional publishing() scope. Memory, SQLite, JSONL, IndexedDB and
StoreStorage implement it natively; StoreStorage through the new
ChordStore.commitSeries (one store transaction per call), replacing
commitInBatch.

A normal run makes no storage reads: bounded record cache, conversation
tails, task-document index, kept context ranges. A tool slot's running status
and a generation's attempt are stored with the next commit (12 instead of 14
commits per run).

One Harness with 128 conversations over storage with a 4 ms round trip: about
5 -> 440 agent runs per second. Design: packages/durable/docs/group-commit.md.
2026-10-30 10:41:11 +00:00

71 lines
2.7 KiB
TypeScript

import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
import { registerStorageConformance } from "@earendil-works/pi-durable/testing";
import { describe, expect, it } from "vitest";
import { idFromNumber, seqFromNumber } from "../src/ids.ts";
import { MemoryStorage } from "../src/storage/memory.ts";
import { commitWrites } from "../src/testing/commit-writes.ts";
import { type EntryId, ROOT_CONVERSATION_ID, type Seq, type StorageWrite } from "../src/types.ts";
registerStorageConformance({ describe, expect, it }, "MemoryStorage", (use) => use(new MemoryStorage()));
it("does not expose retained state through a prepared commit", async () => {
const storage = new MemoryStorage();
await commitWrites(storage, [{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], BACKGROUND_CONTEXT);
const entryId = idFromNumber<EntryId>(2);
const prepared = storage.prepareCommit([
{
type: "entry",
value: {
id: entryId,
conversationId: ROOT_CONVERSATION_ID,
kind: "test",
data: { nested: [1] },
},
},
]);
const exposed = prepared.writes[0];
if (exposed.type !== "entry") throw new Error("Expected an entry write");
expect(() => (exposed.value.data as { nested: number[] }).nested.push(2)).toThrow();
expect(prepared.apply()).toBe(seqFromNumber(2));
expect(prepared.apply()).toBe(seqFromNumber(2));
expect((await storage.entry(entryId, BACKGROUND_CONTEXT))?.entry.data).toEqual({ nested: [1] });
});
it("delivers the receipts of the batches applied before one whose apply throws, then rejects", async () => {
const failure = new Error("apply failed");
class FailingApply extends MemoryStorage {
failAt: Seq | undefined;
override prepareCommit(writes: readonly StorageWrite[], seq?: Seq) {
const prepared = super.prepareCommit(writes, seq);
if (prepared.seq !== this.failAt) return prepared;
return {
...prepared,
apply: (): Seq => {
throw failure;
},
};
}
}
const storage = new FailingApply();
const root = await commitWrites(
storage,
[{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }],
BACKGROUND_CONTEXT,
);
const [first, second] = [await storage.mintId<EntryId>(), await storage.mintId<EntryId>()];
storage.failAt = seqFromNumber(root + 2);
const receipts: [number, Seq][] = [];
await expect(
storage.commit(
[first, second].map((id) => [
{ type: "entry", value: { id, conversationId: ROOT_CONVERSATION_ID, kind: "m" } },
]),
(index, seq) => receipts.push([index, seq]),
BACKGROUND_CONTEXT,
),
).rejects.toBe(failure);
expect(receipts).toEqual([[0, root + 1]]);
expect((await storage.entry(first, BACKGROUND_CONTEXT))?.commitSeq).toBe(root + 1);
expect(await storage.entry(second, BACKGROUND_CONTEXT)).toBeUndefined();
});