@earendil-works/pi-durable 0.87.0 → 0.99.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 (177) hide show
  1. package/README.md +72 -1
  2. package/dist/documents.d.ts +66 -0
  3. package/dist/documents.d.ts.map +1 -0
  4. package/dist/documents.js +98 -0
  5. package/dist/documents.js.map +1 -0
  6. package/dist/entries.d.ts +13 -0
  7. package/dist/entries.d.ts.map +1 -0
  8. package/dist/entries.js +18 -0
  9. package/dist/entries.js.map +1 -0
  10. package/dist/env/index.d.ts +131 -0
  11. package/dist/env/index.d.ts.map +1 -0
  12. package/dist/env/index.js +45 -0
  13. package/dist/env/index.js.map +1 -0
  14. package/dist/env/node.d.ts +45 -0
  15. package/dist/env/node.d.ts.map +1 -0
  16. package/dist/env/node.js +908 -0
  17. package/dist/env/node.js.map +1 -0
  18. package/dist/env/utils/adaptive-publisher.d.ts +24 -0
  19. package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
  20. package/dist/env/utils/adaptive-publisher.js +79 -0
  21. package/dist/env/utils/adaptive-publisher.js.map +1 -0
  22. package/dist/env/utils/output-capture.d.ts +31 -0
  23. package/dist/env/utils/output-capture.d.ts.map +1 -0
  24. package/dist/env/utils/output-capture.js +238 -0
  25. package/dist/env/utils/output-capture.js.map +1 -0
  26. package/dist/env/utils/truncate.d.ts +71 -0
  27. package/dist/env/utils/truncate.d.ts.map +1 -0
  28. package/dist/env/utils/truncate.js +294 -0
  29. package/dist/env/utils/truncate.js.map +1 -0
  30. package/dist/errors.d.ts +15 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +24 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/harness/config.d.ts +20 -0
  35. package/dist/harness/config.d.ts.map +1 -0
  36. package/dist/harness/config.js +18 -0
  37. package/dist/harness/config.js.map +1 -0
  38. package/dist/harness/context.d.ts +27 -0
  39. package/dist/harness/context.d.ts.map +1 -0
  40. package/dist/harness/context.js +109 -0
  41. package/dist/harness/context.js.map +1 -0
  42. package/dist/harness/generation.d.ts +36 -0
  43. package/dist/harness/generation.d.ts.map +1 -0
  44. package/dist/harness/generation.js +266 -0
  45. package/dist/harness/generation.js.map +1 -0
  46. package/dist/harness/harness.d.ts +10 -0
  47. package/dist/harness/harness.d.ts.map +1 -0
  48. package/dist/harness/harness.js +266 -0
  49. package/dist/harness/harness.js.map +1 -0
  50. package/dist/harness/live.d.ts +40 -0
  51. package/dist/harness/live.d.ts.map +1 -0
  52. package/dist/harness/live.js +41 -0
  53. package/dist/harness/live.js.map +1 -0
  54. package/dist/harness/prompt.d.ts +24 -0
  55. package/dist/harness/prompt.d.ts.map +1 -0
  56. package/dist/harness/prompt.js +88 -0
  57. package/dist/harness/prompt.js.map +1 -0
  58. package/dist/harness/registry.d.ts +7 -0
  59. package/dist/harness/registry.d.ts.map +1 -0
  60. package/dist/harness/registry.js +309 -0
  61. package/dist/harness/registry.js.map +1 -0
  62. package/dist/harness/scheduler.d.ts +63 -0
  63. package/dist/harness/scheduler.d.ts.map +1 -0
  64. package/dist/harness/scheduler.js +605 -0
  65. package/dist/harness/scheduler.js.map +1 -0
  66. package/dist/harness/submissions.d.ts +24 -0
  67. package/dist/harness/submissions.d.ts.map +1 -0
  68. package/dist/harness/submissions.js +133 -0
  69. package/dist/harness/submissions.js.map +1 -0
  70. package/dist/harness/types.d.ts +358 -0
  71. package/dist/harness/types.d.ts.map +1 -0
  72. package/dist/harness/types.js +2 -0
  73. package/dist/harness/types.js.map +1 -0
  74. package/dist/harness/util.d.ts +14 -0
  75. package/dist/harness/util.d.ts.map +1 -0
  76. package/dist/harness/util.js +55 -0
  77. package/dist/harness/util.js.map +1 -0
  78. package/dist/ids.d.ts +6 -0
  79. package/dist/ids.d.ts.map +1 -0
  80. package/dist/ids.js +9 -0
  81. package/dist/ids.js.map +1 -0
  82. package/dist/index.d.ts +13 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +11 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/forks.d.ts +10 -0
  87. package/dist/session/forks.d.ts.map +1 -0
  88. package/dist/session/forks.js +42 -0
  89. package/dist/session/forks.js.map +1 -0
  90. package/dist/session/observation.d.ts +28 -0
  91. package/dist/session/observation.d.ts.map +1 -0
  92. package/dist/session/observation.js +280 -0
  93. package/dist/session/observation.js.map +1 -0
  94. package/dist/session/session.d.ts +59 -0
  95. package/dist/session/session.d.ts.map +1 -0
  96. package/dist/session/session.js +380 -0
  97. package/dist/session/session.js.map +1 -0
  98. package/dist/session/transaction.d.ts +105 -0
  99. package/dist/session/transaction.d.ts.map +1 -0
  100. package/dist/session/transaction.js +718 -0
  101. package/dist/session/transaction.js.map +1 -0
  102. package/dist/storage/jsonl/index.d.ts +2 -0
  103. package/dist/storage/jsonl/index.d.ts.map +1 -0
  104. package/dist/storage/jsonl/index.js +2 -0
  105. package/dist/storage/jsonl/index.js.map +1 -0
  106. package/dist/storage/jsonl/node.d.ts +6 -0
  107. package/dist/storage/jsonl/node.d.ts.map +1 -0
  108. package/dist/storage/jsonl/node.js +8 -0
  109. package/dist/storage/jsonl/node.js.map +1 -0
  110. package/dist/storage/jsonl/storage.d.ts +115 -0
  111. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  112. package/dist/storage/jsonl/storage.js +681 -0
  113. package/dist/storage/jsonl/storage.js.map +1 -0
  114. package/dist/storage/memory.d.ts +60 -0
  115. package/dist/storage/memory.d.ts.map +1 -0
  116. package/dist/storage/memory.js +681 -0
  117. package/dist/storage/memory.js.map +1 -0
  118. package/dist/storage/sqlite/database.d.ts +30 -0
  119. package/dist/storage/sqlite/database.d.ts.map +1 -0
  120. package/dist/storage/sqlite/database.js +2 -0
  121. package/dist/storage/sqlite/database.js.map +1 -0
  122. package/dist/storage/sqlite/index.d.ts +4 -0
  123. package/dist/storage/sqlite/index.d.ts.map +1 -0
  124. package/dist/storage/sqlite/index.js +3 -0
  125. package/dist/storage/sqlite/index.js.map +1 -0
  126. package/dist/storage/sqlite/migrations.d.ts +11 -0
  127. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  128. package/dist/storage/sqlite/migrations.js +111 -0
  129. package/dist/storage/sqlite/migrations.js.map +1 -0
  130. package/dist/storage/sqlite/node.d.ts +25 -0
  131. package/dist/storage/sqlite/node.d.ts.map +1 -0
  132. package/dist/storage/sqlite/node.js +97 -0
  133. package/dist/storage/sqlite/node.js.map +1 -0
  134. package/dist/storage/sqlite/storage.d.ts +51 -0
  135. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  136. package/dist/storage/sqlite/storage.js +628 -0
  137. package/dist/storage/sqlite/storage.js.map +1 -0
  138. package/dist/tasks.d.ts +6 -0
  139. package/dist/tasks.d.ts.map +1 -0
  140. package/dist/tasks.js +5 -0
  141. package/dist/tasks.js.map +1 -0
  142. package/dist/testing/assertions.d.ts +5 -0
  143. package/dist/testing/assertions.d.ts.map +1 -0
  144. package/dist/testing/assertions.js +25 -0
  145. package/dist/testing/assertions.js.map +1 -0
  146. package/dist/testing/index.d.ts +6 -0
  147. package/dist/testing/index.d.ts.map +1 -0
  148. package/dist/testing/index.js +5 -0
  149. package/dist/testing/index.js.map +1 -0
  150. package/dist/testing/runner.d.ts +10 -0
  151. package/dist/testing/runner.d.ts.map +1 -0
  152. package/dist/testing/runner.js +14 -0
  153. package/dist/testing/runner.js.map +1 -0
  154. package/dist/testing/storage-benchmark.d.ts +41 -0
  155. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  156. package/dist/testing/storage-benchmark.js +351 -0
  157. package/dist/testing/storage-benchmark.js.map +1 -0
  158. package/dist/testing/storage-conformance.d.ts +4 -0
  159. package/dist/testing/storage-conformance.d.ts.map +1 -0
  160. package/dist/testing/storage-conformance.js +1122 -0
  161. package/dist/testing/storage-conformance.js.map +1 -0
  162. package/dist/testing/types.d.ts +19 -0
  163. package/dist/testing/types.d.ts.map +1 -0
  164. package/dist/testing/types.js +2 -0
  165. package/dist/testing/types.js.map +1 -0
  166. package/dist/types.d.ts +525 -78
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/types.js.map +1 -1
  169. package/docs/chord-delta-findings.md +1 -1
  170. package/docs/pico-v5-chord-usage.md +90 -102
  171. package/docs/pico-v5-handoff.md +493 -253
  172. package/docs/pico-v5.md +1901 -848
  173. package/package.json +47 -5
  174. package/dist/memory-storage.d.ts +0 -37
  175. package/dist/memory-storage.d.ts.map +0 -1
  176. package/dist/memory-storage.js +0 -322
  177. package/dist/memory-storage.js.map +0 -1
