From 5c1736f82d201d09664642ab0d88147da94b450e Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 19 Oct 2026 18:17:42 +0000 Subject: [PATCH] feat: checkpointWhen is required on Durable document definitions Breaking change: defineDoc and defineDocFamily now require an explicit checkpointWhen predicate; no default is supplied. Set policies on built-in documents, examples, tests, and triage state, and document the required API. --- packages/durable/CHANGELOG.md | 1 + packages/durable/README.md | 1 + packages/durable/docs/spec.md | 16 +++-- packages/durable/src/codemode/codemode.ts | 5 ++ packages/durable/src/documents.ts | 2 +- packages/durable/src/harness/tool.ts | 2 + packages/durable/src/session/transaction.ts | 2 +- packages/durable/src/types.ts | 4 +- .../durable/test/codemode-checkpoints.test.ts | 61 +++++++++++++++++++ .../durable/test/document-append-bench.ts | 2 + .../durable/test/examples/01-documents.ts | 1 + packages/durable/test/examples/02-forks.ts | 1 + .../test/examples/03-owned-conversations.ts | 1 + .../durable/test/examples/04-chord-state.ts | 1 + packages/durable/test/examples/05-watches.ts | 1 + packages/durable/test/examples/06-harness.ts | 1 + .../test/examples/11-extension-state.ts | 1 + .../test/examples/23-subagent-background.ts | 1 + .../durable/test/examples/27-plan-mode.ts | 1 + .../examples/29-sandbox-per-conversation.ts | 1 + .../test/harness-conversations.test.ts | 1 + .../durable/test/harness-generation.test.ts | 1 + packages/durable/test/harness-inbox.test.ts | 1 + .../durable/test/harness-lifecycle.test.ts | 5 ++ .../durable/test/harness-ownership.test.ts | 1 + .../durable/test/harness-structured.test.ts | 1 + .../test/harness-tasks-recovery.test.ts | 1 + packages/durable/test/harness-tasks.test.ts | 6 ++ packages/durable/test/harness-view.test.ts | 1 + .../session-checkpoints-migrations.test.ts | 23 +++++++ .../durable/test/session-definitions.test.ts | 47 +++++++++++--- .../test/session-document-retention.test.ts | 9 +++ .../durable/test/session-documents.test.ts | 7 +++ packages/durable/test/session-forks.test.ts | 23 +++++++ packages/durable/test/session-states.test.ts | 5 ++ packages/durable/test/session-tables.test.ts | 3 + packages/durable/test/session-watches.test.ts | 3 + packages/durable/test/spec-usage.test.ts | 2 + scripts/issues/triage.ts | 1 + 39 files changed, 231 insertions(+), 16 deletions(-) create mode 100644 packages/durable/test/codemode-checkpoints.test.ts diff --git a/packages/durable/CHANGELOG.md b/packages/durable/CHANGELOG.md index 27536a160..1bb24cccd 100644 --- a/packages/durable/CHANGELOG.md +++ b/packages/durable/CHANGELOG.md @@ -9,6 +9,7 @@ - Opening a SQLite storage migrates it to schema version 2 automatically, rebuilding its task table. A migrated database cannot be opened by 1.3.0 or earlier, so a deployment cannot roll back past this release. JSONL storage needs no migration, but 1.3.0 or earlier never resumes a sleeping task it finds (a run's conversation stays busy). - `TaskRuntime` has `sleepUntil()` and `setTimer()`, and `ToolExecutionApi` has `now()`: code that builds either object by hand, such as a test fake or a tool wrapper that does not spread the original, must add them. - A task cut off by a crash 5 times in a row without committing a new state is parked instead of run again (see the crash-loop guard under Added); set `settings.restartLimit` to `Infinity` to keep the old behavior. `TaskInspection`'s blocked reasons include `restart_limit`. +- `defineDoc()` and `defineDocFamily()` require an explicit `checkpointWhen` predicate on every definition; there is no default. Use `() => false` to keep ordinary changes as deltas, or supply a policy that periodically writes a base. ### Added diff --git a/packages/durable/README.md b/packages/durable/README.md index 60b50ed3d..f43e67564 100644 --- a/packages/durable/README.md +++ b/packages/durable/README.md @@ -735,6 +735,7 @@ const Todos = defineDoc<{ items: string[] }>({ history: "latest", // or "rewindable" to read old values with snapshotAsOf() fork: "initial", // what a fork starts with: "initial", "current", or "asOf" initial: () => ({ items: [] }), + checkpointWhen: () => false, // required: true stores this change as a complete base, bounding replay }); await root.commit(async (tx) => { diff --git a/packages/durable/docs/spec.md b/packages/durable/docs/spec.md index eb957dbea..3e206cf8e 100644 --- a/packages/durable/docs/spec.md +++ b/packages/durable/docs/spec.md @@ -940,6 +940,7 @@ keeps that in its own document and reads it through `target.read`: const ContainerDoc = defineDoc<{ image: string }>({ kind: "app.container", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ image: "node:22" }), + checkpointWhen: () => false, }); const harness = await Harness.open(storage, { models, @@ -1099,7 +1100,8 @@ type CommonDocDefinition = { readonly version: number; initial(): T; migrate?(value: JsonObject, fromVersion: number): T; - checkpointWhen?(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; + /** Required, no default: whether this change is stored as a complete base (section 3.5). */ + checkpointWhen(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; }; type DocDefinition = @@ -1217,6 +1219,9 @@ Validation rules: - Task documents are current-only, are never copied by a conversation fork, and retire atomically when their task becomes terminal. - `initial()` and `migrate()` return JSON objects. +- Every `defineDoc()` and `defineDocFamily()` definition supplies + `checkpointWhen`; there is no default. A definition that wants only the + required bases writes `checkpointWhen: () => false`. `checkpointWhen()` only selects complete storage bases to bound replay. It does not change scope, lifetime, history, or fork semantics. @@ -1540,7 +1545,7 @@ For an ordinary later mutation, the definition alone decides whether the storage record is a base: ```ts -const useBase = definition.checkpointWhen?.(candidateValue, ops, { deltasSinceBase }) ?? false; +const useBase = definition.checkpointWhen(candidateValue, ops, { deltasSinceBase }); ``` The Session evaluates this predicate exactly once after tracker preparation. @@ -3004,6 +3009,7 @@ document holds that behavior's state: const PlanModeDoc = defineDoc<{ enabled: boolean }>({ kind: "app.plan-mode", version: 1, scope: "conversation", history: "latest", fork: "current", initial: () => ({ enabled: false }), + checkpointWhen: () => false, }); export const PlanMode = defineExtension({ name: "plan-mode", @@ -5525,9 +5531,9 @@ These are contracts, not invitations to add defensive machinery: - **Family initialization:** the first acquisition of an absent family address selects its seed. Existing instances and later calls ignore seeds; a seed is neither identity nor an update. -- **Checkpoint starvation:** if `checkpointWhen()` never returns true, replay - and current-only document storage can grow without bound while the document is - live. +- **Checkpoint starvation:** if `checkpointWhen()` never returns true, as with + `() => false`, replay and current-only document storage can grow without bound + while the document is live. - **Wrong fork setting:** `current`, `initial`, and `asOf` are product semantics, not optimizations. Changing one changes child conversation behavior. - **Schema stability:** document kinds and visible mount paths are diff --git a/packages/durable/src/codemode/codemode.ts b/packages/durable/src/codemode/codemode.ts index a58b2b345..d1f04d223 100644 --- a/packages/durable/src/codemode/codemode.ts +++ b/packages/durable/src/codemode/codemode.ts @@ -126,6 +126,8 @@ export const CodemodeRunDoc = defineDoc({ version: 1, scope: "task", initial: () => ({}), + // The document retires with its task. + checkpointWhen: () => false, }); type JournalEntry = { @@ -150,6 +152,8 @@ export const CodemodeJournalDoc = defineDoc({ version: 1, scope: "task", initial: () => ({ entries: [] }), + // The document retires with its task. + checkpointWhen: () => false, }); /** @@ -163,6 +167,7 @@ export const CodemodeStoreDoc = defineDoc<{ values: Record }> history: "rewindable", fork: "asOf", initial: () => ({ values: {} }), + checkpointWhen: (_value, _ops, { deltasSinceBase }) => deltasSinceBase >= 64, }); /** One tool call a script made, as the code mode call's details show it. */ diff --git a/packages/durable/src/documents.ts b/packages/durable/src/documents.ts index 92753d019..2dbde8fd0 100644 --- a/packages/durable/src/documents.ts +++ b/packages/durable/src/documents.ts @@ -86,7 +86,7 @@ export type AnyDocDefinition = DocumentSemantics & { readonly family?: true; initial(seed?: JsonValue): JsonObject; migrate?(value: JsonObject, fromVersion: number): JsonObject; - checkpointWhen?(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; + checkpointWhen(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; }; /** Erased singleton or family token. */ diff --git a/packages/durable/src/harness/tool.ts b/packages/durable/src/harness/tool.ts index f82c041f8..52bc4a3ad 100644 --- a/packages/durable/src/harness/tool.ts +++ b/packages/durable/src/harness/tool.ts @@ -109,6 +109,8 @@ export const NestedCallDoc = defineDocFamily({ scope: "task", family: true, initial: () => ({}), + // The document retires with its task. + checkpointWhen: () => false, }); type Runtime = TaskRuntime; diff --git a/packages/durable/src/session/transaction.ts b/packages/durable/src/session/transaction.ts index c0909aaa0..e7b783408 100644 --- a/packages/durable/src/session/transaction.ts +++ b/packages/durable/src/session/transaction.ts @@ -865,7 +865,7 @@ export class Transaction implements Tx { if (plan.content?.type === "document.change" && plan.content.content.kind === "delta" && change?.loaded) { const { prepared, version } = change; const info = { deltasSinceBase: change.loaded.deltasSinceBase }; - if (change.definition?.checkpointWhen?.(prepared.value, prepared.ops, info)) { + if (change.definition!.checkpointWhen(prepared.value, prepared.ops, info)) { plan.content = { ...plan.content, content: { version, kind: "base", value: prepared.value } }; } } diff --git a/packages/durable/src/types.ts b/packages/durable/src/types.ts index d307f258b..fe69f8e20 100644 --- a/packages/durable/src/types.ts +++ b/packages/durable/src/types.ts @@ -74,8 +74,8 @@ export type CommonDocDefinition = { readonly version: number; initial(): T; migrate?(value: JsonObject, fromVersion: number): T; - /** Return true to store this ordinary change as a complete base instead of a delta. */ - checkpointWhen?(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; + /** Required policy: return true to store this ordinary change as a complete base instead of a delta. */ + checkpointWhen(value: Readonly, ops: readonly Op[], info: CheckpointInfo): boolean; }; /** Singleton document definition. */ diff --git a/packages/durable/test/codemode-checkpoints.test.ts b/packages/durable/test/codemode-checkpoints.test.ts new file mode 100644 index 000000000..098feb12d --- /dev/null +++ b/packages/durable/test/codemode-checkpoints.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { CodemodeJournalDoc, CodemodeRunDoc, CodemodeStoreDoc } from "../src/codemode/codemode.ts"; +import { NestedCallDoc } from "../src/harness/tool.ts"; +import { context, createConversation, openTestSession } from "./session-support.ts"; + +describe("built-in document checkpoint policies", () => { + it("keeps task-scoped documents as deltas until their task retires", () => { + const info = { deltasSinceBase: 1000 }; + expect(NestedCallDoc.definition.checkpointWhen({}, [], info)).toBe(false); + expect(CodemodeRunDoc.definition.checkpointWhen({}, [], info)).toBe(false); + expect(CodemodeJournalDoc.definition.checkpointWhen({ entries: [] }, [], info)).toBe(false); + }); + + it("checkpoints the codemode store after 64 deltas without changing historical reads or forks", async () => { + const { session, storage } = openTestSession(); + try { + const conversationId = await createConversation(session); + let cutoff = await session.commit(async (tx) => { + (await tx.doc(CodemodeStoreDoc, conversationId)).values.counter = 0; + return (await tx.appendEntry(conversationId, { kind: "test.counter" })).id; + }, context); + const record = await storage.findDocument( + { kind: CodemodeStoreDoc.definition.kind, scope: { kind: "conversation", conversationId } }, + "current", + context, + ); + if (record === undefined) throw new Error("Expected codemode store"); + for (let counter = 1; counter <= 64; counter++) { + const entry = await session.commit(async (tx) => { + (await tx.doc(CodemodeStoreDoc, conversationId)).values.counter = counter; + return (await tx.appendEntry(conversationId, { kind: "test.counter" })).id; + }, context); + if (counter === 32) cutoff = entry; + } + expect((await storage.document(record.id, "current", context))?.deltasSinceBase).toBe(64); + await session.commit(async (tx) => { + (await tx.doc(CodemodeStoreDoc, conversationId)).values.counter = 65; + }, context); + expect((await storage.document(record.id, "current", context))?.deltasSinceBase).toBe(0); + expect(storage.commits.at(-1)).toContainEqual({ + type: "document.change", + id: record.id, + content: { kind: "base", version: 1, value: { values: { counter: 65 } } }, + }); + expect(await session.snapshotAsOf(CodemodeStoreDoc, conversationId, cutoff, context)).toEqual({ + values: { counter: 32 }, + }); + const fork = await session.commit( + (tx) => tx.forkConversation(conversationId, cutoff, { ownership: { kind: "ownerless" } }), + context, + ); + expect(await session.snapshot(CodemodeStoreDoc, fork.id, context)).toEqual({ values: { counter: 32 } }); + await session.commit(async (tx) => { + (await tx.doc(CodemodeStoreDoc, conversationId)).values.counter = 66; + }, context); + expect((await storage.document(record.id, "current", context))?.deltasSinceBase).toBe(1); + } finally { + await session.close(context); + } + }); +}); diff --git a/packages/durable/test/document-append-bench.ts b/packages/durable/test/document-append-bench.ts index a2c160d7d..650fbd377 100644 --- a/packages/durable/test/document-append-bench.ts +++ b/packages/durable/test/document-append-bench.ts @@ -13,6 +13,7 @@ import { createSession } from "../src/session/session.ts"; type Entry = { key: string; issued: number; at: number }; const Journal = defineDoc<{ entries: Entry[] }>({ + checkpointWhen: () => false, kind: "bench.journal", version: 1, scope: "session", @@ -20,6 +21,7 @@ const Journal = defineDoc<{ entries: Entry[] }>({ }); const Member = defineDocFamily<{ issued: number; at: number }, null>({ + checkpointWhen: () => false, kind: "bench.member", version: 1, scope: "session", diff --git a/packages/durable/test/examples/01-documents.ts b/packages/durable/test/examples/01-documents.ts index d6638eba6..bb22db1bf 100644 --- a/packages/durable/test/examples/01-documents.ts +++ b/packages/durable/test/examples/01-documents.ts @@ -12,6 +12,7 @@ const session = createSession(new MemoryStorage()); // looked like when a particular entry was written. // `fork` says what a forked copy of the conversation starts with (see 02-forks.ts). const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "example.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/02-forks.ts b/packages/durable/test/examples/02-forks.ts index ef51a84a5..ee4467806 100644 --- a/packages/durable/test/examples/02-forks.ts +++ b/packages/durable/test/examples/02-forks.ts @@ -8,6 +8,7 @@ const context = BACKGROUND_CONTEXT; const session = createSession(new MemoryStorage()); const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "example.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/03-owned-conversations.ts b/packages/durable/test/examples/03-owned-conversations.ts index cdf3240f6..a6086a4cc 100644 --- a/packages/durable/test/examples/03-owned-conversations.ts +++ b/packages/durable/test/examples/03-owned-conversations.ts @@ -29,6 +29,7 @@ const Supervisor = defineTask({ const AgentRegistry = defineDoc<{ agents: Record; }>({ + checkpointWhen: () => false, kind: "example.agent-registry", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/04-chord-state.ts b/packages/durable/test/examples/04-chord-state.ts index 50778fd6d..75d3e3b7b 100644 --- a/packages/durable/test/examples/04-chord-state.ts +++ b/packages/durable/test/examples/04-chord-state.ts @@ -8,6 +8,7 @@ const context = BACKGROUND_CONTEXT; const session = createSession(new MemoryStorage()); const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "example.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/05-watches.ts b/packages/durable/test/examples/05-watches.ts index c0c6d6b58..9dba1c965 100644 --- a/packages/durable/test/examples/05-watches.ts +++ b/packages/durable/test/examples/05-watches.ts @@ -8,6 +8,7 @@ const context = BACKGROUND_CONTEXT; const session = createSession(new MemoryStorage()); const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "example.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/06-harness.ts b/packages/durable/test/examples/06-harness.ts index 141e8e667..e4ebdeb6d 100644 --- a/packages/durable/test/examples/06-harness.ts +++ b/packages/durable/test/examples/06-harness.ts @@ -34,6 +34,7 @@ registry.install(Files); const harness = await Harness.open(new MemoryStorage(), { models: createModels(), registry }, context); const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "example.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/11-extension-state.ts b/packages/durable/test/examples/11-extension-state.ts index a75a7ef4a..8bd2e75da 100644 --- a/packages/durable/test/examples/11-extension-state.ts +++ b/packages/durable/test/examples/11-extension-state.ts @@ -20,6 +20,7 @@ const context = BACKGROUND_CONTEXT; // A todo list per conversation. Rewindable with `asOf`, so a fork starts with // the todos its parent had at the fork entry. const Todos = defineDoc<{ items: string[] }>({ + checkpointWhen: () => false, kind: "example.todos", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/23-subagent-background.ts b/packages/durable/test/examples/23-subagent-background.ts index 261a4e7e4..6b2b4a533 100644 --- a/packages/durable/test/examples/23-subagent-background.ts +++ b/packages/durable/test/examples/23-subagent-background.ts @@ -44,6 +44,7 @@ type Subagent = { reported: EntryId[]; }; const Subagents = defineDoc<{ agents: Record; reporters: Record }>({ + checkpointWhen: () => false, kind: "app.subagents", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/27-plan-mode.ts b/packages/durable/test/examples/27-plan-mode.ts index ebd6438da..9736140f0 100644 --- a/packages/durable/test/examples/27-plan-mode.ts +++ b/packages/durable/test/examples/27-plan-mode.ts @@ -27,6 +27,7 @@ const context = BACKGROUND_CONTEXT; // The current plan of a conversation. A fork keeps the plan it had at the fork entry. const PlanDoc = defineDoc<{ steps: string[] }>({ + checkpointWhen: () => false, kind: "app.plan", version: 1, scope: "conversation", diff --git a/packages/durable/test/examples/29-sandbox-per-conversation.ts b/packages/durable/test/examples/29-sandbox-per-conversation.ts index 792e328cb..d570b5721 100644 --- a/packages/durable/test/examples/29-sandbox-per-conversation.ts +++ b/packages/durable/test/examples/29-sandbox-per-conversation.ts @@ -17,6 +17,7 @@ const context = BACKGROUND_CONTEXT; // Which sandbox a conversation runs in. `fork: "initial"`: a fork gets no sandbox until the app assigns one. const Sandbox = defineDoc<{ path?: string }>({ + checkpointWhen: () => false, kind: "app.sandbox", version: 1, scope: "conversation", diff --git a/packages/durable/test/harness-conversations.test.ts b/packages/durable/test/harness-conversations.test.ts index ed5deba6e..30697772a 100644 --- a/packages/durable/test/harness-conversations.test.ts +++ b/packages/durable/test/harness-conversations.test.ts @@ -41,6 +41,7 @@ afterEach(async () => { }); const NoteDoc = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.note", version: 1, scope: "conversation", diff --git a/packages/durable/test/harness-generation.test.ts b/packages/durable/test/harness-generation.test.ts index 858197ebf..381a2cfc1 100644 --- a/packages/durable/test/harness-generation.test.ts +++ b/packages/durable/test/harness-generation.test.ts @@ -539,6 +539,7 @@ describe("generation", () => { it("renders sections that read conversation documents through input.read", async () => { const Agent = defineDoc<{ cwd: string; kind: string }>({ + checkpointWhen: () => false, kind: "test.agent", version: 1, scope: "conversation", diff --git a/packages/durable/test/harness-inbox.test.ts b/packages/durable/test/harness-inbox.test.ts index 91717e516..9c7a0505e 100644 --- a/packages/durable/test/harness-inbox.test.ts +++ b/packages/durable/test/harness-inbox.test.ts @@ -186,6 +186,7 @@ describe("inbox", () => { // Occupy the line, let the answer queue its boundary commit behind it, then change the mode. const held = storage.holdCommits(); const Marker = defineDoc<{ n: number }>({ + checkpointWhen: () => false, kind: "test.marker", version: 1, scope: "session", diff --git a/packages/durable/test/harness-lifecycle.test.ts b/packages/durable/test/harness-lifecycle.test.ts index a961af1c3..91fce7ecb 100644 --- a/packages/durable/test/harness-lifecycle.test.ts +++ b/packages/durable/test/harness-lifecycle.test.ts @@ -276,6 +276,7 @@ describe("Harness close", () => { it("publishes no frame to states and watches from a commit that settles during close", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.close-frames", version: 1, scope: "session", @@ -337,6 +338,7 @@ describe("Harness close", () => { it("leaves no subscription behind a watch acquisition cancelled on the line", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.cancelled-watch", version: 1, scope: "session", @@ -451,12 +453,14 @@ describe("Harness close", () => { it("completes reads queued at the seal and rejects queued waits and acquisitions that would follow commits", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.queued-at-seal", version: 1, scope: "session", initial: () => ({ text: "" }), }); const Absent = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.queued-absent", version: 1, scope: "session", @@ -535,6 +539,7 @@ describe("scheduling on a paused Harness", () => { it("never schedules from a read-only viewer", async () => { const { harness, root, id, submissionId, ran } = await paused(); const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.viewer-notes", version: 1, scope: "session", diff --git a/packages/durable/test/harness-ownership.test.ts b/packages/durable/test/harness-ownership.test.ts index 4fd669847..40430cdc8 100644 --- a/packages/durable/test/harness-ownership.test.ts +++ b/packages/durable/test/harness-ownership.test.ts @@ -1313,6 +1313,7 @@ describe("owned conversations from tools and supervisors", () => { directories.add(directory); const path = join(directory, "session.sqlite"); const Children = defineDoc<{ child?: ConversationId }>({ + checkpointWhen: () => false, kind: "test.children", version: 1, scope: "conversation", diff --git a/packages/durable/test/harness-structured.test.ts b/packages/durable/test/harness-structured.test.ts index 6970f6e91..16d191e0a 100644 --- a/packages/durable/test/harness-structured.test.ts +++ b/packages/durable/test/harness-structured.test.ts @@ -132,6 +132,7 @@ const Unregistered = defineTask({ }); const TaskNotes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.task-notes", version: 1, scope: "task", diff --git a/packages/durable/test/harness-tasks-recovery.test.ts b/packages/durable/test/harness-tasks-recovery.test.ts index ebaea6e1b..29b98d8d9 100644 --- a/packages/durable/test/harness-tasks-recovery.test.ts +++ b/packages/durable/test/harness-tasks-recovery.test.ts @@ -525,6 +525,7 @@ describe("blocked tasks", () => { it("settles an aborted blocked task as orphaned and retires its documents", async () => { const Scratch = defineDoc<{ n: number }>({ + checkpointWhen: () => false, kind: "test.orphan-scratch", version: 1, scope: "task", diff --git a/packages/durable/test/harness-tasks.test.ts b/packages/durable/test/harness-tasks.test.ts index 899d4fe59..f76cb8308 100644 --- a/packages/durable/test/harness-tasks.test.ts +++ b/packages/durable/test/harness-tasks.test.ts @@ -187,6 +187,7 @@ describe("task phases", () => { it("commits results with entries atomically, keeps memos until terminal, and retires task documents", async () => { const Progress = defineDoc<{ lines: string[] }>({ + checkpointWhen: () => false, kind: "test.task-progress", version: 1, scope: "task", @@ -521,6 +522,7 @@ describe("task phases", () => { describe("task runtime", () => { it("rejects runtime operations after the invocation ends and stops its watches", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.task-notes", version: 1, scope: "session", @@ -530,6 +532,7 @@ describe("task runtime", () => { let watchClosed: Promise | undefined; const delivered: string[] = []; const Absent = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.task-absent", version: 1, scope: "session", @@ -578,6 +581,7 @@ describe("task runtime", () => { it("reads committed documents and context through the runtime, and forwards the clock and reports", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.runtime-notes", version: 1, scope: "conversation", @@ -653,6 +657,7 @@ describe("task runtime", () => { it("stops a watch whose acquisition finishes after the invocation ended", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.late-watch", version: 1, scope: "session", @@ -1164,6 +1169,7 @@ describe("task close", () => { it("seals admission before signalling handlers and stops watches before joining them", async () => { const Notes = defineDoc<{ text: string }>({ + checkpointWhen: () => false, kind: "test.close-notes", version: 1, scope: "session", diff --git a/packages/durable/test/harness-view.test.ts b/packages/durable/test/harness-view.test.ts index ce05d4c2a..44f12e91d 100644 --- a/packages/durable/test/harness-view.test.ts +++ b/packages/durable/test/harness-view.test.ts @@ -326,6 +326,7 @@ describe("conversation view", () => { it("publishes one frame for a commit that appends several entries and edits a document", async () => { const { harness, root } = await openChat(new MemoryStorage(), chatSetup()); const Other = defineDoc<{ n: number }>({ + checkpointWhen: () => false, kind: "app.other", version: 1, scope: "conversation", diff --git a/packages/durable/test/session-checkpoints-migrations.test.ts b/packages/durable/test/session-checkpoints-migrations.test.ts index 375287979..20e56d721 100644 --- a/packages/durable/test/session-checkpoints-migrations.test.ts +++ b/packages/durable/test/session-checkpoints-migrations.test.ts @@ -63,6 +63,7 @@ describe("Session document checkpoints", () => { }, }); const DefaultDoc = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "checkpoint.default", version: 1, scope: "session", @@ -333,6 +334,7 @@ describe("Session document migrations", () => { it("migrates read-only once per cold load, copies the callback result, and writes nothing", async () => { type Current = { count: number; labels: string[] }; const Old = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.read-only", version: 1, scope: "session", @@ -341,6 +343,7 @@ describe("Session document migrations", () => { let calls = 0; let retained: Current | undefined; const Current = defineDoc({ + checkpointWhen: () => false, kind: "migration.read-only", version: 3, scope: "session", @@ -375,6 +378,7 @@ describe("Session document migrations", () => { it("writes the required base on the first successful transaction, then writes deltas", async () => { const Old = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.transition", version: 1, scope: "session", @@ -439,6 +443,7 @@ describe("Session document migrations", () => { it("rolls migration and edits back with the callback, then coalesces later edits into one base", async () => { const Old = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.rollback", version: 1, scope: "session", @@ -446,6 +451,7 @@ describe("Session document migrations", () => { }); let migrations = 0; const Current = defineDoc<{ count: number; migrated: boolean }>({ + checkpointWhen: () => false, kind: "migration.rollback", version: 2, scope: "session", @@ -495,12 +501,14 @@ describe("Session document migrations", () => { it("strict-checks migration results before tracker ownership and remains usable after rejection", async () => { const Old = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.invalid", version: 1, scope: "session", initial: () => ({ count: 1 }), }); const Invalid = defineDoc({ + checkpointWhen: () => false, kind: "migration.invalid", version: 2, scope: "session", @@ -508,6 +516,7 @@ describe("Session document migrations", () => { migrate: () => ({ invalid: new Date() }) as unknown as JsonObject, }); const Other = defineDoc<{ ok: boolean }>({ + checkpointWhen: () => false, kind: "migration.invalid.other", version: 1, scope: "session", @@ -528,18 +537,21 @@ describe("Session document migrations", () => { it("rejects newer stored versions and older versions without migration for snapshots and transactions", async () => { const V2 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.compatibility", version: 2, scope: "session", initial: () => ({ count: 2 }), }); const V1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.compatibility", version: 1, scope: "session", initial: () => ({ count: 1 }), }); const V3WithoutMigration = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.compatibility", version: 3, scope: "session", @@ -563,6 +575,7 @@ describe("Session document migrations", () => { it("persists a required migration base before retirement without consulting the checkpoint predicate", async () => { const Old = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.retire", version: 1, scope: "session", @@ -596,12 +609,14 @@ describe("Session document migrations", () => { it("leaves unaccessed older documents and unavailable definitions untouched", async () => { const FirstV1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.lazy.first", version: 1, scope: "session", initial: () => ({ count: 1 }), }); const SecondV1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.lazy.second", version: 1, scope: "session", @@ -609,6 +624,7 @@ describe("Session document migrations", () => { }); let secondMigrations = 0; const FirstV2 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.lazy.first", version: 2, scope: "session", @@ -616,6 +632,7 @@ describe("Session document migrations", () => { migrate: (value) => ({ count: value.count as number }), }); defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "migration.lazy.second", version: 2, scope: "session", @@ -647,6 +664,7 @@ describe("Session document migrations", () => { describe("Session historical document snapshots", () => { it("migrates current and historical rewindable values independently and follows fork ancestry", async () => { const V1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "history.migration", version: 1, scope: "conversation", @@ -655,6 +673,7 @@ describe("Session historical document snapshots", () => { initial: () => ({ count: 0 }), }); const Family = defineDocFamily<{ seed: string; count: number }, string>({ + checkpointWhen: () => false, kind: "history.family", version: 1, family: true, @@ -665,6 +684,7 @@ describe("Session historical document snapshots", () => { }); const migrations: number[] = []; const V3 = defineDoc<{ count: number; version: number }>({ + checkpointWhen: () => false, kind: "history.migration", version: 3, scope: "conversation", @@ -734,6 +754,7 @@ describe("Session historical document snapshots", () => { it("selects the incarnation alive at the entry commit across retirement and recreation", async () => { const Doc = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "history.incarnation", version: 1, scope: "conversation", @@ -776,12 +797,14 @@ describe("Session tracker cache across definition versions", () => { type V1 = { name: string }; type V2 = { names: string[] }; const V1Doc = defineDoc({ + checkpointWhen: () => false, kind: "cache.versioned", version: 1, scope: "session", initial: () => ({ name: "first" }), }); const V2Doc = defineDoc({ + checkpointWhen: () => false, kind: "cache.versioned", version: 2, scope: "session", diff --git a/packages/durable/test/session-definitions.test.ts b/packages/durable/test/session-definitions.test.ts index ddc791187..1146a1067 100644 --- a/packages/durable/test/session-definitions.test.ts +++ b/packages/durable/test/session-definitions.test.ts @@ -23,8 +23,15 @@ import { context } from "./session-support.ts"; type State = { value: number }; const initial = (): State => ({ value: 0 }); -const SessionDoc = defineDoc({ kind: "t.session", version: 1, scope: "session", initial }); +const SessionDoc = defineDoc({ + checkpointWhen: () => false, + kind: "t.session", + version: 1, + scope: "session", + initial, +}); const LatestDoc = defineDoc({ + checkpointWhen: () => false, kind: "t.latest", version: 1, scope: "conversation", @@ -33,6 +40,7 @@ const LatestDoc = defineDoc({ initial, }); const RewindableDoc = defineDoc({ + checkpointWhen: () => false, kind: "t.rewindable", version: 1, scope: "conversation", @@ -40,8 +48,9 @@ const RewindableDoc = defineDoc({ fork: "asOf", initial, }); -const TaskDoc = defineDoc({ kind: "t.task", version: 1, scope: "task", initial }); +const TaskDoc = defineDoc({ checkpointWhen: () => false, kind: "t.task", version: 1, scope: "task", initial }); const SessionFamily = defineDocFamily({ + checkpointWhen: () => false, kind: "t.session-family", version: 1, family: true, @@ -49,6 +58,7 @@ const SessionFamily = defineDocFamily({ initial: (seed) => ({ value: seed }), }); const ConversationFamily = defineDocFamily({ + checkpointWhen: () => false, kind: "t.conversation-family", version: 1, family: true, @@ -58,6 +68,7 @@ const ConversationFamily = defineDocFamily({ initial: (seed) => ({ value: seed }), }); const TaskFamily = defineDocFamily({ + checkpointWhen: () => false, kind: "t.task-family", version: 1, family: true, @@ -66,12 +77,34 @@ const TaskFamily = defineDocFamily({ }); describe("document definitions", () => { + it("requires an explicit checkpoint predicate for documents and families", () => { + const check = () => { + // @ts-expect-error document definitions require checkpointWhen + defineDoc({ kind: "t.missing-checkpoint", version: 1, scope: "session", initial }); + const family = { + kind: "t.missing-family-checkpoint", + version: 1, + family: true, + scope: "session", + initial: (seed: number) => ({ value: seed }), + } as const; + // @ts-expect-error document family definitions require checkpointWhen + defineDocFamily(family); + }; + expect(check).toBeTypeOf("function"); + }); + it("validates persisted version semantics", () => { - expect(() => defineDoc({ kind: "k", version: 0, scope: "session", initial })).toThrow("positive integer"); - expect(() => defineDoc({ kind: "k", version: 1.5, scope: "session", initial })).toThrow( - "positive integer", - ); - expect(defineDoc({ kind: "", version: 1, scope: "session", initial }).definition.kind).toBe(""); + expect(() => + defineDoc({ checkpointWhen: () => false, kind: "k", version: 0, scope: "session", initial }), + ).toThrow("positive integer"); + expect(() => + defineDoc({ checkpointWhen: () => false, kind: "k", version: 1.5, scope: "session", initial }), + ).toThrow("positive integer"); + expect( + defineDoc({ checkpointWhen: () => false, kind: "", version: 1, scope: "session", initial }).definition + .kind, + ).toBe(""); expect(SessionDoc.definition.kind).toBe("t.session"); }); diff --git a/packages/durable/test/session-document-retention.test.ts b/packages/durable/test/session-document-retention.test.ts index c64845183..04f78586b 100644 --- a/packages/durable/test/session-document-retention.test.ts +++ b/packages/durable/test/session-document-retention.test.ts @@ -22,6 +22,7 @@ import { deferred, eventually } from "./task-support.ts"; type Counter = { count: number }; const CounterDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.counter", version: 1, scope: "conversation", @@ -30,6 +31,7 @@ const CounterDoc = defineDoc({ initial: () => ({ count: 0 }), }); const RewindableDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.rewindable", version: 1, scope: "conversation", @@ -39,6 +41,7 @@ const RewindableDoc = defineDoc({ }); /** A third-party conversation family: one member per event. */ const EventDoc = defineDocFamily<{ seq: number }, number>({ + checkpointWhen: () => false, kind: "test.retention.event", version: 1, scope: "conversation", @@ -48,6 +51,7 @@ const EventDoc = defineDocFamily<{ seq: number }, number>({ initial: (seq) => ({ seq }), }); const SessionCounterDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.session-counter", version: 1, scope: "session", @@ -73,6 +77,7 @@ const defaultForRun = /** A conversation document any Harness test can read. */ const TestDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.harness", version: 1, scope: "conversation", @@ -396,6 +401,7 @@ describe("document cache retention", () => { it("migrates a document again after its eviction", async () => { const V1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "test.retention.migrating", version: 1, scope: "session", @@ -403,6 +409,7 @@ describe("document cache retention", () => { }); let migrations = 0; const V2 = defineDoc<{ count: number; migrated: boolean }>({ + checkpointWhen: () => false, kind: "test.retention.migrating", version: 2, scope: "session", @@ -538,12 +545,14 @@ describe("document cache retention", () => { it("keeps a reloaded newer version cached while an observer of the older one is attached", async () => { const V1 = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.versioned", version: 1, scope: "session", initial: () => ({ count: 3 }), }); const V2 = defineDoc({ + checkpointWhen: () => false, kind: "test.retention.versioned", version: 2, scope: "session", diff --git a/packages/durable/test/session-documents.test.ts b/packages/durable/test/session-documents.test.ts index 293778b02..b92f67776 100644 --- a/packages/durable/test/session-documents.test.ts +++ b/packages/durable/test/session-documents.test.ts @@ -14,6 +14,7 @@ type Live = { message?: string; items: string[]; nested: { count: number }; othe let liveInitCount = 0; const LiveDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.live", version: 1, scope: "conversation", @@ -26,6 +27,7 @@ const LiveDoc = defineDoc({ }); const RewindableLiveDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.live", version: 1, scope: "conversation", @@ -35,6 +37,7 @@ const RewindableLiveDoc = defineDoc({ }); const LiveDocV2 = defineDoc({ + checkpointWhen: () => false, kind: "test.live", version: 2, scope: "conversation", @@ -45,6 +48,7 @@ const LiveDocV2 = defineDoc({ type Counter = { count: number }; const CounterDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.counter", version: 1, scope: "session", @@ -54,6 +58,7 @@ const CounterDoc = defineDoc({ type Member = { seed: string; hits: number }; const seeds: string[] = []; const MemberDoc = defineDocFamily({ + checkpointWhen: () => false, kind: "test.member", version: 1, family: true, @@ -312,6 +317,7 @@ describe("Session document transactions", () => { const { session, storage } = openTestSession(); let initialized = 0; const LateDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.late", version: 1, scope: "session", @@ -381,6 +387,7 @@ describe("Session document transactions", () => { const { session, storage } = openTestSession(); const conversationId = await createConversation(session); const DateDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.date", version: 1, scope: "session", diff --git a/packages/durable/test/session-forks.test.ts b/packages/durable/test/session-forks.test.ts index cef7c7938..3c541b548 100644 --- a/packages/durable/test/session-forks.test.ts +++ b/packages/durable/test/session-forks.test.ts @@ -28,6 +28,7 @@ function documentCopies(writes: readonly StorageWrite[]) { describe("Session conversation document forks", () => { it("copies as-of and current singleton and family bases while leaving initial documents absent", async () => { const AsOf = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.policies.as-of", version: 1, scope: "conversation", @@ -36,6 +37,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "as-initial" }), }); const Current = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.policies.current", version: 1, scope: "conversation", @@ -44,6 +46,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "current-initial" }), }); const Initial = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.policies.initial", version: 1, scope: "conversation", @@ -52,6 +55,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "fresh" }), }); const AsOfFamily = defineDocFamily<{ value: string }, string>({ + checkpointWhen: () => false, kind: "fork.policies.as-of-family", version: 1, family: true, @@ -61,6 +65,7 @@ describe("Session conversation document forks", () => { initial: (seed) => ({ value: seed }), }); const CurrentFamily = defineDocFamily<{ value: string }, string>({ + checkpointWhen: () => false, kind: "fork.policies.current-family", version: 1, family: true, @@ -150,6 +155,7 @@ describe("Session conversation document forks", () => { it("uses final document state from the fork entry commit while excluding later same-commit entries", async () => { const Doc = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.same-commit", version: 1, scope: "conversation", @@ -178,6 +184,7 @@ describe("Session conversation document forks", () => { it("selects the entry-owning ancestor for as-of copies and the immediate parent for current copies", async () => { const AsOf = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.ancestry.as-of", version: 1, scope: "conversation", @@ -186,6 +193,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "initial" }), }); const Current = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.ancestry.current", version: 1, scope: "conversation", @@ -229,6 +237,7 @@ describe("Session conversation document forks", () => { it("copies the stored value and version without consulting migration definitions or migrated caches", async () => { const V1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "fork.stored-version", version: 1, scope: "conversation", @@ -238,6 +247,7 @@ describe("Session conversation document forks", () => { }); let migrations = 0; const V3 = defineDoc<{ count: number; migrated: boolean }>({ + checkpointWhen: () => false, kind: "fork.stored-version", version: 3, scope: "conversation", @@ -279,6 +289,7 @@ describe("Session conversation document forks", () => { it("coalesces a typed migration and override into the copied creation base", async () => { const V1 = defineDoc<{ count: number }>({ + checkpointWhen: () => false, kind: "fork.override", version: 1, scope: "conversation", @@ -337,6 +348,7 @@ describe("Session conversation document forks", () => { it("copies the incarnation alive at the fork point across retirement and recreation", async () => { const Doc = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.incarnations", version: 1, scope: "conversation", @@ -410,6 +422,7 @@ describe("Session conversation document forks", () => { it("rejects duplicate as-of and current selections for one child address before admission", async () => { const AsOf = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.duplicate-policy", version: 1, scope: "conversation", @@ -418,6 +431,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "as-of" }), }); const Current = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.duplicate-policy", version: 1, scope: "conversation", @@ -448,6 +462,7 @@ describe("Session conversation document forks", () => { it("rejects current and as-of source writes in the fork transaction", async () => { const Current = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.same-transaction-current", version: 1, scope: "conversation", @@ -456,6 +471,7 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "committed" }), }); const AsOf = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.same-transaction-as-of", version: 1, scope: "conversation", @@ -501,6 +517,7 @@ describe("Session conversation document forks", () => { it("rolls every copied base back when later pre-admission assembly fails", async () => { const Copied = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.rollback.copied", version: 1, scope: "conversation", @@ -551,6 +568,7 @@ describe("Session conversation document forks", () => { it("fails the Session on a rejected fork commit, which leaves no effect", async () => { const Doc = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.storage-rejected", version: 1, scope: "conversation", @@ -583,6 +601,7 @@ describe("Session conversation document forks", () => { it("retires a copied document and can recreate the address in the fork transaction", async () => { const Doc = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.retire-recreate", version: 1, scope: "conversation", @@ -612,6 +631,7 @@ describe("Session conversation document forks", () => { it("copies only conversation documents, leaving Session and task documents in their original scopes", async () => { const Copied = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.scope.conversation", version: 1, scope: "conversation", @@ -620,12 +640,14 @@ describe("Session conversation document forks", () => { initial: () => ({ value: "conversation" }), }); const SessionOnly = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.scope.session", version: 1, scope: "session", initial: () => ({ value: "session" }), }); const TaskOnly = defineDoc<{ value: string }>({ + checkpointWhen: () => false, kind: "fork.scope.task", version: 1, scope: "task", @@ -666,6 +688,7 @@ describe("Session conversation document forks", () => { it("copies every family member across storage scan pages", async () => { const Family = defineDocFamily<{ value: number }, number>({ + checkpointWhen: () => false, kind: "fork.pagination", version: 1, family: true, diff --git a/packages/durable/test/session-states.test.ts b/packages/durable/test/session-states.test.ts index 40edcd3ce..121912c79 100644 --- a/packages/durable/test/session-states.test.ts +++ b/packages/durable/test/session-states.test.ts @@ -5,6 +5,7 @@ import { context, createConversation, documentChanges, flush, openTestSession } type State = { value: number; retained: { label: string } }; const StateDoc = defineDoc({ + checkpointWhen: () => false, kind: "state.state", version: 1, scope: "session", @@ -12,6 +13,7 @@ const StateDoc = defineDoc({ }); const FamilyDoc = defineDocFamily<{ value: number }, string>({ + checkpointWhen: () => false, kind: "state.family", version: 1, family: true, @@ -131,6 +133,7 @@ describe("Session document states", () => { it("cold-loads a definition-free fork copy", async () => { const Copied = defineDoc<{ value: number }>({ + checkpointWhen: () => false, kind: "state.copied", version: 1, scope: "conversation", @@ -158,12 +161,14 @@ describe("Session document states", () => { it("hydrates a migrated tracker without writing and skips an equal version-base update", async () => { const Old = defineDoc<{ value: number }>({ + checkpointWhen: () => false, kind: "state.migration", version: 1, scope: "session", initial: () => ({ value: 3 }), }); const Current = defineDoc<{ value: number; migrated: boolean }>({ + checkpointWhen: () => false, kind: "state.migration", version: 2, scope: "session", diff --git a/packages/durable/test/session-tables.test.ts b/packages/durable/test/session-tables.test.ts index 2afb280ee..fe017504f 100644 --- a/packages/durable/test/session-tables.test.ts +++ b/packages/durable/test/session-tables.test.ts @@ -24,12 +24,14 @@ const WorkTask = defineTask<{ path: string }, Checkpoint, { ok: boolean }>({ type Progress = { lines: string[] }; const ProgressDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.progress", version: 1, scope: "task", initial: () => ({ lines: [] }), }); const StepDoc = defineDocFamily({ + checkpointWhen: () => false, kind: "test.step", version: 1, family: true, @@ -39,6 +41,7 @@ const StepDoc = defineDocFamily({ type Notes = { text: string }; const NotesDoc = defineDoc({ + checkpointWhen: () => false, kind: "test.notes", version: 1, scope: "conversation", diff --git a/packages/durable/test/session-watches.test.ts b/packages/durable/test/session-watches.test.ts index 3b03d1822..13e0ae46a 100644 --- a/packages/durable/test/session-watches.test.ts +++ b/packages/durable/test/session-watches.test.ts @@ -14,6 +14,7 @@ import { context, documentChanges, flush, openTestSession } from "./session-supp type State = { value: number; items: string[]; retained: { label: string } }; const StateDoc = defineDoc({ + checkpointWhen: () => false, kind: "watch.state", version: 1, scope: "session", @@ -430,12 +431,14 @@ describe("Session document watches", () => { it("hydrates migration without writing and observes the later exact edit", async () => { const Old = defineDoc<{ value: number }>({ + checkpointWhen: () => false, kind: "watch.migration", version: 1, scope: "session", initial: () => ({ value: 3 }), }); const Current = defineDoc<{ value: number; migrated: boolean }>({ + checkpointWhen: () => false, kind: "watch.migration", version: 2, scope: "session", diff --git a/packages/durable/test/spec-usage.test.ts b/packages/durable/test/spec-usage.test.ts index 7ed3478b5..489f48204 100644 --- a/packages/durable/test/spec-usage.test.ts +++ b/packages/durable/test/spec-usage.test.ts @@ -134,6 +134,7 @@ function examples() { // ─── Section 7.2: hooks reading extension state ────────────────────────────── const PlanModeDoc = defineDoc<{ enabled: boolean }>({ + checkpointWhen: () => false, kind: "app.plan-mode", version: 1, scope: "conversation", @@ -270,6 +271,7 @@ function examples() { // Absent: the conversation runs locally. Only conversations with this document run in a container. // Subagents do not copy it: their creator writes it too when they should run in the container. const ContainerDoc = defineDoc<{ image: string }>({ + checkpointWhen: () => false, kind: "app.container", version: 1, scope: "conversation", diff --git a/scripts/issues/triage.ts b/scripts/issues/triage.ts index 9142f1e1d..d803ff843 100644 --- a/scripts/issues/triage.ts +++ b/scripts/issues/triage.ts @@ -59,6 +59,7 @@ export type IssueState = { type TriageState = { inboxSince: string; issues: Record }; const StateDoc = defineDoc({ + checkpointWhen: (_value, _ops, { deltasSinceBase }) => deltasSinceBase >= 64, kind: "triage.state", version: 1, scope: "session",