@earendil-works/pi-durable 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +128 -3
  3. package/dist/codemode/codemode.d.ts.map +1 -1
  4. package/dist/codemode/codemode.js +12 -4
  5. package/dist/codemode/codemode.js.map +1 -1
  6. package/dist/codemode/models.d.ts.map +1 -1
  7. package/dist/codemode/models.js +2 -0
  8. package/dist/codemode/models.js.map +1 -1
  9. package/dist/documents.d.ts +1 -1
  10. package/dist/documents.d.ts.map +1 -1
  11. package/dist/documents.js.map +1 -1
  12. package/dist/env/index.d.ts +1 -0
  13. package/dist/env/index.d.ts.map +1 -1
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/node.d.ts.map +1 -1
  16. package/dist/env/node.js +11 -3
  17. package/dist/env/node.js.map +1 -1
  18. package/dist/harness/agent.d.ts +13 -2
  19. package/dist/harness/agent.d.ts.map +1 -1
  20. package/dist/harness/agent.js +39 -5
  21. package/dist/harness/agent.js.map +1 -1
  22. package/dist/harness/compaction.d.ts +3 -1
  23. package/dist/harness/compaction.d.ts.map +1 -1
  24. package/dist/harness/compaction.js +19 -4
  25. package/dist/harness/compaction.js.map +1 -1
  26. package/dist/harness/context.d.ts +5 -0
  27. package/dist/harness/context.d.ts.map +1 -1
  28. package/dist/harness/context.js +46 -12
  29. package/dist/harness/context.js.map +1 -1
  30. package/dist/harness/generation.d.ts.map +1 -1
  31. package/dist/harness/generation.js +67 -27
  32. package/dist/harness/generation.js.map +1 -1
  33. package/dist/harness/harness.d.ts +5 -2
  34. package/dist/harness/harness.d.ts.map +1 -1
  35. package/dist/harness/harness.js +78 -13
  36. package/dist/harness/harness.js.map +1 -1
  37. package/dist/harness/inbox.d.ts +2 -2
  38. package/dist/harness/inbox.d.ts.map +1 -1
  39. package/dist/harness/inbox.js +3 -3
  40. package/dist/harness/inbox.js.map +1 -1
  41. package/dist/harness/prompt.d.ts +1 -1
  42. package/dist/harness/prompt.d.ts.map +1 -1
  43. package/dist/harness/prompt.js +1 -1
  44. package/dist/harness/prompt.js.map +1 -1
  45. package/dist/harness/scheduler.d.ts +55 -10
  46. package/dist/harness/scheduler.d.ts.map +1 -1
  47. package/dist/harness/scheduler.js +617 -124
  48. package/dist/harness/scheduler.js.map +1 -1
  49. package/dist/harness/submissions.d.ts +7 -2
  50. package/dist/harness/submissions.d.ts.map +1 -1
  51. package/dist/harness/submissions.js +48 -17
  52. package/dist/harness/submissions.js.map +1 -1
  53. package/dist/harness/task-graph.d.ts +18 -1
  54. package/dist/harness/task-graph.d.ts.map +1 -1
  55. package/dist/harness/task-graph.js +50 -11
  56. package/dist/harness/task-graph.js.map +1 -1
  57. package/dist/harness/telemetry.d.ts +307 -0
  58. package/dist/harness/telemetry.d.ts.map +1 -0
  59. package/dist/harness/telemetry.js +136 -0
  60. package/dist/harness/telemetry.js.map +1 -0
  61. package/dist/harness/tool.d.ts.map +1 -1
  62. package/dist/harness/tool.js +54 -8
  63. package/dist/harness/tool.js.map +1 -1
  64. package/dist/harness/types.d.ts +110 -15
  65. package/dist/harness/types.d.ts.map +1 -1
  66. package/dist/harness/types.js.map +1 -1
  67. package/dist/harness/util.d.ts +4 -0
  68. package/dist/harness/util.d.ts.map +1 -1
  69. package/dist/harness/util.js +4 -0
  70. package/dist/harness/util.js.map +1 -1
  71. package/dist/index.d.ts +6 -4
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +3 -2
  74. package/dist/index.js.map +1 -1
  75. package/dist/session/session.d.ts +17 -6
  76. package/dist/session/session.d.ts.map +1 -1
  77. package/dist/session/session.js +64 -32
  78. package/dist/session/session.js.map +1 -1
  79. package/dist/session/transaction.d.ts +5 -1
  80. package/dist/session/transaction.d.ts.map +1 -1
  81. package/dist/session/transaction.js +26 -1
  82. package/dist/session/transaction.js.map +1 -1
  83. package/dist/storage/indexeddb.d.ts +60 -0
  84. package/dist/storage/indexeddb.d.ts.map +1 -0
  85. package/dist/storage/indexeddb.js +831 -0
  86. package/dist/storage/indexeddb.js.map +1 -0
  87. package/dist/storage/jsonl/storage.d.ts +16 -0
  88. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  89. package/dist/storage/memory.js +1 -1
  90. package/dist/storage/memory.js.map +1 -1
  91. package/dist/storage/sqlite/cloudflare.d.ts +6 -43
  92. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -1
  93. package/dist/storage/sqlite/cloudflare.js +6 -101
  94. package/dist/storage/sqlite/cloudflare.js.map +1 -1
  95. package/dist/storage/sqlite/database.d.ts +1 -36
  96. package/dist/storage/sqlite/database.d.ts.map +1 -1
  97. package/dist/storage/sqlite/database.js.map +1 -1
  98. package/dist/storage/sqlite/index.d.ts +2 -2
  99. package/dist/storage/sqlite/index.d.ts.map +1 -1
  100. package/dist/storage/sqlite/index.js.map +1 -1
  101. package/dist/storage/sqlite/migrations.d.ts +7 -6
  102. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  103. package/dist/storage/sqlite/migrations.js +116 -106
  104. package/dist/storage/sqlite/migrations.js.map +1 -1
  105. package/dist/storage/sqlite/node.d.ts +6 -39
  106. package/dist/storage/sqlite/node.d.ts.map +1 -1
  107. package/dist/storage/sqlite/node.js +8 -175
  108. package/dist/storage/sqlite/node.js.map +1 -1
  109. package/dist/storage/sqlite/schema.d.ts +45 -0
  110. package/dist/storage/sqlite/schema.d.ts.map +1 -0
  111. package/dist/storage/sqlite/schema.js +50 -0
  112. package/dist/storage/sqlite/schema.js.map +1 -0
  113. package/dist/storage/sqlite/storage.d.ts +10 -2
  114. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  115. package/dist/storage/sqlite/storage.js +46 -43
  116. package/dist/storage/sqlite/storage.js.map +1 -1
  117. package/dist/testing/storage-conformance.d.ts.map +1 -1
  118. package/dist/testing/storage-conformance.js +17 -0
  119. package/dist/testing/storage-conformance.js.map +1 -1
  120. package/dist/timers.d.ts +21 -0
  121. package/dist/timers.d.ts.map +1 -0
  122. package/dist/timers.js +72 -0
  123. package/dist/timers.js.map +1 -0
  124. package/dist/tools/image-processor.d.ts +4 -1
  125. package/dist/tools/image-processor.d.ts.map +1 -1
  126. package/dist/tools/image-processor.js +8 -18
  127. package/dist/tools/image-processor.js.map +1 -1
  128. package/dist/tools/read.d.ts.map +1 -1
  129. package/dist/tools/read.js +1 -1
  130. package/dist/tools/read.js.map +1 -1
  131. package/dist/truncate.d.ts +3 -2
  132. package/dist/truncate.d.ts.map +1 -1
  133. package/dist/truncate.js +4 -34
  134. package/dist/truncate.js.map +1 -1
  135. package/dist/types.d.ts +130 -9
  136. package/dist/types.d.ts.map +1 -1
  137. package/dist/types.js.map +1 -1
  138. package/package.json +15 -5
  139. package/dist/harness/output.d.ts +0 -69
  140. package/dist/harness/output.d.ts.map +0 -1
  141. package/dist/harness/output.js +0 -318
  142. package/dist/harness/output.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2026-10-21
