@earendil-works/pi-durable 1.4.0 → 1.5.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 (221) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +181 -84
  3. package/dist/codemode/codemode.d.ts +4 -4
  4. package/dist/codemode/codemode.d.ts.map +1 -1
  5. package/dist/codemode/codemode.js +9 -12
  6. package/dist/codemode/codemode.js.map +1 -1
  7. package/dist/codemode/models.js +4 -4
  8. package/dist/codemode/models.js.map +1 -1
  9. package/dist/env/index.d.ts +12 -10
  10. package/dist/env/index.d.ts.map +1 -1
  11. package/dist/env/index.js.map +1 -1
  12. package/dist/env/node-watch.d.ts +3 -3
  13. package/dist/env/node-watch.d.ts.map +1 -1
  14. package/dist/env/node-watch.js +18 -16
  15. package/dist/env/node-watch.js.map +1 -1
  16. package/dist/env/node.d.ts +2 -2
  17. package/dist/env/node.d.ts.map +1 -1
  18. package/dist/env/node.js +1 -1
  19. package/dist/env/node.js.map +1 -1
  20. package/dist/errors.d.ts +18 -2
  21. package/dist/errors.d.ts.map +1 -1
  22. package/dist/errors.js +26 -3
  23. package/dist/errors.js.map +1 -1
  24. package/dist/harness/agent.d.ts.map +1 -1
  25. package/dist/harness/agent.js +10 -2
  26. package/dist/harness/agent.js.map +1 -1
  27. package/dist/harness/compaction.d.ts.map +1 -1
  28. package/dist/harness/compaction.js +19 -2
  29. package/dist/harness/compaction.js.map +1 -1
  30. package/dist/harness/context.d.ts +23 -7
  31. package/dist/harness/context.d.ts.map +1 -1
  32. package/dist/harness/context.js +71 -25
  33. package/dist/harness/context.js.map +1 -1
  34. package/dist/harness/define.d.ts +7 -3
  35. package/dist/harness/define.d.ts.map +1 -1
  36. package/dist/harness/define.js +4 -0
  37. package/dist/harness/define.js.map +1 -1
  38. package/dist/harness/generation.d.ts +38 -5
  39. package/dist/harness/generation.d.ts.map +1 -1
  40. package/dist/harness/generation.js +445 -96
  41. package/dist/harness/generation.js.map +1 -1
  42. package/dist/harness/harness.d.ts +2 -0
  43. package/dist/harness/harness.d.ts.map +1 -1
  44. package/dist/harness/harness.js +87 -62
  45. package/dist/harness/harness.js.map +1 -1
  46. package/dist/harness/inbox.d.ts +5 -1
  47. package/dist/harness/inbox.d.ts.map +1 -1
  48. package/dist/harness/inbox.js +4 -1
  49. package/dist/harness/inbox.js.map +1 -1
  50. package/dist/harness/json.d.ts +20 -4
  51. package/dist/harness/json.d.ts.map +1 -1
  52. package/dist/harness/json.js +23 -5
  53. package/dist/harness/json.js.map +1 -1
  54. package/dist/harness/live.d.ts +122 -16
  55. package/dist/harness/live.d.ts.map +1 -1
  56. package/dist/harness/live.js +93 -12
  57. package/dist/harness/live.js.map +1 -1
  58. package/dist/harness/partial-changes.d.ts +54 -0
  59. package/dist/harness/partial-changes.d.ts.map +1 -0
  60. package/dist/harness/partial-changes.js +162 -0
  61. package/dist/harness/partial-changes.js.map +1 -0
  62. package/dist/harness/registry-reader.d.ts +11 -0
  63. package/dist/harness/registry-reader.d.ts.map +1 -0
  64. package/dist/harness/registry-reader.js +25 -0
  65. package/dist/harness/registry-reader.js.map +1 -0
  66. package/dist/harness/scheduler.d.ts +46 -19
  67. package/dist/harness/scheduler.d.ts.map +1 -1
  68. package/dist/harness/scheduler.js +973 -376
  69. package/dist/harness/scheduler.js.map +1 -1
  70. package/dist/harness/submissions.d.ts +17 -10
  71. package/dist/harness/submissions.d.ts.map +1 -1
  72. package/dist/harness/submissions.js +62 -25
  73. package/dist/harness/submissions.js.map +1 -1
  74. package/dist/harness/task-graph.d.ts +12 -9
  75. package/dist/harness/task-graph.d.ts.map +1 -1
  76. package/dist/harness/task-graph.js +38 -28
  77. package/dist/harness/task-graph.js.map +1 -1
  78. package/dist/harness/telemetry.d.ts +11 -11
  79. package/dist/harness/telemetry.d.ts.map +1 -1
  80. package/dist/harness/telemetry.js +7 -7
  81. package/dist/harness/telemetry.js.map +1 -1
  82. package/dist/harness/tool.d.ts +2 -0
  83. package/dist/harness/tool.d.ts.map +1 -1
  84. package/dist/harness/tool.js +171 -55
  85. package/dist/harness/tool.js.map +1 -1
  86. package/dist/harness/transcript.d.ts +72 -0
  87. package/dist/harness/transcript.d.ts.map +1 -0
  88. package/dist/harness/transcript.js +264 -0
  89. package/dist/harness/transcript.js.map +1 -0
  90. package/dist/harness/types.d.ts +218 -59
  91. package/dist/harness/types.d.ts.map +1 -1
  92. package/dist/harness/types.js.map +1 -1
  93. package/dist/harness/util.d.ts +3 -1
  94. package/dist/harness/util.d.ts.map +1 -1
  95. package/dist/harness/util.js +9 -0
  96. package/dist/harness/util.js.map +1 -1
  97. package/dist/harness/view.d.ts +43 -22
  98. package/dist/harness/view.d.ts.map +1 -1
  99. package/dist/harness/view.js +130 -71
  100. package/dist/harness/view.js.map +1 -1
  101. package/dist/index.d.ts +9 -7
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +4 -3
  104. package/dist/index.js.map +1 -1
  105. package/dist/search.d.ts +19 -3
  106. package/dist/search.d.ts.map +1 -1
  107. package/dist/search.js +107 -44
  108. package/dist/search.js.map +1 -1
  109. package/dist/session/cached-storage.d.ts +71 -0
  110. package/dist/session/cached-storage.d.ts.map +1 -0
  111. package/dist/session/cached-storage.js +269 -0
  112. package/dist/session/cached-storage.js.map +1 -0
  113. package/dist/session/conversation-tails.d.ts +33 -0
  114. package/dist/session/conversation-tails.d.ts.map +1 -0
  115. package/dist/session/conversation-tails.js +101 -0
  116. package/dist/session/conversation-tails.js.map +1 -0
  117. package/dist/session/keys.d.ts +68 -0
  118. package/dist/session/keys.d.ts.map +1 -0
  119. package/dist/session/keys.js +161 -0
  120. package/dist/session/keys.js.map +1 -0
  121. package/dist/session/session.d.ts +167 -26
  122. package/dist/session/session.d.ts.map +1 -1
  123. package/dist/session/session.js +1029 -171
  124. package/dist/session/session.js.map +1 -1
  125. package/dist/session/transaction.d.ts +81 -3
  126. package/dist/session/transaction.d.ts.map +1 -1
  127. package/dist/session/transaction.js +224 -37
  128. package/dist/session/transaction.js.map +1 -1
  129. package/dist/session/writer.d.ts +74 -0
  130. package/dist/session/writer.d.ts.map +1 -0
  131. package/dist/session/writer.js +294 -0
  132. package/dist/session/writer.js.map +1 -0
  133. package/dist/storage/indexeddb.d.ts +10 -2
  134. package/dist/storage/indexeddb.d.ts.map +1 -1
  135. package/dist/storage/indexeddb.js +91 -41
  136. package/dist/storage/indexeddb.js.map +1 -1
  137. package/dist/storage/jsonl/storage.d.ts +14 -2
  138. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  139. package/dist/storage/jsonl/storage.js +88 -24
  140. package/dist/storage/jsonl/storage.js.map +1 -1
  141. package/dist/storage/memory.d.ts +7 -2
  142. package/dist/storage/memory.d.ts.map +1 -1
  143. package/dist/storage/memory.js +28 -2
  144. package/dist/storage/memory.js.map +1 -1
  145. package/dist/storage/sqlite/storage.d.ts +14 -2
  146. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  147. package/dist/storage/sqlite/storage.js +110 -36
  148. package/dist/storage/sqlite/storage.js.map +1 -1
  149. package/dist/storage/store/commit.d.ts +33 -0
  150. package/dist/storage/store/commit.d.ts.map +1 -0
  151. package/dist/storage/store/commit.js +286 -0
  152. package/dist/storage/store/commit.js.map +1 -0
  153. package/dist/storage/store/documents.d.ts +43 -0
  154. package/dist/storage/store/documents.d.ts.map +1 -0
  155. package/dist/storage/store/documents.js +102 -0
  156. package/dist/storage/store/documents.js.map +1 -0
  157. package/dist/storage/store/index.d.ts +2 -0
  158. package/dist/storage/store/index.d.ts.map +1 -0
  159. package/dist/storage/store/index.js +2 -0
  160. package/dist/storage/store/index.js.map +1 -0
  161. package/dist/storage/store/queries.d.ts +49 -0
  162. package/dist/storage/store/queries.d.ts.map +1 -0
  163. package/dist/storage/store/queries.js +93 -0
  164. package/dist/storage/store/queries.js.map +1 -0
  165. package/dist/storage/store/storage.d.ts +59 -0
  166. package/dist/storage/store/storage.d.ts.map +1 -0
  167. package/dist/storage/store/storage.js +438 -0
  168. package/dist/storage/store/storage.js.map +1 -0
  169. package/dist/storage/store/tables.d.ts +54 -0
  170. package/dist/storage/store/tables.d.ts.map +1 -0
  171. package/dist/storage/store/tables.js +108 -0
  172. package/dist/storage/store/tables.js.map +1 -0
  173. package/dist/testing/commit-writes.d.ts +5 -0
  174. package/dist/testing/commit-writes.d.ts.map +1 -0
  175. package/dist/testing/commit-writes.js +12 -0
  176. package/dist/testing/commit-writes.js.map +1 -0
  177. package/dist/testing/env-conformance.js +7 -7
  178. package/dist/testing/env-conformance.js.map +1 -1
  179. package/dist/testing/index.d.ts +1 -0
  180. package/dist/testing/index.d.ts.map +1 -1
  181. package/dist/testing/index.js +1 -0
  182. package/dist/testing/index.js.map +1 -1
  183. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  184. package/dist/testing/storage-benchmark.js +18 -17
  185. package/dist/testing/storage-benchmark.js.map +1 -1
  186. package/dist/testing/storage-conformance.d.ts.map +1 -1
  187. package/dist/testing/storage-conformance.js +354 -69
  188. package/dist/testing/storage-conformance.js.map +1 -1
  189. package/dist/tool-search/tool-search.d.ts +6 -12
  190. package/dist/tool-search/tool-search.d.ts.map +1 -1
  191. package/dist/tool-search/tool-search.js +7 -7
  192. package/dist/tool-search/tool-search.js.map +1 -1
  193. package/dist/tools/bash.d.ts +12 -12
  194. package/dist/tools/bash.d.ts.map +1 -1
  195. package/dist/tools/bash.js +16 -15
  196. package/dist/tools/bash.js.map +1 -1
  197. package/dist/tools/edit.d.ts +7 -7
  198. package/dist/tools/edit.d.ts.map +1 -1
  199. package/dist/tools/edit.js +9 -9
  200. package/dist/tools/edit.js.map +1 -1
  201. package/dist/tools/index.d.ts +32 -32
  202. package/dist/tools/read.d.ts +6 -6
  203. package/dist/tools/read.d.ts.map +1 -1
  204. package/dist/tools/read.js +19 -8
  205. package/dist/tools/read.js.map +1 -1
  206. package/dist/tools/write.d.ts +4 -4
  207. package/dist/tools/write.d.ts.map +1 -1
  208. package/dist/tools/write.js +5 -5
  209. package/dist/tools/write.js.map +1 -1
  210. package/dist/types.d.ts +223 -77
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/types.js.map +1 -1
  213. package/package.json +19 -12
  214. package/dist/harness/events.d.ts +0 -186
  215. package/dist/harness/events.d.ts.map +0 -1
  216. package/dist/harness/events.js +0 -373
  217. package/dist/harness/events.js.map +0 -1
  218. package/dist/session/observation.d.ts +0 -42
  219. package/dist/session/observation.d.ts.map +0 -1
  220. package/dist/session/observation.js +0 -281
  221. package/dist/session/observation.js.map +0 -1
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AttachedReplicatedState, Context, Draft, JsonValue } from "@earendil-works/chord";
1
+ import type { Context, Draft, JsonValue, WatchCall, WatchEnd, WatchHandle } from "@earendil-works/chord";
2
2
  import type { Op } from "@earendil-works/chord/delta";
