@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.
- package/README.md +72 -1
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +13 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +18 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/index.d.ts +131 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +45 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/node.d.ts +45 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +908 -0
- package/dist/env/node.js.map +1 -0
- package/dist/env/utils/adaptive-publisher.d.ts +24 -0
- package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
- package/dist/env/utils/adaptive-publisher.js +79 -0
- package/dist/env/utils/adaptive-publisher.js.map +1 -0
- package/dist/env/utils/output-capture.d.ts +31 -0
- package/dist/env/utils/output-capture.d.ts.map +1 -0
- package/dist/env/utils/output-capture.js +238 -0
- package/dist/env/utils/output-capture.js.map +1 -0
- package/dist/env/utils/truncate.d.ts +71 -0
- package/dist/env/utils/truncate.d.ts.map +1 -0
- package/dist/env/utils/truncate.js +294 -0
- package/dist/env/utils/truncate.js.map +1 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +24 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/config.d.ts +20 -0
- package/dist/harness/config.d.ts.map +1 -0
- package/dist/harness/config.js +18 -0
- package/dist/harness/config.js.map +1 -0
- package/dist/harness/context.d.ts +27 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +109 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/generation.d.ts +36 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +266 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +10 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +266 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/live.d.ts +40 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +41 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/prompt.d.ts +24 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +88 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/registry.d.ts +7 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +309 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/scheduler.d.ts +63 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +605 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +24 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +133 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/types.d.ts +358 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/util.d.ts +14 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +55 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +28 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +280 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +59 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +380 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +105 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +718 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +115 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +681 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +60 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +681 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +30 -0
- package/dist/storage/sqlite/database.d.ts.map +1 -0
- package/dist/storage/sqlite/database.js +2 -0
- package/dist/storage/sqlite/database.js.map +1 -0
- package/dist/storage/sqlite/index.d.ts +4 -0
- package/dist/storage/sqlite/index.d.ts.map +1 -0
- package/dist/storage/sqlite/index.js +3 -0
- package/dist/storage/sqlite/index.js.map +1 -0
- package/dist/storage/sqlite/migrations.d.ts +11 -0
- package/dist/storage/sqlite/migrations.d.ts.map +1 -0
- package/dist/storage/sqlite/migrations.js +111 -0
- package/dist/storage/sqlite/migrations.js.map +1 -0
- package/dist/storage/sqlite/node.d.ts +25 -0
- package/dist/storage/sqlite/node.d.ts.map +1 -0
- package/dist/storage/sqlite/node.js +97 -0
- package/dist/storage/sqlite/node.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +51 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +628 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +10 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +14 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +351 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1122 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/types.d.ts +525 -78
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/chord-delta-findings.md +1 -1
- package/docs/pico-v5-chord-usage.md +90 -102
- package/docs/pico-v5-handoff.md +493 -253
- package/docs/pico-v5.md +1901 -848
- package/package.json +47 -5
- package/dist/memory-storage.d.ts +0 -37
- package/dist/memory-storage.d.ts.map +0 -1
- package/dist/memory-storage.js +0 -322
- 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
|
|
16
|
-
only inside that commit callback, including all nested objects.
|
|
17
|
-
- **
|
|
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`, `
|
|
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.
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
committed
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
79
|
-
checkpointWhen:
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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(() =>
|
|
86
|
+
env.own(() => state.dispose());
|
|
97
87
|
env.provide(Canvas, {
|
|
98
|
-
state
|
|
88
|
+
state,
|
|
99
89
|
async addStroke(stroke, context) {
|
|
100
90
|
await session.commit(async tx => {
|
|
101
|
-
const draft = await tx.doc(CanvasDoc
|
|
102
|
-
draft.strokes.push(stroke); //
|
|
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
|
|
137
|
-
`setup
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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.
|
|
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:
|
|
194
|
-
checkpointWhen:
|
|
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;
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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,
|
|
224
|
-
draft.comments.push(comment); //
|
|
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",
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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.
|
|
308
|
-
|
|
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
|
|
312
|
-
producer commits D1 and D2 before start:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
323
|
-
|
|
324
|
-
transition journal. Retirement ends the
|
|
325
|
-
|
|
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
|
-
|
|
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
|
|
352
|
-
callback succeeds -> tracker
|
|
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 ->
|
|
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
|
|
361
|
-
|
|
362
|
-
nothing and poisons the open Session; close and reopen it instead of
|
|
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
|
-
|
|
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
|
-
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
-
|
|
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),
|