@@ -12,10 +12,9 @@ This guide uses the contracts in the [Pico5 specification](pico-v5.md).
12
12
  - **Durable document:** JSON persisted as a complete base, Chord operations, and
13
13
  checkpoints. Its definition token declares schema, version, scope, and initialization;
14
14
  conversation scope also declares history and fork behavior.
15
- - **Transaction draft:** the mutable tracked object from `await tx.doc`, valid
16
- only inside that commit callback, including all nested objects.
17
- - **DocumentSource:** an opaque handle to one document incarnation's committed changes,
18
- not a mutable value or a public subscription API.
15
+ - **Transaction draft:** the revocable copy-on-write object from `await tx.doc`,
16
+ valid only inside that commit callback, including all nested objects.
17
+ - **DocumentState:** a disposable read-only Chord state bound to one document incarnation.
19
18
  - **ReplicatedState:** Chord's immutable complete values through `value` and
20
19
  `subscribe(listener)`. `value` is `undefined` before hydration or while disconnected.
21
20
  The listener receives `(value, context, delivery)`; delivery contains `kind`
@@ -28,12 +27,12 @@ terminal and never participate in conversation forks.
28
27
  ## Imports and the adapter boundary
29
28
 
30
29
  Examples build on one another. Pico5 names (`defineDoc`, `defineDocFamily`,
31
- `Session`, `DocumentSource`, `Id`, `TaskRuntime`, `DocumentObserver`)
30
+ `Session`, `DocumentState`, `Id`, `TaskRuntime`, `DocumentObserver`)
32
31
  refer to normative contracts, without a specified import path or runnable Pico5