3
3
  import type { Message, Models } from "@earendil-works/pi-ai";
4
4
  import type { TelemetryCarrier } from "@earendil-works/pi-telemetry";
@@ -107,18 +107,25 @@ export type ReportTask = {
107
107
  readonly kind: string;
108
108
  readonly conversationId: ConversationId;
109
109
  };
110
- /** A host callback, by its `HarnessOptions` name; `env` is the environment builder, `models` a provider call. */
111
- export type HostCallback = "now" | "settings" | "registry" | "env" | "models" | "setTimer" | "onWake";
112
110
  /**
113
- * What a report to `HarnessOptions.onReport` is about. `extension`, where present, names the extension whose code
114
- * failed, when known; for a task's own code, the one that installed the definition the task ran.
115
- * - `session_failed`: the error failed the Session, which closes: a Storage error, often a platform reset.
111
+ * A host callback, by its `HarnessOptions` name; `env` is the environment builder, `models` a provider call, and
112
+ * `telemetry` a telemetry adapter call made while a commit is published, which must not fail it.
113
+ */
114
+ export type HostCallback = "now" | "settings" | "registry" | "env" | "models" | "setTimer" | "onWake" | "telemetry";
115
+ /**
116
+ * What a report to `HarnessOptions.onReport` is about, by `kind` (`Report` adds the error). `extension`, where present,
117
+ * names the extension whose code failed, when known; for a task's own code, the one that installed the definition the
118
+ * task ran.
119
+ * - `storage_failed`: the error failed the Session, which closes: a Storage error, often a platform reset.
116
120
  * - `task_faulted`: the task ended `faulted`: its phase threw (`reason: "threw"`), or its abort handler was cut off
117
- * `settings.restartLimit` times (`reason: "restart_limit"`).
118
- * - `task_parked`: the restart limit parked the task until `Harness.retryTask()`.
121
+ * `settings.restartLimit` times (`reason: "restart_limit"`), as was a phase of a task of an extension's kind.
122
+ * - `task_interrupted`: crashes ended the task: it ended itself after being cut off `settings.restartLimit` times in a
123
+ * row (`reason: "restart_limit"`; a tool call, a compaction, a generation), or a generation's run ended because the
124
+ * host kept stopping while the model answered (`reason: "interrupted"`). `restarts` is the task's count when it ended.
119
125
  * - `migration_failed`: the task's stored state could not be migrated; it is blocked until the registry changes.
120
126
  * - `task_outdated`: a running task keeps running under its old definition, which was uninstalled (`missing_task`)
121
- * or replaced by one that cannot take over its state (`incompatible_task`).
127
+ * or replaced by one that cannot take over its state: older than the task (`task_too_old`), or newer without a
128
+ * `migrate` (`migration_failed`).
122
129
  * - `task_error`: the task's own `runtime.report()`, or a failure in running it.
123
130
  * - `extension_error`: a hook, a section, a wrapper, or a tool's invalid structured output; with the task that ran it,
124
131
  * or, outside a task, the conversation whose agent was resolved.
@@ -126,47 +133,53 @@ export type HostCallback = "now" | "settings" | "registry" | "env" | "models" |
126
133
  * - `listener_error`: a commit, close, watch, view, or document listener threw.
127
134
  */