4
+
5
+ ### Breaking Changes
6
+
7
+ - `TaskRuntime.sleep()` is removed: it waited inside a phase, which holds an invocation and, after a restart, reruns the phase and restarts the wait. A task now ends its phase asleep with `runtime.sleepUntil(until, checkpoint, context)` or by returning `{ status: "sleeping", until, checkpoint }` from `runtime.commit()`; move the work after the wait into the next phase and carry what it needs in the checkpoint. `TaskRuntime.setTimer()` paces work inside a phase without waiting for it.
8
+ - Tasks have a `sleeping` status, so code that handles every `TaskState`, `TaskGraphState`, or `TaskInspection` state needs a case for it.
9
+ - 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).
10
+ - `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.
11
+ - 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`.
12
+ - `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.
13
+
14
+ ### Added
15
+
16
+ - Added `IndexedDbStorage` (`@earendil-works/pi-durable/storage/indexeddb`), a browser storage backed by IndexedDB.
17
+ - `HarnessOptions.telemetry` emits the content-free `DURABLE_TELEMETRY_SCHEMA`: public submission admission spans, task phase spans (including extension tasks), committed entry and submission events, running phase-change checkpoints, and nested-tool replay events. Optional propagation stores opaque carriers on submissions and tasks so queued inputs and reopened tasks retain their trace; `TaskOptions.trace` accepts a carrier override or `"root"` (absence inherits the creator's carrier); creation-time `links` connect inputs sharing a run, while steer admissions link to the running trace. Internal admissions emit events without a submit span. Optional metrics record phase duration and live task counts. Failed phases record fixed error-status categories (`task_faulted`, `task_failed`, `task_orphaned`) and diagnostics capped at 500 characters; aborted phases keep status `ok`. Every `onReport` report also emits a fixed-field `pi.durable.report` event on its current span (dropped by the NOOP span when there is none), plus a `pi.durable.reports` counter by type; report telemetry contains no error payload and leaves the callback unchanged. Clock failures report without a phase context. Generation and compaction forward their phase context to pi-ai requests.
18
+ - `settings.compaction.modelOverrides` sets compaction thresholds per model, keyed by exact `"provider/modelId"`, so one harness can serve small- and large-window models; the threshold check, overflow recovery, and the compaction task use the entry of the model they act for ([#10942](https://github.com/earendil-works/pi/issues/10942)).
19
+ - Durable sleeps: a sleeping task holds no invocation, survives restarts, and resumes at its checkpoint once the Harness clock reaches `until`.
20
+ - `HarnessOptions.setTimer` arms the timers of the Session, the scheduler, and its tasks (progress throttles, idle-memory eviction, sleeps), also for `createSession()`; with `now`, they run on a host or fake clock. A foreground timer the host throws on or declines falls back to `setTimeout`. `ToolExecutionApi.now()` reads the Harness clock.
21
+ - `Harness.wake()` and `HarnessOptions.onWake` tell a host when the Session next needs a process: `active` while work can run, `at` for the earliest sleep. With `onWake`, the Harness keeps no timer for sleeps, so a Cloudflare Durable Object can hibernate and wake on its alarm, calling `resume()`.
22
+ - Crash-loop guard: open counts the restarts of each task that a crash, eviction, or reset cut off (`TaskRecord.restarts`); a close writes the tasks it stopped back as `pending` uncounted, and a new state the task commits starts the count over. At `settings.restartLimit` the task is parked: blocked with reason `restart_limit` in `inspect()` and the task graph, not active in `wake()`, and reported once. `Harness.retryTask(id)` runs it again. An abort handler cut off that many times ends its task `faulted`.
23
+ - `onReport(error, info)`: `info` (`ReportInfo`) says what happened in `type`: `session_failed`; `task_faulted`, `task_parked`, `migration_failed`, `task_outdated`, or `task_error` with the task and the extension that installed it; `extension_error` with the extension's name; `host_error` with the failing `HostCallback`; or `listener_error`.
24
+ - `settings.afterFailedRun`: a run that fails with `model_error` or `no_model` keeps the inputs queued behind it (`keep`, the default), starts the next run with them (`continue`), or settles them `unanswered` with reason `run_failed` (`withdraw`).
25
+ - The task graph shows why a task cannot run (`TaskGraphNode.blocked`: `missing_task`, `task_too_old`, `migration_failed`, or `restart_limit`) and follows registry changes and failed migrations.
26
+ - Added `HarnessOptions.identity` (a pi-ai `ClientIdentity`) to set the `User-Agent` and provider attribution of every model request through the Harness, including hooks and tools. `api.models` in tools and hooks is now a wrapper of the host's `Models` that adds the identity, not the same object.
27
+ - `HarnessOptions.models` and `HarnessOptions.settings` also accept a function returning the current value, like `env`. Generation, compaction, hooks and tools call it at each use (`api.models` reads the current value at each access), so a host can replace its model configuration or settings without reopening the Harness. A plain object still works.
28
+
29
+ ### Changed
30
+
31
+ - A task phase that faults is reported through `onReport` (as `task_faulted`), not only recorded in its outcome.
32
+ - `onReport` gets a second argument, `info`, and `TaskRuntime.report()` takes an optional `from` (see Added); an `onReport` written for one argument still works.
33
+ - Generation retry backoffs, deferred-response polls, and compaction retry backoffs sleep instead of waiting in a running phase, so a process can stop or hibernate during them. A record an earlier version stored mid-backoff sleeps again when it resumes before its time. A deferred poll whose model is gone after a reopen now fails `no_model` once the poll is due, not at once.
34
+ - Code mode measures script deadlines and wall time on the Harness clock (`HarnessOptions.now`) instead of `Date.now()`.
35
+ - SQLite drivers, portable interfaces, and the generic migration runner now come from `@earendil-works/chord/sqlite`. Existing Durable SQLite entry points keep re-exporting their public names; storage helpers and Durable schema migrations stay here.
36
+ - `ToolControl.terminate` is now typed `boolean` instead of `true`, matching `pi-agent-core` and `pi-coding-agent`; `false` does not end the run ([#10858](https://github.com/earendil-works/pi/issues/10858)).
37
+
38
+ ### Fixed
39
+
40
+ - In Cloudflare Workers with `nodejs_compat`, the timers that evict idle documents and contexts were armed, since timers there have an `unref()`; a pending one kept a Durable Object from hibernating and blocked its eviction. They are now declined in Workers, and memory is freed at later commits.
41
+ - The README's Errors section now limits the `StorageRequestError` exemption to reads: a `StorageRequestError` thrown by `commit()` or `mintId()` fails the Harness like any other storage error ([#11014](https://github.com/earendil-works/pi/issues/11014)).
42
+ - The Node environment now detects the legacy WSL `bash.exe` launcher from `%SystemRoot%` instead of assuming `C:\Windows` ([#9501](https://github.com/earendil-works/pi/pull/9501) by [@petrroll](https://github.com/petrroll)).
43
+
44
+ ## [1.3.0] - 2026-10-15
45
+
46
+ ### Added
47
+
48
+ - `SqliteStorage.open(database, { prefix })` can namespace every table and index; `openNodeSqliteStorage()` and `openDurableObjectSqliteStorage()` forward it, for databases shared with host tables.
49
+
50
+ ### Changed
51
+
52
+ - Cloudflare Durable Object SQLite adapters created for the same storage object now coordinate their operations, allowing multiple Pi storages to safely share one Durable Object database.
53
+
54
+ ### Fixed
55
+
56
+ - The system baseline written after a compaction now leads the model context when the kept history includes an assistant message, as does a first baseline written after earlier answers, instead of following the new input; Anthropic and OpenAI Codex requests sent it as a mid-conversation system message without an initial prompt or tool set. Other system messages after an assistant message stay in place ([#10904](https://github.com/earendil-works/pi/issues/10904)).
57
+ - `toBase64`, used by `read` and the Photon image processor, no longer takes about 140 MB of heap to encode a 3 MiB image, which exceeded the 128 MB memory limit of a Cloudflare Durable Object; it now builds a binary string 8 KiB at a time and encodes it with `btoa` ([#10923](https://github.com/earendil-works/pi/issues/10923)).
58
+
3
59
  ## [1.2.0] - 2026-10-12
4
60
 
5
61
  ### Breaking Changes
package/README.md CHANGED
@@ -75,7 +75,19 @@ What happened:
75
75
  - `submit()` durably admits your input and returns a `Submission`. A built-in generation task calls the model and appends the answer.
76
76
  - `wait()` resolves once the input is answered (`done`) or has failed (`unanswered`, with a reason).
77
77
 
78
- Every async call takes a Chord `Context`, which carries cancellation. `BACKGROUND_CONTEXT` never cancels. Cancelling a wait only cancels that wait, never the work.
78
+ Every async call takes a Chord `Context`, which carries cancellation and its current `TelemetrySpan`. `BACKGROUND_CONTEXT` never cancels and uses a NOOP span. The Harness owns the root adapter; adapters are never stored in a `Context`. Cancelling a wait only cancels that wait, never the work.
79
+
80
+ `HarnessOptions.identity` sets how model requests identify the app to providers (a pi-ai `ClientIdentity`), merged over `DEFAULT_HARNESS_IDENTITY` (`{ attribution: PI_ATTRIBUTION }`: OpenRouter, NVIDIA NIM, and OpenCode attribution headers name pi). It applies to generation, compaction, hooks, and tools, over the identity the host's `Models` was created with. Pass your app's attribution, for example `{ attribution: { name: "my-app", url: "https://my-app.example" } }`, or `{ attribution: null }` to send none.
81
+
82
+ `HarnessOptions.telemetry` accepts a host-owned `TelemetryContext` from `@earendil-works/pi-telemetry`, with optional `propagation` and `metrics` capabilities; omitted, it is a no-op. `DURABLE_TELEMETRY_SCHEMA`, exported from the package root, documents its typed spans, events, and metrics. Attributes contain IDs and control metadata, never messages, arguments, or results; IDs point to the transcript.
83
+
84
+ - `pi.durable.submit` covers the public submit call and its commit. Each input starts a new root trace with a link to its caller's current span; a write is a child of its caller. Duplicate `requestId` preserves the original submission and carrier. Internal admissions, such as compaction writes, emit events on their phase span without a submit span.
85
+ - `pi.durable.phase` covers one run or abort phase of every task, including its decision commit, never the next phase, a durable wait, or a restart. Its attributes identify the task, phase, attempt when present, `resumed`, and outcome. Tool phases also identify the tool and call. Nested tasks and background compactions inherit the creating phase; model requests receive that phase context. Hooks add no spans.
86
+ - Failed phase outcomes set error status with a fixed category (`task_faulted`, `task_failed`, or `task_orphaned`) and the outcome's diagnostic message, truncated to 500 characters (the reason for an orphaned task). Diagnostics can contain caller content; no error detail or exception name is recorded. An `aborted` outcome keeps status `ok`, including interruption by close or cancellation.
87
+ - Every `onReport` report also emits a `pi.durable.report` event on its current span. Without a span, the NOOP span drops the event; no root report span is created. Only `type`, optional fixed `reason`, `extension`, and `task.id` are recorded; no error payload is included. `pi.durable.reports` counts all reports by `type` only, including those without a span. Clock failures report without a phase context. The `onReport` callback is unchanged.
88
+ - Committed entries and settled submissions emit `pi.durable.entry` and `pi.durable.submission`. Committed running phase changes emit `pi.durable.checkpoint`; a nested call answered from its stored result emits `pi.durable.replay`.
89
+ - Propagation stores optional opaque `trace` carriers in submission and task JSON without a storage migration. Phase spans extract their task's stored carrier, or start at the adapter root when absent or unknown. `TaskOptions.trace` inherits the creator's carrier when absent, accepts an explicit carrier override, or uses `"root"` to start at the adapter root without storing a carrier. A run started for several queued inputs takes the first input's trace; its first invocation's phase spans link to the other inputs through immutable task `links`. A steer stays in the running trace and its submit span links to that run's stored carrier. A reopened task's first invocation has `resumed: true` in the same trace.
90
+ - Metrics record `pi.durable.phase.duration` (milliseconds; task, phase, outcome) and `pi.durable.tasks.active` (live tasks; task name), without record IDs. Closing the Harness releases its active counts.
79
91
 
80
92
  ## Concepts
81
93
 
@@ -125,6 +137,90 @@ const again = await root.submit({ type: "input", content: "Hello", requestId: "g
125
137
 
126
138
  `harness.submission(id)` reacquires a submission by ID, for example to wait for it after a restart.
127
139
 
140
+ ### Sleeping and Waking Up
141
+
142
+ A task waits for time by ending its phase asleep. The sleep is stored, so the task holds no invocation meanwhile, survives restarts, and resumes at the checkpoint once the Harness clock reaches `until`:
143
+
144
+ ```typescript
145
+ const Reminder = defineTask<{ text: string; at: number }, { phase: "wait" } | { phase: "deliver" }, null>({
146
+ name: "app.reminder",
147
+ version: 1,
148
+ initial: () => ({ phase: "wait" }),
149
+ phases: {
150
+ // Resolves once the sleep is stored; the phase ends there.
151
+ wait: (task, runtime, context) => runtime.sleepUntil(task.input.at, { phase: "deliver" }, context),
152
+ deliver: async (task, runtime, context) => {
153
+ await notify(task.input.text);
154
+ await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), context);
155
+ },
156
+ },
157
+ abort: (_task, runtime, context) =>
158
+ runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), context),
159
+ });
160
+ ```
161
+
162
+ There is no sleep in the middle of a phase: a phase reruns from its top after a restart, so work after the wait goes in the next phase, and what it needs goes in the checkpoint. `{ status: "sleeping", until, checkpoint }` returned from `runtime.commit()` is the same sleep, committed together with other writes. Retry backoffs and deferred-response polls of the built-in tasks sleep the same way.
163
+
164
+ The timers of the Session, the scheduler, and its tasks go through `setTimer` (default `setTimeout`), and their times through `now` (default `Date.now`); pass both to run a Harness on a fake clock. The Node.js environment, file watcher, and image worker keep their own timers. Sleep deadlines are stored on `now`, so every process that opens one storage must use the same clock. While scheduling runs, a timer resumes sleeping tasks when they are due and keeps a Node.js process alive meanwhile.
165
+
166
+ A host whose process can be evicted, such as a Cloudflare Durable Object, wakes it itself. With `onWake`, the Harness arms no timer for sleeps; it tells the host what `harness.wake()` says whenever that changes: `active` while some task can run now or is running, and `at`, the earliest sleep. The host keeps its alarm armed and calls `resume()` when it fires:
167
+
168
+ ```typescript
169
+ export class Session extends DurableObject<Env> {
170
+ #harness!: Harness;
171
+
172
+ constructor(ctx: DurableObjectState, env: Env) {
173
+ super(ctx, env);
174
+ void ctx.blockConcurrencyWhile(async () => {
175
+ // A fallback before anything can throw: an object whose open or recovery fails is started again.
176
+ const fallback = Date.now() + 60_000;
177
+ const current = await ctx.storage.getAlarm();
178
+ if (current === null || current > fallback) await ctx.storage.setAlarm(fallback);
179
+ const storage = await openDurableObjectSqliteStorage(ctx.storage);
180
+ const onWake = (wake: Wake) =>
181
+ // A failed alarm write resets the object; it arms again when it next starts.
182
+ void this.#arm(wake).catch(() => ctx.abort("Alarm write failed"));
183
+ this.#harness = await Harness.open(storage, { models, registry, onWake }, context);
184
+ // Armed before `resume()` runs anything, as for any call that can start work.
185
+ await this.#arm(this.#harness.wake());
186
+ this.#harness.resume(); // work a lost instance left, and due sleeps
187
+ });
188
+ }
189
+
190
+ override async alarm(): Promise<void> {
191
+ await this.#arm({ ...this.#harness.wake(), active: true });
192
+ this.#harness.resume(); // due sleeps and work a lost instance left
193
+ }
194
+
195
+ // Arm before any call that can start work (submit, compact, abortTask, abort, a registry change): `onWake` runs
196
+ // after the commit. An early alarm is harmless, a missing one strands the work.
197
+ async submit(conversation: Conversation, input: string, requestId: string): Promise<void> {
198
+ await this.#arm({ ...this.#harness.wake(), active: true });
199
+ await conversation.submit({ type: "input", content: input, requestId }, context);
200
+ }
201
+
202
+ async #arm(wake: Wake): Promise<void> {
203
+ // A recovery heartbeat while work runs, never later than the earliest sleep.
204
+ const heartbeat = wake.active ? Date.now() + 60_000 : undefined;
205
+ const at = heartbeat === undefined || wake.at === undefined ? (heartbeat ?? wake.at) : Math.min(heartbeat, wake.at);
206
+ if (at === undefined) await this.ctx.storage.deleteAlarm();
207
+ else await this.ctx.storage.setAlarm(at);
208
+ }
209
+ }
210
+ ```
211
+
212
+ The default `setTimer` declines the timers that only free memory in Workers, so no pending timer keeps the object from hibernating or being evicted; memory is then freed at later commits.
213
+
214
+ ### Failures and Recovery
215
+
216
+ **Crash loops.** Work that deterministically crashes its process, by exhausting memory or CPU before its next checkpoint, would otherwise be resumed and crash again forever. Open counts each task a crash, eviction, or reset cut off; a close is not counted, and a new state the task commits starts the count over. Once the count reaches `settings.restartLimit` (default 5; `Infinity` never parks), the task is parked: `inspect()` and the task graph show it as blocked with reason `restart_limit`, `wake()` does not count it as active, and `onReport` gets it once as `task_parked`. Every task running when a process dies is counted, so show parked tasks and offer `harness.retryTask(id)`, which runs one again. `abortTask()` still runs its abort handler; an abort handler cut off that many times ends the task `faulted`.
217
+
218
+ **Reports.** `onReport(error, info)` says what happened in `info.type`: `session_failed` for the error that failed the Harness (a Storage error, often a platform reset); `task_faulted`, `task_parked`, `migration_failed`, `task_outdated`, or `task_error` for a task, with `info.task` (its `id`, `kind`, and `conversationId`) and the extension that installed it in `info.extension`; `extension_error` for hooks, sections, wrappers, and tools, with the extension's name; `host_error` for a host callback, named in `info.callback`; and `listener_error`. A host can feed `task_faulted` reports of one kind to its own breaker.
219
+
220
+ **Queued inputs after a failed run.** A run that fails with a model error leaves the inputs queued behind it waiting until the next input arrives. `settings.afterFailedRun` changes that: `continue` starts the next run with them, as after an answer, and `withdraw` settles them `unanswered` with reason `run_failed`.
221
+
222
+ **Blocked tasks.** Tasks blocked on a missing or unmigratable definition show their reason in the task graph too, and a registry change that unblocks them makes `onWake` report the Session active, so a host need not poll them.
223
+
128
224
  ## Extensions
129
225
 
130
226
  Code the Harness runs, other than its built-in tasks, comes in named extensions installed in a registry your process owns:
@@ -319,7 +415,7 @@ const harness = await Harness.open(storage, {
319
415
  compaction: { reserveTokens: 16384 },
320
416
  progress: { partialIntervalMs: 100, outputIntervalMs: 100 },
321
417
  toolExecution: "parallel",
322
- contextRetentionMs: 600_000, // idle conversations keep their context in memory this long (checked lazily where timers cannot be unreferenced)
418
+ contextRetentionMs: 600_000, // idle conversations keep their context in memory this long (checked lazily where background timers are declined, as in Workers)
323
419
  documentRetentionMs: 600_000, // loaded documents nothing observes stay in memory this long after their last use; observed ones always
324
420
  get followUpMode() {
325
421
  return userSettings.followUpMode;
@@ -475,10 +571,16 @@ settings: {
475
571
  reserveTokens: 16384, // above contextWindow - reserveTokens, the next request waits for a compaction
476
572
  keepRecentTokens: 20000, // roughly how much recent context stays verbatim
477
573
  backgroundTokens: 32768, // this far below that, a compaction starts in the background; 0 disables it
574
+ modelOverrides: {
575
+ // exact "provider/modelId"; each field set here replaces the one above for that model
576
+ "local/small-32k": { reserveTokens: 4096, keepRecentTokens: 8000, backgroundTokens: 4096 },
577
+ },
478
578
  },
479
579
  }
480
580
  ```