33
32
  package. In those contracts, `ConversationRecord`, `EntryRecord`, and
34
33
  `TaskRecord` are persisted records, while `Conversation` is the public
35
- conversation object and `Entry`/`Task` are typed definitions. These Chord imports
36
- exist today:
34
+ conversation object and `Entry`/`Task` are typed definitions. The Chord imports
35
+ below are the concrete APIs used by the examples:
37
36
 
38
37
  ```ts
39
38
  import {
@@ -42,26 +41,15 @@ import {
42
41
  type ReplicatedState,
43
42
  } from "@earendil-works/chord";
44
43
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
45
- import { applyImmutable } from "@earendil-works/chord/delta";
46
-
47
- // The spec calls this JsonObject; current Chord exports JsonValue, not JsonObject.
44
+ // The Pico specification calls this object-root constraint JsonObject.
48
45
  type JsonObject = { [key: string]: JsonValue };
49
-
50
- // Required by Pico5, but not a current Chord export yet.
51
- declare function documentReplicatedState<T extends JsonObject>(
52
- source: DocumentSource<T>, context: Context,
53
- ): Promise<{ readonly state: ReplicatedState<T | null>; dispose(): void }>;
54
46
  ```
55
47
 
56
- The adapter must register Chord-recognized state, atomically hydrate a matching
57
- committed value and adapter-owned delivery sequence, and forward committed ops without another
58
- tracker or re-diff. Disposal releases observation, not the document. Even Chord's
59
- hydration requests must never flush an uncommitted draft.
60
-
61
- Current `env.replicatedState(initial)` creates an **in-memory**, immutable
62
- latest-value source with transaction-scoped `.change(context, callback)` and
63
- `.replace(context, value)`, not a durable source adapter. There is no
64
- `replicatedState.own(source)`. Do not mirror documents into another state source.
48
+ `documentState()` performs Chord adoption internally: it atomically captures a
49
+ committed snapshot, registers for every later exact frame, and returns an already
50
+ hydrated read-only state. Operations covered by the snapshot are never
51
+ redelivered. Disposing the state releases observation, not the document. Pico
52
+ remains the sole mutator.
65
53
 