128
135
  export type ReportInfo = {
129
- readonly type: "session_failed";
136
+ readonly kind: "storage_failed";
130
137
  } | {
131
- readonly type: "task_faulted";
138
+ readonly kind: "task_faulted";
132
139
  readonly task: ReportTask;
133
140
  readonly extension?: string;
134
141
  readonly reason: "threw" | "restart_limit";
135
142
  } | {
136
- readonly type: "task_parked";
143
+ readonly kind: "task_interrupted";
137
144
  readonly task: ReportTask;
138
145
  readonly extension?: string;
146
+ readonly reason: "restart_limit" | "interrupted";
147
+ readonly restarts: number;
139
148
  } | {
140
- readonly type: "migration_failed";
149
+ readonly kind: "migration_failed";
141
150
  readonly task: ReportTask;
142
151
  readonly extension?: string;
143
152
  } | {
144
- readonly type: "task_outdated";
153
+ readonly kind: "task_outdated";
145
154
  readonly task: ReportTask;
146
155
  readonly extension?: string;
147
- readonly reason: "missing_task" | "incompatible_task";
156
+ readonly reason: "missing_task" | "task_too_old" | "migration_failed";
148
157
  } | {
149
- readonly type: "task_error";
158
+ readonly kind: "task_error";
150
159
  readonly task: ReportTask;
151
160
  readonly extension?: string;
152
161
  } | {
153
- readonly type: "extension_error";
162
+ readonly kind: "extension_error";
154
163
  readonly extension?: string;
155
164
  readonly task?: ReportTask;
156
165
  readonly conversationId?: ConversationId;
157
166
  } | {
158
- readonly type: "host_error";
167
+ readonly kind: "host_error";
159
168
  readonly callback: HostCallback;
160
169
  readonly task?: ReportTask;
161
170
  } | {
162
- readonly type: "listener_error";
171
+ readonly kind: "listener_error";
172
+ };
173
+ /** One report to `HarnessOptions.onReport`: what it is about (`ReportInfo`, by `kind`) and the `error` reported. */
174
+ export type Report = ReportInfo & {
175
+ readonly error: unknown;
163
176
  };