481
581
 
582
+ The fixed token counts rarely suit a 32k and a 1M context window alike; `modelOverrides` lets one harness serve both. The conversation's current model picks the entry, at the threshold check, after a context overflow, and when a compaction selects what to summarize.
583
+
482
584
  When a provider rejects a request because the context is too long, generation compacts and retries once. A summary that would cut before the start of the current context settles as `stale` when it is placed, so when several are in flight, the furthest cut stays in effect. Summarization spend counts in `pi.usage`. A `beforeCompact` hook on `CompactionTask` can decline or supply its own summary.
483
585
 
484
586
  When a compaction that a generation waits for at the blocking threshold fails, the generation fails too, without sending a request: its inputs settle `unanswered` with `model_error` and detail `Compaction failed: <error>`. One that finds nothing to compact or is declined lets the request go ahead.
@@ -635,6 +737,7 @@ const Todos = defineDoc<{ items: string[] }>({
635
737
  history: "latest", // or "rewindable" to read old values with snapshotAsOf()
636
738
  fork: "initial", // what a fork starts with: "initial", "current", or "asOf"
637
739
  initial: () => ({ items: [] }),
740
+ checkpointWhen: () => false, // required: true stores this change as a complete base, bounding replay
638
741
  });
639
742
 
640
743
  await root.commit(async (tx) => {
@@ -658,10 +761,32 @@ const usage = await harness.usage(context); // { models: { "openai/gpt-6-sol": U
658
761
  | Backend | Import | Notes |
659
762
  |---|---|---|
660
763
  | Memory | `MemoryStorage` from the package root | Nothing is persisted. |
764
+ | IndexedDB | `IndexedDbStorage.open(name)` from `@earendil-works/pi-durable/storage/indexeddb` | Browser-only storage in one IndexedDB database. |
661
765
  | SQLite | `openNodeSqliteStorage(file)` from `@earendil-works/pi-durable/storage/sqlite/node` | One database file. WAL mode with `synchronous = NORMAL`: commits survive process crashes; the newest may be lost on power or host failure. |
662
766
  | JSONL | `openNodeJsonlStorage(directory, context)` from `@earendil-works/pi-durable/storage/jsonl/node` | Append-only files in one directory. Pass `{ fsync: true }` to flush before each commit marker. Keeps the whole history in memory too, so memory grows with it; use SQLite for long-running agents. |
663
767
  | Cloudflare Durable Object | `openDurableObjectSqliteStorage(ctx.storage)` from `@earendil-works/pi-durable/storage/sqlite/cloudflare` | The SQLite storage of one SQLite-backed Durable Object; commits are its storage transactions. |
664
768
 
769
+ In a browser, open IndexedDB storage with `await IndexedDbStorage.open(name)`. One owner must hold the database at a
770
+ time; use a [Web Lock](https://developer.mozilla.org/docs/Web/API/Web_Locks_API) around its lifetime rather than opening
771
+ it in competing tabs or workers. Call `await navigator.storage.persist()` when durable retention matters.
772
+
773
+ SQLite storage can namespace its tables and indexes when sharing a database: pass `{ prefix: "pi_" }` to
774
+ `SqliteStorage.open(database, options)`, `openNodeSqliteStorage(path, options)`, or
775
+ `openDurableObjectSqliteStorage(ctx.storage, options)`. Storage renders the prefixed names into its SQL, so adapters
776
+ need no support. A prefix must be a SQLite identifier and cannot begin with `sqlite_`; a host may reserve more, such
777
+ as `_cf_` in Durable Objects. Changing the prefix does not migrate existing data.
778
+
779
+ Multiple Pi storages opened through the Cloudflare adapter with the same storage object automatically share an
780
+ operation queue. To share the database with host tables, create an adapter and use it for both Pi and all host queries;
781
+ direct storage calls bypass the queue and may be rolled back with a Pi transaction:
782
+
783
+ ```typescript
784
+ const database = new DurableObjectSqliteDatabase(ctx.storage);
785
+ const piStorage = await SqliteStorage.open(database, { prefix: "pi_" });
786
+ await database.exec("CREATE TABLE IF NOT EXISTS host_notes (text TEXT)");
787
+ await database.run("INSERT INTO host_notes (text) VALUES (?)", text);
788
+ ```
789
+
665
790
  One process owns a storage at a time; there is no cross-process locking. The portable SQLite and JSONL cores (`/storage/sqlite`, `/storage/jsonl`) run without Node APIs, for example on Bun or in Cloudflare Durable Objects, given an asynchronous `SqliteDatabase` facade or a `FileSystem` from `@earendil-works/pi-durable/env`.
666
791
 
667
792
  SQLite adapters implement promise-based `exec`, `run`, `get`, `all`, `transaction`, and `close`. `run`, `get`, and `all` take SQL text plus positional bindings; adapters may cache prepared statements by SQL text. A transaction callback receives a transaction handle; all work in the transaction must use it, and the handle expires when the callback settles. Adapters must queue unrelated operations and other transactions until the transaction finishes, so calling `database` itself inside the callback never settles:
@@ -696,7 +821,7 @@ The package root loads TypeBox, because the tool task validates arguments with p
696
821
  Three rules decide what an error does:
697
822
 
698
823
  1. **A storage error ends the Harness.** The call that hit it rejects with the error. Everything after it, and every pending wait, rejects with `SessionFailed` (its `cause` is that error); watches and event streams end with `session_failed`; running tasks get their abort signal; `onReport` gets the error once. The Harness then closes itself. Nothing is retried; a custom storage retries its own transient failures. Reopen to recover: commits are atomic, so the store is consistent, and resubmitting with the same `requestId` finds a submission that was already admitted.
699
- 2. **An invalid request fails only that call:** a storage throws `StorageRequestError` for a bad cursor, an unknown conversation, or history a document does not keep. A read you cancelled fails only itself too.
824
+ 2. **An invalid read fails only that call:** a storage read throws `StorageRequestError` for a bad cursor, an unknown conversation, or history a document does not keep. A read you cancelled fails only itself too. The exemption covers reads only: any error from `commit()` or `mintId()`, `StorageRequestError` included, ends the Harness as in rule 1.
700
825
  3. **Your code fails only its own unit:** a throwing task phase faults that task, a throwing tool gets an error result, a throwing hook is reported and skipped (`beforeTool` blocks the call), and a throwing commit callback rolls back that commit. Listeners that throw, or return a rejected promise, are reported and the others still run. The exception is a host `RegistryReader` whose `snapshot()` throws: the Harness cannot schedule without it, so it fails the Harness like a storage error.
701
826
 
702
827
  `harness.closed` settles once the Harness has closed, by `close()` or after a storage error, so a host restarts on failure like this:
@@ -1 +1 @@
1
- {"version":3,"file":"codemode.d.ts","sourceRoot":"","sources":["../../src/codemode/codemode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"codemode.d.ts","sourceRoot":"","sources":["../../src/codemode/codemode.ts"],"names":[],"mappings":"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"}
@@ -57,12 +57,16 @@ export const CodemodeRunDoc = defineDoc({
57
57
  version: 1,
58
58
  scope: "task",
59
59
  initial: () => ({}),
60
+ // The document retires with its task.
61
+ checkpointWhen: () => false,
60
62
  });
61
63
  export const CodemodeJournalDoc = defineDoc({
62
64
  kind: "pi.codemode.journal",
63
65
  version: 1,
64
66
  scope: "task",
65
67
  initial: () => ({ entries: [] }),
68
+ // The document retires with its task.
69
+ checkpointWhen: () => false,
66
70
  });
67
71
  /**
68
72
  * What scripts keep with `store()` and read with `load()`, per conversation. Rewindable and copied as of the fork
@@ -75,6 +79,7 @@ export const CodemodeStoreDoc = defineDoc({
75
79
  history: "rewindable",
76
80
  fork: "asOf",
77
81
  initial: () => ({ values: {} }),
82
+ checkpointWhen: (_value, _ops, { deltasSinceBase }) => deltasSinceBase >= 64,
78
83
  });
79
84
  const ARGS_PREVIEW_CHARS = 200;
80
85
  const ERROR_PREVIEW_CHARS = 500;
@@ -244,7 +249,7 @@ class CodemodeCall {
244
249
  }
245
250
  entry.tools.push(tool.name);
246
251
  }
247
- const now = Date.now();
252
+ const now = api.now();
248
253
  const seed = crypto.getRandomValues(new Uint32Array(1))[0];
249
254
  const found = await api.commit(async (tx) => {
250
255
  const run = await tx.doc(CodemodeRunDoc, api.taskId);
@@ -288,7 +293,7 @@ class CodemodeCall {
288
293
  if (this.start.version !== CODEMODE_MODULE_VERSION) {
289
294
  return this.decide(errorResult(`The script cannot resume: it started under code mode version ${this.start.version}, this is ${CODEMODE_MODULE_VERSION}.`));
290
295
  }
291
- const remaining = this.start.deadline === undefined ? undefined : this.start.deadline - Date.now();
296
+ const remaining = this.start.deadline === undefined ? undefined : this.start.deadline - this.api.now();
292
297
  if (remaining !== undefined && remaining <= 0) {
293
298
  return this.decide(errorResult(`Script timed out: its deadline passed while it was interrupted.`));
294
299
  }
@@ -380,7 +385,7 @@ class CodemodeCall {
380
385
  const details = this.details();
381
386
  const formatted = await formatResult(result, {
382
387
  generatedImages: this.generatedImages,
383
- wallMs: Date.now() - this.start.startedAt,
388
+ wallMs: this.api.now() - this.start.startedAt,
384
389
  maxOutputTokens: this.start.maxOutputTokens,
385
390
  calls: details.calls,
386
391
  limits: OUTPUT_LIMITS,
@@ -645,7 +650,10 @@ class CodemodeCall {
645
650
  },
646
651
  }));
647
652
  running.then((done) => {
648
- this.queue.push({ id: call.id, at: Date.now(), ...done });
653
+ // A result after the outcome is decided has nowhere to go, and the call's API has ended.
654
+ if (this.stopped)
655
+ return;
656
+ this.queue.push({ id: call.id, at: this.api.now(), ...done });
649
657
  void this.pump();
650
658
  }, (error) => {
651
659
  // Stopped meanwhile: the outcome is decided, or a waiting model call gave up.