66
54
  ## 1. A Session-wide canvas
67
55
 
@@ -75,8 +63,8 @@ type CanvasState = { strokes: Stroke[] };
75
63
  const CanvasDoc = defineDoc<CanvasState>({
76
64
  kind: "app.canvas", version: 1, scope: "session",
77
65
  initial: () => ({ strokes: [] }),
78
- // This append-only example adds one stroke per commit.
79
- checkpointWhen: value => value.strokes.length % 100 === 0,
66
+ // Store a complete base after at most 99 replayed deltas.
67
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 99,
80
68
  });
81
69
 
82
70
  interface CanvasService {
@@ -86,20 +74,22 @@ interface CanvasService {
86
74
  const Canvas = defineService<CanvasService>("app.canvas");
87
75
 
88
76
  async function createCanvasFacet(session: Session, context: Context): Promise<Facet> {
89
- const target = { scope: "session" } as const;
90
- // Get-or-create: an absent canvas is initialized in a serialized commit.
91
- const source = await session.documentSource(CanvasDoc, target, context);
92
- const replica = await documentReplicatedState(source, context);
77
+ // Creation is explicit; observation never writes.
78
+ await session.commit(async tx => {
79
+ await tx.doc(CanvasDoc);
80
+ }, context);
81
+ const state = await session.documentState(CanvasDoc, context);
82
+ if (state === undefined) throw new Error("canvas was retired during setup");
93
83
  return defineFacet({
94
84
  id: "app.canvas/session",
95
85
  setup(env) {
96
- env.own(() => replica.dispose());
86
+ env.own(() => state.dispose());
97
87
  env.provide(Canvas, {
98
- state: replica.state,
88
+ state,
99
89
  async addStroke(stroke, context) {
100
90
  await session.commit(async tx => {
101
- const draft = await tx.doc(CanvasDoc, target);
102
- draft.strokes.push(stroke); // The draft now owns stroke; caller must not mutate it.
91
+ const draft = await tx.doc(CanvasDoc);
92
+ draft.strokes.push(stroke); // Chord copies the assigned stroke by value.
103
93
  }, context);
104
94
  },
105
95
  });
@@ -133,12 +123,13 @@ async function runCanvasExample(session: Session): Promise<void> {
133
123
  }
134
124
  ```
135
125
 
136
- The application owns the open Session. Acquire state **before** synchronous
137
- `setup`; `env.provide` cannot run in `onActivate`. Install one provider per
138
- Session host. Chord may reload presentation facets independently, but v1 does
139
- not use facet reload to replace Session-side task or hook implementations. A
140
- host extension code change closes and reopens the Harness with the new
141
- definition set.
126
+ The application owns the open Session. Acquire the document state asynchronously
127
+ before synchronous `setup`, then transfer its disposal to the facet;
128
+ `env.provide` cannot run in `onActivate`. Install one provider per
129
+ Session host. Chord may reload presentation facets independently. Session-side
130
+ tools, hooks, tasks, and prompt sections reload through the Harness
131
+ registry: publish the replacement in one `batch()`. Work already running keeps
132
+ the old code, so the facet manages its old resources' lifetime itself.
142
133
 
143
134
  Remote clients supply a `RemoteServiceTransport` connected to the host's `services`
144
135
  provider; Chord prescribes no socket protocol. `CanvasConsumer` also works unchanged
@@ -166,7 +157,7 @@ async function connectCanvas(transport: RemoteServiceTransport, context: Context
166
157
  ```
167
158
 
168
159
  ```text
169
- first acquisition -> commit base { strokes: [] }
160
+ explicit first tx.doc -> commit base { strokes: [] }
170
161
  addStroke(A) -> commit A -> local and remote subscribers see A
171
162
  worker restarts -> open same durable storage; install canvas facet again
172
163
  acquire source -> load base + committed deltas, without rerunning initial()
@@ -181,7 +172,7 @@ A method's successful commit does not promise every remote callback has run yet.
181
172
 
182
173
  A **document family** uses one definition for many instances. The logical key is
183
174
  `(kind, conversationId, key)`; the persisted numeric document ID identifies an
184
- incarnation and is never reused. Initializer input is not part of the key.
175
+ incarnation and is never reused. The creation seed is not part of the key.
185
176
 
186
177
  ```ts
187
178
  type ReviewInput = { path: string; patch: string };
@@ -190,8 +181,8 @@ type ReviewState = ReviewInput & { comments: ReviewComment[] };
190
181
  const ReviewDoc = defineDocFamily<ReviewState, ReviewInput>({
191
182
  kind: "app.diff-review", version: 1, family: true, scope: "conversation",
192
183
  history: "latest", fork: "current",
193
- initial: input => ({ path: input.path, patch: input.patch, comments: [] }),
194
- checkpointWhen: value => value.comments.length % 50 === 0,
184
+ initial: seed => ({ path: seed.path, patch: seed.patch, comments: [] }),
185
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 49,
195
186
  });
196
187
  interface DiffReviewService {
197
188
  readonly state: ReplicatedState<ReviewState | null>;
@@ -202,7 +193,7 @@ const DiffReviews = defineService<DiffReviewService>("app.diff-reviews");
202
193
 
203
194
  function reviewFacet(
204
195
  session: Session, conversationId: Id,
205
- reviews: readonly { key: string; initial: ReviewInput }[], context: Context,
196
+ reviews: readonly { key: string; seed: ReviewInput }[], context: Context,
206
197
  ): Facet {
207
198
  return defineFacet({
208
199
  id: "app.diff-reviews/session",
@@ -210,18 +201,20 @@ function reviewFacet(
210
201
  const instances = env.provideMany(DiffReviews);
211
202
  env.onActivate(async () => {
212
203
  for (const review of reviews) {
213
- const target = { scope: "conversation" as const, conversationId, ...review };
214
- const source = await session.documentSource(ReviewDoc, target, context);
215
- const replica = await documentReplicatedState(source, context);
216
- env.own(() => replica.dispose());
204
+ await session.commit(async tx => {
205
+ await tx.doc(ReviewDoc, conversationId, review.key, review.seed);
206
+ }, context);
207
+ const state = await session.documentState(ReviewDoc, conversationId, review.key, context);
208
+ if (state === undefined) throw new Error("review was retired during setup");
209
+ env.own(() => state.dispose());
217
210
  // Chord instance keys route services; they are not numeric document incarnation IDs.
218
211
  instances.spawn(JSON.stringify([conversationId, review.key]), {
219
- state: replica.state,
212
+ state,
220
213
  async identity() { return { conversationId, key: review.key }; },
221
214
  async addComment(comment, context) {
222
215
  await session.commit(async tx => {
223
- const draft = await tx.doc(ReviewDoc, target);
224
- draft.comments.push(comment); // Do not mutate comment after this assignment.
216
+ const draft = await tx.doc(ReviewDoc, conversationId, review.key, review.seed);
217
+ draft.comments.push(comment); // Chord copies the assigned comment by value.
225
218
  }, context);
226
219
  },
227
220
  }); // The facet owns spawned service lifetimes automatically.
@@ -232,7 +225,7 @@ function reviewFacet(
232
225
  }
233
226
  ```
234
227
 
235
- Example input: `[{ key: "review-7", initial: { path: "a.ts", patch: "-old\n+new" } }]`.
228
+ Example input: `[{ key: "review-7", seed: { path: "a.ts", patch: "-old\n+new" } }]`.
236
229
  Pass distinct family keys and a real conversation ID; install the facet as above.
237
230
  Keyed consumers use `env.observe(DiffReviews, handler)`, not `env.use`. The handler
238
231
  receives `(review, context)`; call `review.identity(context)` to identify it and
@@ -249,9 +242,9 @@ fork at E with current: child review-7 contains A and B
249
242
  child adds C: parent still contains only A and B
250
243
  ```
251
244
 
252
- With `fork: "initial"`, no instance copies; first child access uses supplied input.
245
+ With `fork: "initial"`, no instance copies; the first child `tx.doc()` uses its supplied seed.
253
246
  `latest` cannot read history or fork `asOf`; use `history: "rewindable"` with `asOf`
254
- to reflect E's commit. Reaccess ignores `initial`; updating the patch requires
247
+ to reflect E's commit. Reaccess ignores later seeds; updating the patch requires
255
248
  an explicit mutation or a distinct review key.
256
249
 
257
250
  ## 3. Task-scoped output and a tool/task watch
@@ -265,18 +258,15 @@ type JobOutput = { stdout: string; chunks: number };
265
258
  const JobOutputDoc = defineDoc<JobOutput>({
266
259
  kind: "app.job-output", version: 1, scope: "task",
267
260
  initial: () => ({ stdout: "", chunks: 0 }),
268
- checkpointWhen: value => value.chunks % 100 === 0,
261
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 99,
269
262
  });
270
- function jobOutputTarget(taskId: Id) {
271
- return { scope: "task" as const, taskId };
272
- }
273
263
  async function appendJobOutput(
274
264
  runtime: TaskRuntime<JobInput, { phase: "running" }, null, {}>,
275
265
  chunk: string, context: Context,
276
266
  ): Promise<void> {
277
267
  // Read process output outside this callback. The runtime gates the live task.
278
268
  await runtime.commit(async tx => {
279
- const draft = await tx.doc(JobOutputDoc, jobOutputTarget(runtime.taskId));
269
+ const draft = await tx.doc(JobOutputDoc, runtime.taskId);
280
270
  draft.stdout = (draft.stdout + chunk).slice(-50_000);
281
271
  draft.chunks += 1;
282
272
  }, context);
@@ -285,44 +275,43 @@ async function observeJob(
285
275
  api: DocumentObserver, producerTaskId: Id,
286
276
  finished: Promise<void>, context: Context,
287
277
  ): Promise<void> {
288
- const target = jobOutputTarget(producerTaskId);
289
- const watch = await api.watchDoc(JobOutputDoc, target, context);
290
- let value = watch.value;
291
- console.log(value === null ? "retired" : value.stdout);
278
+ const watch = await api.watchDoc(JobOutputDoc, producerTaskId, context);
279
+ if (watch === undefined) return;
280
+ console.log(watch.value === null ? "retired" : watch.value.stdout);
292
281
  try {
293
- watch.start(async (ops, _context) => {
294
- value = applyImmutable(value, ops);
282
+ watch.start(async (value, ops, _context) => {
283
+ await sendCommittedFrame(value, ops);
295
284
  await render(value === null ? "retired" : value.stdout);
296
285
  }); // Serialized callbacks never overlap.
297
286
  await finished; // Caller-supplied observation lifetime; outside any commit.
298
287
  } finally {
299
- watch.stop(); // Idempotent; prevents another callback from starting.
300
- await watch.closed; // Wait for an in-flight callback to settle.
288
+ await watch.stop(); // Idempotent; prevents another callback from starting.
301
289
  }
302
290
  }
303
291
  ```
304
292
 
305
293
  The running phase calls `appendJobOutput` and must also commit its next checkpoint
306
294
  or terminal outcome. `api` is the invocation's `DocumentObserver`; `TaskRuntime`
307
- includes that interface. Acquisition validates the target task is live and
308
- derives its conversation from the task record.
295
+ includes that interface. `tx.doc()` creation validates that the target task is live and derives its
296
+ conversation from the task record. A later non-creating watch lookup returns
297
+ `undefined` after the task is terminal and its document has retired.
309
298
 
310
299
  ```text
311
- watchDoc: capture immutable V0 + register for later committed operation batches
312
- producer commits D1 and D2 before start: queue D1, D2
313
- pending operation count exceeds its limit: replace suffix with [["r", V2]]
314
- start: caller has initialized from V0; deliver the reset through the async listener
315
- producer commits D3 while listener awaits: queue D3; never overlap callbacks
316
- unrelated commit has no source ops: enqueue nothing
317
- producer becomes terminal: queue [["r", null]], deliver it, then close as retired
300
+ watchDoc: capture immutable V0 + register for later exact committed frames
301
+ producer commits D1 and D2 before start: buffer (V1, D1), (V2, D2)
302
+ start: caller has initialized from V0; deliver D1, then D2 serially
303
+ producer commits D3 while listener awaits: buffer D3; never overlap callbacks
304
+ 101 pending frames: replace the pending suffix with [["r", newestValue]]
305
+ producer becomes terminal: deliver [["r", null]], then close as retired
318
306
  ```
319
307
 
320
308
  Watches automatically stop when their invocation ends. A Session-acquired watch
321
309
  is caller-owned and stops on Session close. Previously delivered snapshots never
322
- mutate. Slow or unstarted delivery may coalesce an undelivered suffix into a
323
- complete reset, so a watch is convergent state observation rather than a
324
- transition journal. Retirement ends the incarnation's stream; recreation
325
- requires a new watch/source and a new numeric document ID. A terminal task cannot
310
+ mutate. Slow or unstarted delivery retains up to 100 exact frames, then replaces
311
+ the undelivered suffix with a full-value root replacement. A watch is convergent
312
+ state observation rather than a transition journal. Retirement ends the
313
+ incarnation's stream; a service exposing that state must withdraw or remain
314
+ terminal at `null`. Recreation requires a new watch/state and numeric document ID. A terminal task cannot
326
315
  create more output. Task-scoped documents
327
316
  never copy into forks. Preserve required output in result entries or Session- or
328
317
  conversation-scoped documents in the terminal commit before retirement. Use a
@@ -336,11 +325,11 @@ under `docs` by stable document kind, not numeric incarnation ID. The spec's
336
325
  illustrative path mapping is:
337
326
 
338
327
  ```text
339
- document ["s", ["message"], value]
340
- -> view ["s", ["docs", "pi.live", "message"], value]
328
+ document ["s", ["generation", "message"], value]
329
+ -> view ["s", ["docs", "pi.live", "generation", "message"], value]
341
330
  ```
342
331
 
343
- Built-in kinds and fields await approval; `pi.live` is illustrative, not an available API.
332
+ `pi.live` is specified in `pico-v5.md` section 8.2.
344
333
  The mount publishes one batch per complete Session commit: entry/head changes and
345
334
  changed mounted documents together, without a tracker or semantic projection.
346
335
  Third-party documents are **not automatically mounted**; use their own Chord
@@ -348,36 +337,35 @@ services or trusted invocation watches. A retired source publishes `null`; a
348
337
  dynamic service should then withdraw its instance rather than expose stale data.
349
338
 
350
339
  ```text
351
- addStroke -> hold Session mutation line -> await tx.doc -> mutate tracked draft
352
- callback succeeds -> tracker flush: incremental ops + candidate value
340
+ addStroke -> hold Session mutation line -> await tx.doc -> mutate tracker change draft
341
+ callback succeeds -> tracker prepare: immutable candidate + immutable ops
353
342
  Session checkpoint predicate selects a base or delta exactly once
354
343
  atomic storage commit: persist selected document and record writes
355
- storage succeeds -> materialize immutable published value + enqueue value/ops, still on line
344
+ storage succeeds -> adopt candidate + enqueue candidate/ops, still on line
356
345
  release line -> deliver committed source ops -> adapter -> local/remote Chord consumers
357
346
  late subscriber -> atomically capture committed value + adapter sequence + subscription
358
347
  ```
359
348
 
360
- No visible-undurable path exists. Callback failure restores the unchanged
361
- baseline. Failure after flush, including checkpoint/storage failure, publishes
362
- nothing and poisons the open Session; close and reopen it instead of continuing.
349
+ No visible-undurable path exists. Callback, tracker preparation, and checkpoint
350
+ failure abort normally before Storage admission. An uncertain Storage failure
351
+ publishes nothing and poisons the open Session; close and reopen it instead of
352
+ continuing.
363
353
 
364
354
  ## Footguns
365
355
 
366
- - Never retain drafts, nested proxies, or bound array methods across commits.
367
- Never mutate inserted aliases or insert one mutable object at multiple paths.
356
+ - Never retain drafts, nested proxies, or bound array methods across commits;
357
+ they are revoked. Assigned containers are copied by value.
368
358
  - Async commit holds the line through storage settlement and baseline adoption.
369
359
  Await document access there, not models, processes, network calls, or humans.
370
- - Normal `snapshot`, `documentSource`, and `watchDoc` reads are get-or-create and
371
- lazily migrate through the supplied token. Harness open does not scan ordinary
372
- documents. Family `initial` is first-creation input, not an update.
360
+ - Only `tx.doc()` creates. `snapshot`, `documentState`, and `watchDoc` return
361
+ `undefined` when absent and never write. Family seeds are used only when the
362
+ first `tx.doc()` creates an incarnation; later seeds are ignored.
373
363
  - Initialize from the fixed `watch.value` before `start()`. Slow or unstarted
374
- delivery may coalesce an undelivered suffix into a root replacement when its
375
- operation count exceeds the limit, omitting intermediate states. Queue
376
- accounting never serializes operations to estimate bytes. Do not use a watch
377
- as an audit log.
378
- - A listener may call `stop()`, but must not await its own `closed` promise.
379
- - Definitions own checkpoints, not storage heuristics. Revise the counting predicates
380
- above if mutations change. Keep document kinds, versions, fork policies, and
364
+ delivery retains up to 100 exact frames, then replaces the pending suffix with
365
+ one full-value root replacement. No serialized-byte accounting is performed.
366
+ Do not use a watch as an audit log.
367
+ - `stop()` prevents future callbacks but does not abort or join one already running.
368
+ - Definitions own checkpoints, not storage heuristics. Keep document kinds, versions, fork policies, and
381
369
  public paths stable; schema changes require migration, not a source-only rename.
382
370
 
383
371
  Chord sources: [types](../../chord/src/types.ts), [facet examples](../../chord/test/facets.test.ts),