164
177
  /** Whose code a task's `runtime.report()` is about when not the task's own: an extension's, or a host callback. */
165
178
  export type ReportFrom = {
166
- readonly type: "extension";
179
+ readonly kind: "extension";
167
180
  readonly extension?: string;
168
181
  } | {
169
- readonly type: "host";
182
+ readonly kind: "host";
170
183
  readonly callback: HostCallback;
171
184
  };
172
185
  /**
@@ -233,9 +246,11 @@ export interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver
233
246
  * Commit on the Session line after rereading the task. Rejects when the task is terminal, the invocation ended, the
234
247
  * Harness is closing, or, in a run invocation, the task carries an abort mark. A returned state replaces the task's
235
248
  * state in the same commit; returning nothing leaves it unchanged. `tx.createTask()` defaults to the task's
236
- * conversation. Inside the callback read only through `tx`: `snapshot()`, `documentState()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
249
+ * conversation. Inside the callback read only through `tx`: `snapshot()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
250
+ * `options.persistence` may defer storing a commit that changes only documents (`CommitPersistence`); one that returns
251
+ * a state is immediate.
237
252
  */
238
- commit(change: (tx: Tx, current: RunningTask<I, S, R>) => NextTaskState<S, R> | undefined | Promise<NextTaskState<S, R> | undefined>, context: Context): Promise<void>;
253
+ commit(change: (tx: Tx, current: RunningTask<I, S, R>) => NextTaskState<S, R> | undefined | Promise<NextTaskState<S, R> | undefined>, context: Context, options?: CommitOptions): Promise<void>;
239
254
  /** Read a durable memo of this task. */
240
255
  memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
241
256
  /** Store `candidate` unless a memo already exists; return the durable winner. */
@@ -265,6 +280,12 @@ export interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver
265
280
  entry(id: EntryId, context: Context): Promise<EntryRecord | undefined>;
266
281
  /** Undefined when the entry is absent, not visible, or has another kind. */
267
282
  entry<D extends JsonValue>(token: Entry<D>, id: EntryId, context: Context): Promise<TypedEntry<D> | undefined>;
283
+ /**
284
+ * Committed raw history of the task's conversation, fork-aware, as `Conversation.entries()`: every entry, also those a
285
+ * compaction summary or reset left behind. Entries never change, so a range bounded by entry IDs reads the same
286
+ * before and after a compaction, and in a rerun after a crash.
287
+ */
288
+ entries(query: Omit<EntryQuery, "conversationId">, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<EntryRecord, Cursor>>;
268
289
  /** Committed raw active transcript and model context, optionally cut off at the visible entry `at`. */
269
290
  context(conversationId: ConversationId, context: Context, options?: {
270
291
  readonly at?: EntryId;
@@ -434,7 +455,7 @@ type SubmissionRecordBase = {
434
455
  /** Host-provided deduplication key, scoped to the conversation. */
435
456
  readonly requestId?: string;
436
457
  };
437
- /** Durable lifecycle of one admitted user input or passive entry write. */
458
+ /** Durable lifecycle of one admitted user input, passive entry write, or continue. */
438
459
  export type SubmissionRecord = (SubmissionRecordBase & {
439
460
  readonly type: "input";
440
461
  } & ({
@@ -488,6 +509,30 @@ export type SubmissionRecord = (SubmissionRecordBase & {
488
509
  readonly answer?: never;
489
510
  readonly reason: string;
490
511
  readonly detail?: JsonValue;
512
+ })) | (SubmissionRecordBase & {
513
+ /** A run started without a user message; it places no entry and is never queued. */
514
+ readonly type: "continue";
515
+ } & ({
516
+ /** Owned by an active run. */
517
+ readonly status: "placed";
518
+ readonly entry?: never;
519
+ readonly answer?: never;
520
+ readonly reason?: never;
521
+ readonly detail?: never;
522
+ } | {
523
+ /** The run answered. */
524
+ readonly status: "done";
525
+ readonly entry?: never;
526
+ readonly answer: EntryId;
527
+ readonly reason?: never;
528
+ readonly detail?: never;
529
+ } | {
530
+ /** The run ended without an answer. */
531
+ readonly status: "unanswered";
532
+ readonly entry?: never;
533
+ readonly answer?: never;
534
+ readonly reason: string;
535
+ readonly detail?: JsonValue;
491
536
  }));
492
537
  /** Terminal status staged for a submission; identity, type, and entry come from its current record. */
493
538
  export type SubmissionSettlement = {
@@ -619,8 +664,9 @@ type TaskRecordBase<I, R> = {
619
664
  readonly endedAt?: number;
620
665
  /**
621
666
  * How many times in a row a crash, eviction, or reset cut the task off while it ran, without the task changing its
622
- * state since; open counts them, and a close does not. At `HarnessSettings.restartLimit` the task is parked as blocked
623
- * until `Harness.retryTask()`.
667
+ * state since; open counts them, and a close does not. At `HarnessSettings.restartLimit` the task ends (the built-in
668
+ * tasks end themselves, visibly; the scheduler faults any other kind), and a generation reads it to charge an
669
+ * interrupted model request as a failed attempt.
624
670
  */
625
671
  readonly restarts?: number;
626
672
  };
@@ -795,20 +841,30 @@ export type StorageWrite = {
795
841
  readonly type: "document.retire";
796
842
  readonly id: DocumentId;
797
843
  };
798
- /** Committed change of one document incarnation. */
844
+ /**
845
+ * Committed change of one document incarnation: `type: "document"` with `kind: "change"` (a creation or an update) or
846
+ * `kind: "retire"` (this commit retired the incarnation), or a `document.copy`.
847
+ */
799
848
  export type DocumentCommitChange = {
800
849
  readonly type: "document";
850
+ readonly kind: "change";
801
851
  readonly record: DocumentRecord;
802
852
  /** Conversation owning the document; task documents derive it from their task record. Undefined only for Session documents. */
803
853
  readonly conversationId: ConversationId | undefined;
804
- /** Definition version of `value`; absent when this commit retired the incarnation. */
805
- readonly version: number | undefined;
806
- /** Exact adopted immutable revision, or `null` when this commit retired the incarnation. */
807
- readonly value: JsonObject | null;
808
- /** Exact adopted operations for an ordinary update; empty for creation and retirement. */
854
+ /** Definition version of `value`. */
855
+ readonly version: number;
856
+ /** Exact adopted immutable revision. */
857
+ readonly value: JsonObject;
858
+ /** Exact adopted operations for an ordinary update; empty for creation. */
809
859
  readonly ops: readonly Op[];
810
860
  } | {
811
- /** Definition-free child initialization; consumers hydrate through state or watch acquisition. */
861
+ readonly type: "document";
862
+ readonly kind: "retire";
863
+ readonly record: DocumentRecord;
864
+ /** Conversation owning the document; task documents derive it from their task record. Undefined only for Session documents. */
865
+ readonly conversationId: ConversationId | undefined;
866
+ } | {
867
+ /** Definition-free child initialization; consumers hydrate through watch acquisition. */
812
868
  readonly type: "document.copy";
813
869
  readonly record: DocumentRecord;
814
870
  readonly conversationId: ConversationId;
@@ -819,14 +875,20 @@ export type TableCommitChange = Extract<StorageWrite, {
819
875
  readonly type: "conversation" | "entry" | "task" | "submission";
820
876
  }>;
821
877
  export type CommitChange = TableCommitChange | DocumentCommitChange;
822
- /** Every immutable change from one successful Session commit. Change order is unspecified. */
878
+ /**
879
+ * Every immutable change one Session storage transaction stored, with its `seq`. Change order is unspecified. A commit
880
+ * held in memory (`CommitPersistence`) is published once it is stored: by the next commit that reads or writes its
881
+ * document, alone (`seq` of its own), or with that commit, whose document change then carries the held operations
882
+ * followed by its own.
883
+ */
823
884
  export type CommitPublication = {
824
885
  readonly seq: Seq;
825
886
  readonly changes: readonly CommitChange[];
826
887
  };
827
888
  /**
828
889
  * Transaction surface of one Session commit callback.
829
- * Table reads and creation results are trusted immutable values and may be shared with internal commit state.
890
+ * Table reads and creation results are trusted immutable values and may be shared with internal commit state; the
891
+ * Session may freeze them (its caches and context ranges keep them), so mutating one can throw.
830
892
  */
831
893
  export interface Tx {
832
894
  conversation(id: ConversationId): Promise<ConversationRecord | undefined>;
@@ -840,6 +902,8 @@ export interface Tx {
840
902
  latestHeadMarker(conversationId: ConversationId): Promise<(EntryRecord & {
841
903
  readonly head: EntryId;
842
904
  }) | undefined>;
905
+ /** ID of the newest entry visible from the conversation; `undefined` when it has none. */
906
+ latestEntryId(conversationId: ConversationId): Promise<EntryId | undefined>;
843
907
  scanTasks(query: TaskQuery, limit: number, cursor?: Cursor): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
844
908
  /** Committed submission with a conversation-scoped request ID. */
845
909
  submissionByRequest(conversationId: ConversationId, requestId: string): Promise<SubmissionRecord | undefined>;
@@ -864,7 +928,7 @@ export interface Tx {
864
928
  */
865
929
  createSubmission(create: SubmissionCreate): Promise<SubmissionRecord>;
866
930
  /**
867
- * Settle a queued or placed submission; only a placed input can be answered, and a settled submission stays
931
+ * Settle a queued or placed submission; only a placed input or continue can be answered, and a settled submission stays
868
932
  * unchanged. Resolved against this transaction's latest record of the submission, so it works after table writes.
869
933
  * Run tasks settle the inputs they answer.
870
934
  */
@@ -887,32 +951,53 @@ export interface Tx {
887
951
  retireDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string): Promise<void>;
888
952
  retireDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string): Promise<void>;
889
953
  }
890
- /** Disposable, read-only Chord state bound to one committed document incarnation. */
891
- export type DocumentState<T extends JsonObject> = AttachedReplicatedState<Readonly<T> | null>;
892
- /** Terminal result of one document watch. */
893
- export type WatchEnd = {
894
- readonly reason: "stopped" | "cancelled" | "session_closed" | "retired";
895
- } | {
896
- readonly reason: "listener_error";
897
- readonly error: Error;
898
- }
899
- /** The Session failed; `error` is what failed it, usually a storage error (see `SessionFailed`). */
900
- | {
901
- readonly reason: "session_failed";
902
- readonly error: unknown;
954
+ /**
955
+ * Watch of one document address (kind, scope, and key), from the incarnation found at acquisition on. A retirement is a
956
+ * call with `retired` set, the last value the watch was given and no ops (a change made in the retiring commit itself
957
+ * is not shown: that commit publishes the retirement only); the watch goes on, and a document created at the address
958
+ * later arrives as `[["r", value]]`, folded with what is pending. It ends `ended` when its scope's owner ends: a task
959
+ * document's watch once the retirement call of the commit that makes its task terminal returned, every watch at Session
960
+ * close (a pending retirement dropped); `failed` with the error that failed the Session.
961
+ */
962
+ export type DocumentWatch<T extends JsonObject> = WatchHandle<Readonly<T>>;
963
+ /**
964
+ * A synchronous observer of committed Durable state, for adapters such as a Chord source (`observeDoc()`,
965
+ * `Harness.observeConversation()`, `Harness.observeTaskGraph()`), with the shapes of Chord's watch: `advance` gets a
966
+ * `WatchCall`, `end` a `WatchEnd`. `advance` gets every change, unfolded, as memory adopts it: for a stored commit at its
967
+ * publication once Storage stored it, for a deferred one before it is stored. A view or the graph gets at most one call
968
+ * per commit, a document observer one per change at its address (two for a commit that retires and recreates it); the
969
+ * task graph also advances outside any commit's publication, on the Session line, when a registry change or a failed
970
+ * migration changes a
971
+ * `blocked` reason. Durable's failures are final, so every call is a `value` call; a failure ends the observer. Calls
972
+ * may come before the attaching promise resolves; their ops apply from `Observation.value`. `advance` must only record
973
+ * the call, synchronously and quickly, never await, call the Session, or throw. A throw is reported as
974
+ * `listener_error`, detaches the observer, and ends it `listener_error` with the error. `end` is called once: `ended`
975
+ * when the observed scope's owner ends (a task document's task, after its retirement change, inside that commit's
976
+ * `adopt`), `failed` with "session closed" when the Session closes (an adapter follows the next Session) or with the
977
+ * error that failed the Session; never after `detach()`.
978
+ */
979
+ export type Observer<T> = {
980
+ advance(call: WatchCall<T>, context: Context): void;
981
+ end(end: WatchEnd): void;
982
+ };
983
+ /**
984
+ * A synchronous observer of stored commits (`Session.observeCommits()`): `advance` gets each `CommitPublication`, `end`
985
+ * the Session's close (`failed` with "session closed", or with the error that failed the Session).
986
+ */
987
+ export type CommitObserver = {
988
+ advance(publication: CommitPublication, context: Context): void;
989
+ end(end: WatchEnd): void;
903
990
  };
904
- /** Serialized exact-frame observation of an immutable value with bounded pending delivery. */
905
- export interface WatchHandle<T> {
906
- /** Acquisition revision before start; latest delivered immutable revision afterward. */
991
+ /** An attached `CommitObserver`: how to detach it. Idempotent: no `advance` or `end` follows it. */
992
+ export type CommitObservation = {
993
+ detach(): void;
994
+ };
995
+ /** An attached `Observer`: the value at attachment, which every later `advance` starts from, and how to detach it. */
996
+ export type Observation<T> = {
907
997
  readonly value: T;
908
- /** Install the sole asynchronous listener. Never invokes it inline. */
909
- start(listener: (value: T, ops: readonly Op[], context: Context) => Promise<void>): void;
910
- /** Idempotently stop future callbacks and return this watch's terminal result. */
911
- stop(): Promise<WatchEnd>;
912
- /** Settle when the watch terminates; an already-running callback remains caller-owned. */
913
- readonly closed: Promise<WatchEnd>;
914
- }
915
- export type DocumentWatch<T extends JsonObject> = WatchHandle<Readonly<T> | null>;
998
+ /** Idempotent: no `advance` or `end` follows it, not even for the rest of a commit underway. */
999
+ detach(): void;
1000
+ };
916
1001
  /** Committed document reads. */
917
1002
  export type DocumentReader = Pick<Session, "snapshot" | "snapshotAsOf">;
918
1003
  /** Non-creating document watch acquisition shared by Session and later invocation APIs. */
@@ -924,19 +1009,43 @@ export interface DocumentObserver {
924
1009
  watchDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
925
1010
  watchDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
926
1011
  }
927
- /** Why a Session ended: `close()`, or a failed Storage call, whose error it carries. */
1012
+ /** Why a Session ended, by `kind`: `close()`, or a failed Storage call, whose error it carries. */
928
1013
  export type SessionEnd = {
929
- readonly reason: "closed";
1014
+ readonly kind: "closed";
930
1015
  } | {
931
- readonly reason: "failed";
1016
+ readonly kind: "failed";
932
1017
  readonly error: unknown;
933
1018
  };
1019
+ /**
1020
+ * When a commit reaches Storage. `immediate`, the default: before `commit()` resolves. `deferred`: when every write of
1021
+ * the commit changes an existing conversation document with `history: "latest"` and `fork: "initial"` (no creation,
1022
+ * migration, or retirement), the Session adopts it in memory, shows it to watches and views, and resolves, but stores it
1023
+ * later: with the next commit that reads (`tx.doc()`) or writes that document, before the document leaves memory or
1024
+ * retires, at `close()`, and at the latest about `withinMs` after the commit (default `settings.progress.persistWithinMs`,
1025
+ * 1,000 ms; `Infinity` waits for one of the others). A commit that writes anything else is immediate. A crash loses what
1026
+ * is held, so only state whose loss is acceptable, such as streamed progress, should be deferred, and nothing durable may
1027
+ * depend on it without reading it in a commit.
1028
+ */
1029
+ export type CommitPersistence = {
1030
+ readonly kind: "immediate";
1031
+ } | {
1032
+ readonly kind: "deferred";
1033
+ readonly withinMs?: number;
1034
+ };
1035
+ /** Options of one Session commit. */
1036
+ export type CommitOptions = {
1037
+ readonly persistence?: CommitPersistence;
1038
+ };
934
1039
  /** Owner of one mutation line, its records, and its tracked documents. */
935
1040
  export interface Session extends DocumentObserver {
936
1041
  /**
937
- * Run one atomic transaction on the Session mutation line. Inside the callback read only through `tx`: `snapshot()`, `documentState()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
1042
+ * Run one atomic transaction on the Session mutation line. Inside the callback read only through `tx`: `snapshot()`, `watchDoc()`, `getTask()`, and the other Session and runtime reads may queue on the line the callback holds, and then never resolve.
1043
+ * `options.persistence` may defer storing it (`CommitPersistence`). The callback may run while an earlier commit is
1044
+ * being stored: at its first read or write of what an unpublished commit wrote, it waits until that commit is stored
1045
+ * and published; code before that runs early, so act on what it computes only once the commit resolves. Resolves
1046
+ * once the commit is stored and published, after every earlier commit's outcome.
938
1047
  */
939
- commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
1048
+ commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context, options?: CommitOptions): Promise<T>;
940
1049
  /** Seal admission, settle admitted commits, then close storage. */
941
1050
  close(context: Context): Promise<void>;
942
1051
  /**
@@ -944,8 +1053,30 @@ export interface Session extends DocumentObserver {
944
1053
  * which closes the Session itself. A failed Session is reopened from Storage; nothing else is left to clean up.
945
1054
  */
946
1055
  readonly closed: Promise<SessionEnd>;
947
- /** Observe complete commits synchronously after adoption. The listener must not block or call Session APIs. */
948
- subscribeCommits(listener: (publication: CommitPublication, context: Context) => void): () => void;
1056
+ /**
1057
+ * Attach a synchronous `Observer` to one document address, atomically on the Session line, as `watchDoc()` acquires
1058
+ * a watch: the observation's value includes every commit before it, and `advance` gets every later one, retirements
1059
+ * and later incarnations included. `undefined` when the address is absent. `context` governs only the attachment:
1060
+ * cancelled while attaching, the promise rejects and nothing stays attached.
1061
+ */
1062
+ observeDoc<T extends JsonObject>(token: SessionDocToken<T>, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1063
+ observeDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1064
+ observeDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1065
+ observeDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1066
+ observeDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1067
+ observeDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, observer: Observer<Readonly<T>>, context: Context): Promise<Observation<Readonly<T>> | undefined>;
1068
+ /**
1069
+ * Observe stored commits, each with exactly what it stored, except that a document the commit retires is published
1070
+ * as its retirement only; a commit held in memory is published when it is stored (`CommitPublication`). Commits are
1071
+ * published in line order once Storage stored them, a macrotask apart; a publication queues one dispatch, which runs
1072
+ * a microtask later, before every wait the commit resolves and every abort it queues, and calls the observers
1073
+ * attached by then; a commit published while none was attached has no dispatch. There is no order relative to watch listeners. `advance` runs synchronously and must not block or
1074
+ * call Session APIs; a throw is reported as `listener_error`, detaches the observer, and ends it `listener_error`.
1075
+ * `end` is called once, when the Session closes (`ended`, after its last publication) or fails (`failed`), never
1076
+ * after `detach()`. Attaches synchronously; the observation has no `value`: commits are events, not a state, and the
1077
+ * Session does not know Storage's newest `seq` before its own first commit.
1078
+ */
1079
+ observeCommits(observer: CommitObserver): CommitObservation;
949
1080
  /**
950
1081
  * Observe close synchronously when it begins, also when a failed Storage call closes the Session. The listener must
951
1082
  * not block or call Session APIs.
@@ -957,15 +1088,14 @@ export interface Session extends DocumentObserver {
957
1088
  snapshot<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<Readonly<T> | undefined>;
958
1089
  snapshot<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<Readonly<T> | undefined>;
959
1090
  snapshot<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<Readonly<T> | undefined>;
960
- documentState<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentState<T> | undefined>;
961
- documentState<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<DocumentState<T> | undefined>;
962
- documentState<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<DocumentState<T> | undefined>;
963
- documentState<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentState<T> | undefined>;
964
- documentState<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
965
- documentState<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
966
1091
  snapshotAsOf<T extends JsonObject>(token: RewindableConversationDocToken<T>, conversationId: ConversationId, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
967
1092
  snapshotAsOf<T extends JsonObject, I extends JsonValue>(token: RewindableConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
968
1093
  }
1094
+ /** What `Storage.commit()` stored: the number of durable batches, and why the next one was refused, if one was. */
1095
+ export type CommitResult = {
1096
+ readonly stored: number;
1097
+ readonly refused?: unknown;
1098
+ };
969
1099
  /**
970
1100
  * Atomic persistence boundary for Session records.
971
1101
  *
@@ -981,12 +1111,28 @@ export interface Session extends DocumentObserver {
981
1111
  */
982
1112
  export interface Storage {
983
1113
  /**
984
- * Atomically persist one batch and return its sequence. Once resolved, later reads through this storage observe it.
1114
+ * Persist `batches` in order, each as one ordinary atomic commit with its own seq, checked against the state the
1115
+ * earlier ones left, in as few round trips as the backend can. Every batch has at least one write: a commit without
1116
+ * writes never reaches Storage, so the Session never sends an empty batch. Stops at the first batch Durable's own checks refuse
1117
+ * (global IDs, document actions, copy resolution): the batches before it are durable, it and the ones after are not.
1118
+ * Calls `receive(index, seq)` once per durable batch, in order, synchronously, after all of them are durable and
1119
+ * before the next commit starts. Resolves with the number of durable batches and the refusal, if any. Rejects for an
1120
+ * unknown outcome (connection lost, statement or commit failed); `receive` calls made before the rejection still
1121
+ * stand. `receive` is a durability receipt, not publication: the Session publishes later. Once it resolved, later
1122
+ * reads through this storage observe the stored batches.
1123
+ */
1124
+ commit(batches: readonly (readonly StorageWrite[])[], receive: (index: number, seq: Seq) => void, context: Context): Promise<CommitResult>;
1125
+ /**
1126
+ * Optional. Run `publish`, the Session's publication of one stored or held commit (its cache write-through,
1127
+ * adoption, and observers), inside this storage's own scope, such as a store's attached host batch, so what the
1128
+ * publication marks lands together. Call `publish` exactly once, now or soon after; the Session publishes the next
1129
+ * commit only once it ran. Without it, the Session publishes directly.
985
1130
  */
986
- commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
1131
+ publishing?(publish: () => void, context: Context): void;
987
1132
  /**
988
- * Return a fresh branded candidate from the Session-global numeric ID namespace, greater than every ID minted or stored
989
- * before, also across reopen: scans order by ID as creation order, and the Harness relies on it.
1133
+ * Return a fresh branded candidate from the Session-global numeric ID namespace, greater than every ID minted since
1134
+ * this storage was opened and every ID stored, also across reopen: scans order by ID as creation order, and the
1135
+ * Harness relies on it.
990
1136
  */
991
1137
  mintId<I extends Id<string>>(): Promise<I>;
992
1138
  /** Look up one conversation by exact ID. */