@earendil-works/pi-durable 0.86.1 → 0.87.1
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 +29 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/{memory-storage.d.ts → storage/memory.d.ts} +12 -6
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/{memory-storage.js → storage/memory.js} +236 -24
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +26 -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 +104 -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 +45 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +524 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/types.d.ts +113 -34
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/docs/pico-v5-chord-usage.md +64 -62
- package/docs/pico-v5-handoff.md +92 -57
- package/docs/pico-v5.md +519 -350
- package/package.json +20 -3
- package/dist/memory-storage.d.ts.map +0 -1
- package/dist/memory-storage.js.map +0 -1
package/docs/pico-v5.md
CHANGED
|
@@ -4,7 +4,12 @@ Pico5 is a durable, extensible agent harness. This document is normative.
|
|
|
4
4
|
Pico5 uses existing package types as follows:
|
|
5
5
|
|
|
6
6
|
```ts
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
Context,
|
|
9
|
+
Draft,
|
|
10
|
+
JsonValue,
|
|
11
|
+
ReplicatedState,
|
|
12
|
+
} from "@earendil-works/chord";
|
|
8
13
|
import { applyImmutable, type Op } from "@earendil-works/chord/delta";
|
|
9
14
|
import type {
|
|
10
15
|
Message,
|
|
@@ -18,7 +23,7 @@ import type {
|
|
|
18
23
|
} from "@earendil-works/pi-ai";
|
|
19
24
|
|
|
20
25
|
type JsonObject = { [key: string]: JsonValue };
|
|
21
|
-
type
|
|
26
|
+
type TaskOutcomeError = { message: string; detail?: JsonValue };
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
Pico5 targets the transcript `SystemMessage` contract from pi-ai PR
|
|
@@ -32,8 +37,8 @@ The core rule is:
|
|
|
32
37
|
|
|
33
38
|
## 1. Terms and invariants
|
|
34
39
|
|
|
35
|
-
- A **Session** owns one mutation line, conversations, entries, tasks,
|
|
36
|
-
and documents.
|
|
40
|
+
- A **Session** owns one mutation line, conversations, entries, tasks,
|
|
41
|
+
submissions, and documents.
|
|
37
42
|
- A **conversation** is a transcript scope. It may fork another conversation.
|
|
38
43
|
- An **entry** is an immutable transcript record.
|
|
39
44
|
- A **task** is a durable state machine attached to one conversation.
|
|
@@ -50,13 +55,13 @@ Required invariants:
|
|
|
50
55
|
3. All visible progress is durable. There is no volatile publication path.
|
|
51
56
|
4. External effects do not run inside the Session mutation transaction.
|
|
52
57
|
5. Entries and IDs are immutable and never reused after a committed write.
|
|
53
|
-
6. Document drafts
|
|
54
|
-
|
|
55
|
-
membrane.
|
|
58
|
+
6. Document drafts are revoked when their transaction callback settles. Values
|
|
59
|
+
assigned into a draft are copied by value.
|
|
56
60
|
7. The mutation line remains held through storage settlement and committed-state
|
|
57
61
|
adoption. Listener callbacks run later, off the line.
|
|
58
|
-
8.
|
|
59
|
-
|
|
62
|
+
8. An uncertain storage failure is fatal to the open Session. It publishes
|
|
63
|
+
nothing and must be reopened. Preparation and checkpoint failures occur before
|
|
64
|
+
storage admission and roll back normally.
|
|
60
65
|
|
|
61
66
|
## 2. Core records
|
|
62
67
|
|
|
@@ -65,6 +70,7 @@ these contracts.
|
|
|
65
70
|
|
|
66
71
|
```ts
|
|
67
72
|
type Id = number;
|
|
73
|
+
/** Strictly increases between commits; gaps are permitted. */
|
|
68
74
|
type Seq = number;
|
|
69
75
|
const ROOT_CONVERSATION_ID: Id = 1;
|
|
70
76
|
|
|
@@ -123,30 +129,73 @@ type EntryDraft = Omit<EntryRecord, "id" | "conversationId" | "byTaskId" | "head
|
|
|
123
129
|
readonly head?: Id | "self";
|
|
124
130
|
};
|
|
125
131
|
|
|
126
|
-
type
|
|
132
|
+
type SubmissionRecordBase = {
|
|
127
133
|
readonly id: Id;
|
|
128
134
|
readonly conversationId: Id;
|
|
129
135
|
readonly requestId?: string;
|
|
130
136
|
};
|
|
131
137
|
|
|
132
|
-
type
|
|
133
|
-
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
138
|
+
type SubmissionRecord =
|
|
139
|
+
| (SubmissionRecordBase & {
|
|
140
|
+
readonly type: "input";
|
|
141
|
+
} & (
|
|
142
|
+
| {
|
|
143
|
+
readonly status: "queued";
|
|
144
|
+
readonly entry?: never;
|
|
145
|
+
readonly answer?: never;
|
|
146
|
+
readonly reason?: never;
|
|
147
|
+
readonly detail?: never;
|
|
148
|
+
}
|
|
149
|
+
| {
|
|
150
|
+
readonly status: "placed";
|
|
151
|
+
readonly entry: Id;
|
|
152
|
+
readonly answer?: never;
|
|
153
|
+
readonly reason?: never;
|
|
154
|
+
readonly detail?: never;
|
|
155
|
+
}
|
|
156
|
+
| {
|
|
157
|
+
readonly status: "done";
|
|
158
|
+
readonly entry: Id;
|
|
159
|
+
readonly answer: Id;
|
|
160
|
+
readonly reason?: never;
|
|
161
|
+
readonly detail?: never;
|
|
162
|
+
}
|
|
163
|
+
| {
|
|
164
|
+
readonly status: "unanswered";
|
|
165
|
+
readonly entry?: Id;
|
|
166
|
+
readonly answer?: never;
|
|
167
|
+
readonly reason: string;
|
|
168
|
+
readonly detail?: JsonValue;
|
|
169
|
+
}
|
|
170
|
+
))
|
|
171
|
+
| (SubmissionRecordBase & {
|
|
172
|
+
readonly type: "write";
|
|
173
|
+
} & (
|
|
174
|
+
| {
|
|
175
|
+
readonly status: "queued";
|
|
176
|
+
readonly entry?: never;
|
|
177
|
+
readonly answer?: never;
|
|
178
|
+
readonly reason?: never;
|
|
179
|
+
readonly detail?: never;
|
|
180
|
+
}
|
|
181
|
+
| {
|
|
182
|
+
readonly status: "done";
|
|
183
|
+
readonly entry: Id;
|
|
184
|
+
readonly answer?: never;
|
|
185
|
+
readonly reason?: never;
|
|
186
|
+
readonly detail?: never;
|
|
187
|
+
}
|
|
188
|
+
| {
|
|
189
|
+
readonly status: "unanswered";
|
|
190
|
+
readonly entry?: never;
|
|
191
|
+
readonly answer?: never;
|
|
192
|
+
readonly reason: string;
|
|
193
|
+
readonly detail?: JsonValue;
|
|
194
|
+
}
|
|
195
|
+
));
|
|
147
196
|
|
|
148
|
-
type
|
|
149
|
-
? Record extends
|
|
197
|
+
type SubmissionCreate = SubmissionRecord extends infer Record
|
|
198
|
+
? Record extends SubmissionRecord
|
|
150
199
|
? Omit<Record, "id">
|
|
151
200
|
: never
|
|
152
201
|
: never;
|
|
@@ -203,11 +252,24 @@ type ModelRef = {
|
|
|
203
252
|
|
|
204
253
|
type UserInput = UserMessage["content"];
|
|
205
254
|
|
|
206
|
-
type
|
|
207
|
-
readonly content: UserInput;
|
|
255
|
+
type SubmissionDraft = {
|
|
208
256
|
readonly requestId?: string;
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
} & (
|
|
258
|
+
| {
|
|
259
|
+
readonly type: "input";
|
|
260
|
+
readonly content: UserInput;
|
|
261
|
+
readonly whenBusy?: "steer" | "followUp" | "reject";
|
|
262
|
+
readonly entry?: never;
|
|
263
|
+
}
|
|
264
|
+
| {
|
|
265
|
+
readonly type: "write";
|
|
266
|
+
readonly entry: EntryDraft;
|
|
267
|
+
readonly content?: never;
|
|
268
|
+
readonly whenBusy?: never;
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
type InputSubmissionDraft = Extract<SubmissionDraft, { readonly type: "input" }>;
|
|
211
273
|
|
|
212
274
|
type SectionSeed =
|
|
213
275
|
| { readonly key: string; readonly value: JsonValue }
|
|
@@ -255,17 +317,15 @@ type ContextView = {
|
|
|
255
317
|
readonly messages: readonly Message[];
|
|
256
318
|
};
|
|
257
319
|
|
|
258
|
-
type
|
|
320
|
+
type SettledSubmissionRecord = SubmissionRecord & {
|
|
259
321
|
readonly status: "done" | "unanswered";
|
|
260
322
|
};
|
|
261
323
|
|
|
262
|
-
interface
|
|
324
|
+
interface Submission {
|
|
263
325
|
readonly id: Id;
|
|
264
|
-
|
|
265
|
-
wait(context: Context): Promise<
|
|
266
|
-
abort(
|
|
267
|
-
context: Context,
|
|
268
|
-
): Promise<"aborted" | "already_placed" | "not_found">;
|
|
326
|
+
status(context: Context): Promise<SubmissionRecord>;
|
|
327
|
+
wait(context: Context): Promise<SettledSubmissionRecord>;
|
|
328
|
+
abort(context: Context): Promise<"aborted" | "already_placed" | "settled">;
|
|
269
329
|
}
|
|
270
330
|
|
|
271
331
|
type SettledTask<R> = TaskRecord<JsonValue, JsonValue, R> & {
|
|
@@ -280,8 +340,7 @@ type HooksOf<K> = K extends Task<infer _I, infer _S, infer _R, infer H>
|
|
|
280
340
|
|
|
281
341
|
interface Conversation {
|
|
282
342
|
readonly id: Id;
|
|
283
|
-
|
|
284
|
-
write(entry: EntryDraft, context: Context): Promise<InputHandle>;
|
|
343
|
+
submit(submission: SubmissionDraft, context: Context): Promise<Submission>;
|
|
285
344
|
|
|
286
345
|
getModel(context: Context): Promise<ModelRef | undefined>;
|
|
287
346
|
setModel(model: ModelRef | undefined, context: Context): Promise<void>;
|
|
@@ -352,11 +411,12 @@ interface Harness extends Session {
|
|
|
352
411
|
): Promise<Conversation>;
|
|
353
412
|
|
|
354
413
|
getTask(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
|
|
355
|
-
|
|
414
|
+
submission(id: Id, context: Context): Promise<Submission | undefined>;
|
|
415
|
+
abortSubmission(
|
|
356
416
|
id: Id,
|
|
357
417
|
context: Context,
|
|
358
418
|
conversationId?: Id,
|
|
359
|
-
): Promise<"aborted" | "already_placed" | "not_found">;
|
|
419
|
+
): Promise<"aborted" | "already_placed" | "settled" | "not_found">;
|
|
360
420
|
abortTask(id: Id, context: Context): Promise<"marked" | "terminal">;
|
|
361
421
|
markTask(id: Id, context: Context): Promise<"marked" | "terminal">;
|
|
362
422
|
waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
|
|
@@ -375,9 +435,9 @@ declare const Harness: {
|
|
|
375
435
|
This intentionally retains the useful Pico3 host shape. It removes Pico3's
|
|
376
436
|
fixed `rewindable()`/`sticky()` accessors, namespace router, semantic view events,
|
|
377
437
|
and manual Chord view bridge. Typed Pico5 documents and the structural
|
|
378
|
-
conversation watch replace those surfaces. `
|
|
379
|
-
|
|
380
|
-
|
|
438
|
+
conversation watch replace those surfaces. `submit()` durably admits either a
|
|
439
|
+
user input or passive entry write and returns one `Submission` that tracks its
|
|
440
|
+
settlement.
|
|
381
441
|
|
|
382
442
|
`Harness.open()` installs built-in task, tool, and section definitions followed
|
|
383
443
|
by supplied task kinds, tools, and sections. It changes
|
|
@@ -407,13 +467,14 @@ movement during normal operation, not replacement of executing extension code;
|
|
|
407
467
|
section 7.4 governs code reload.
|
|
408
468
|
|
|
409
469
|
Conversation creation atomically commits the conversation, built-in
|
|
410
|
-
configuration and section/tool seeds, and optional input
|
|
470
|
+
configuration and section/tool seeds, and an optional input submission. For a fork,
|
|
411
471
|
omitted model, section, and active-tool values follow their document definition's
|
|
412
472
|
fork policy; provided values override those forked built-in values in the same
|
|
413
473
|
commit. Other documents follow their own definitions without special handling.
|
|
414
474
|
|
|
415
475
|
The built-in conversation configuration document contains the selected model,
|
|
416
|
-
thinking level, section
|
|
476
|
+
thinking level, an ordered array of section key/value records, and active tool
|
|
477
|
+
names. Its initial thinking level
|
|
417
478
|
is `"off"`. It is rewindable with `fork: "asOf"`, so a child starts from the
|
|
418
479
|
configuration visible at its selected entry unless explicit creation seeds
|
|
419
480
|
override it. Conversation creation eagerly
|
|
@@ -443,7 +504,7 @@ Unregistering a tool does not rewrite any conversation.
|
|
|
443
504
|
|
|
444
505
|
If request preparation finds unavailable active names, it performs no provider
|
|
445
506
|
request. It atomically terminalizes the generation task as `failed` with
|
|
446
|
-
`detail: { code: "missing_active_tool", names }`, makes its placed
|
|
507
|
+
`detail: { code: "missing_active_tool", names }`, makes its placed input submissions
|
|
447
508
|
`unanswered` with reason `missing_active_tool`, clears matching turn control, and
|
|
448
509
|
appends a visible model-less diagnostic entry. It does not silently change the
|
|
449
510
|
durable loadout. Historical system entries remain replayable because they store
|
|
@@ -466,7 +527,7 @@ does not guarantee placement of queued passive writes.
|
|
|
466
527
|
`markTask()` only commits `abortRequested`; it neither signals nor joins an
|
|
467
528
|
active invocation. The scheduler notices the mark on its next drain.
|
|
468
529
|
`abortTask()` also signals and joins an active run before starting the abort
|
|
469
|
-
invocation. `Conversation.abort()` withdraws queued
|
|
530
|
+
invocation. `Conversation.abort()` withdraws queued input submissions, marks
|
|
470
531
|
non-background tasks in the conversation and owned subtree, signals them, and
|
|
471
532
|
resolves only after that subtree is ordinarily idle. Passive writes and
|
|
472
533
|
background tasks survive. Conversation idle means no non-background live task
|
|
@@ -480,13 +541,15 @@ commit. Listener failures go to `onReport` and do not stop other listeners. Its
|
|
|
480
541
|
idempotent disposer removes the listener; Harness close removes all remaining
|
|
481
542
|
listeners.
|
|
482
543
|
|
|
483
|
-
`
|
|
484
|
-
creates a user message with the admission timestamp
|
|
485
|
-
`followUp`.
|
|
486
|
-
generation.
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
is the explicit queued
|
|
544
|
+
`submit()` returns after durable admission, not settlement. An input submission
|
|
545
|
+
creates a user message with the admission timestamp; `whenBusy` defaults to
|
|
546
|
+
`followUp`. A write submission uses the ordered passive path in section 6 and
|
|
547
|
+
never starts generation. `Submission.wait()` settles an input only after its
|
|
548
|
+
answer or terminal failure; it settles a write when the entry is placed or the
|
|
549
|
+
write becomes terminally unplaceable. Cancelling `wait()` only cancels that wait.
|
|
550
|
+
It does not withdraw the submission; `Submission.abort()` is the explicit queued
|
|
551
|
+
withdrawal operation. `Harness.submission()` reacquires a submission after
|
|
552
|
+
reopen; records remain queryable after settlement.
|
|
490
553
|
|
|
491
554
|
`abortTask()` durably requests cancellation and returns `marked` after the mark
|
|
492
555
|
is committed, any active run invocation has joined, and an abort invocation has
|
|
@@ -548,45 +611,100 @@ type DocDefinition<T extends JsonObject> =
|
|
|
548
611
|
type DocFamilyDefinition<T extends JsonObject, I extends JsonValue> =
|
|
549
612
|
Omit<CommonDocDefinition<T>, "initial"> & DocumentSemantics & {
|
|
550
613
|
readonly family: true;
|
|
551
|
-
initial(
|
|
614
|
+
initial(seed: I): T;
|
|
552
615
|
};
|
|
553
616
|
|
|
554
|
-
function defineDoc<T extends JsonObject>(
|
|
555
|
-
definition: CommonDocDefinition<T> & RewindableConversationSemantics,
|
|
556
|
-
): RewindableDocToken<T>;
|
|
557
|
-
function defineDoc<T extends JsonObject>(definition: DocDefinition<T>): DocToken<T>;
|
|
558
|
-
|
|
559
|
-
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
560
|
-
definition: Omit<CommonDocDefinition<T>, "initial"> &
|
|
561
|
-
RewindableConversationSemantics & {
|
|
562
|
-
readonly family: true;
|
|
563
|
-
initial(input: I): T;
|
|
564
|
-
},
|
|
565
|
-
): RewindableDocFamilyToken<T, I>;
|
|
566
|
-
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
567
|
-
definition: DocFamilyDefinition<T, I>,
|
|
568
|
-
): DocFamilyToken<T, I>;
|
|
569
|
-
|
|
570
617
|
declare const docType: unique symbol;
|
|
571
|
-
interface DocToken<T extends JsonObject
|
|
572
|
-
readonly definition:
|
|
618
|
+
interface DocToken<T extends JsonObject, D extends DocDefinition<T>> {
|
|
619
|
+
readonly definition: D;
|
|
573
620
|
readonly [docType]?: T;
|
|
574
621
|
}
|
|
575
|
-
interface
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
622
|
+
interface DocFamilyToken<
|
|
623
|
+
T extends JsonObject,
|
|
624
|
+
I extends JsonValue,
|
|
625
|
+
D extends DocFamilyDefinition<T, I>,
|
|
626
|
+
> {
|
|
627
|
+
readonly definition: D;
|
|
580
628
|
readonly [docType]?: T;
|
|
581
629
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
630
|
+
|
|
631
|
+
type SessionDocToken<T extends JsonObject> = DocToken<
|
|
632
|
+
T,
|
|
633
|
+
CommonDocDefinition<T> & { readonly scope: "session" }
|
|
634
|
+
>;
|
|
635
|
+
type ConversationDocToken<T extends JsonObject> = DocToken<
|
|
636
|
+
T,
|
|
637
|
+
CommonDocDefinition<T> & (LatestConversationSemantics | RewindableConversationSemantics)
|
|
638
|
+
>;
|
|
639
|
+
type RewindableConversationDocToken<T extends JsonObject> = DocToken<
|
|
640
|
+
T,
|
|
641
|
+
CommonDocDefinition<T> & RewindableConversationSemantics
|
|
642
|
+
>;
|
|
643
|
+
type TaskDocToken<T extends JsonObject> = DocToken<
|
|
644
|
+
T,
|
|
645
|
+
CommonDocDefinition<T> & { readonly scope: "task" }
|
|
646
|
+
>;
|
|
647
|
+
|
|
648
|
+
type SessionDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
649
|
+
T,
|
|
650
|
+
I,
|
|
651
|
+
DocFamilyDefinition<T, I> & { readonly scope: "session" }
|
|
652
|
+
>;
|
|
653
|
+
type ConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
654
|
+
T,
|
|
655
|
+
I,
|
|
656
|
+
DocFamilyDefinition<T, I> & (LatestConversationSemantics | RewindableConversationSemantics)
|
|
657
|
+
>;
|
|
658
|
+
type RewindableConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
659
|
+
T,
|
|
660
|
+
I,
|
|
661
|
+
DocFamilyDefinition<T, I> & RewindableConversationSemantics
|
|
662
|
+
>;
|
|
663
|
+
type TaskDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
|
|
664
|
+
T,
|
|
665
|
+
I,
|
|
666
|
+
DocFamilyDefinition<T, I> & { readonly scope: "task" }
|
|
667
|
+
>;
|
|
668
|
+
|
|
669
|
+
function defineDoc<T extends JsonObject>(
|
|
670
|
+
definition: CommonDocDefinition<T> & { readonly scope: "session" },
|
|
671
|
+
): SessionDocToken<T>;
|
|
672
|
+
function defineDoc<T extends JsonObject>(
|
|
673
|
+
definition: CommonDocDefinition<T> & LatestConversationSemantics,
|
|
674
|
+
): ConversationDocToken<T>;
|
|
675
|
+
function defineDoc<T extends JsonObject>(
|
|
676
|
+
definition: CommonDocDefinition<T> & RewindableConversationSemantics,
|
|
677
|
+
): RewindableConversationDocToken<T>;
|
|
678
|
+
function defineDoc<T extends JsonObject>(
|
|
679
|
+
definition: CommonDocDefinition<T> & { readonly scope: "task" },
|
|
680
|
+
): TaskDocToken<T>;
|
|
681
|
+
|
|
682
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
683
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & {
|
|
684
|
+
readonly family: true;
|
|
685
|
+
readonly scope: "session";
|
|
686
|
+
initial(seed: I): T;
|
|
687
|
+
},
|
|
688
|
+
): SessionDocFamilyToken<T, I>;
|
|
689
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
690
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & LatestConversationSemantics & {
|
|
691
|
+
readonly family: true;
|
|
692
|
+
initial(seed: I): T;
|
|
693
|
+
},
|
|
694
|
+
): ConversationDocFamilyToken<T, I>;
|
|
695
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
696
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & RewindableConversationSemantics & {
|
|
697
|
+
readonly family: true;
|
|
698
|
+
initial(seed: I): T;
|
|
699
|
+
},
|
|
700
|
+
): RewindableConversationDocFamilyToken<T, I>;
|
|
701
|
+
function defineDocFamily<T extends JsonObject, I extends JsonValue>(
|
|
702
|
+
definition: Omit<CommonDocDefinition<T>, "initial"> & {
|
|
703
|
+
readonly family: true;
|
|
704
|
+
readonly scope: "task";
|
|
705
|
+
initial(seed: I): T;
|
|
706
|
+
},
|
|
707
|
+
): TaskDocFamilyToken<T, I>;
|
|
590
708
|
```
|
|
591
709
|
|
|
592
710
|
Validation rules:
|
|
@@ -612,7 +730,7 @@ case model, tool, inbox, or presentation state.
|
|
|
612
730
|
|
|
613
731
|
### 3.2 Records and lifetimes
|
|
614
732
|
|
|
615
|
-
A persisted document instance has one
|
|
733
|
+
A persisted document instance has one `DocumentRecord`:
|
|
616
734
|
|
|
617
735
|
```ts
|
|
618
736
|
type DocumentRecord = {
|
|
@@ -659,117 +777,92 @@ There is no mutable `session.document()` API.
|
|
|
659
777
|
|
|
660
778
|
```ts
|
|
661
779
|
interface Session extends DocumentObserver {
|
|
662
|
-
commit<T>(
|
|
663
|
-
change: (tx: Tx) => T | Promise<T>,
|
|
664
|
-
context: Context,
|
|
665
|
-
): Promise<T>;
|
|
666
|
-
|
|
780
|
+
commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
|
|
667
781
|
close(context: Context): Promise<void>;
|
|
668
782
|
|
|
669
|
-
snapshot<T extends JsonObject>(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
): Promise<Readonly<T
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
): Promise<
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
context: Context,
|
|
686
|
-
): Promise<Readonly<T> | undefined>;
|
|
687
|
-
|
|
688
|
-
snapshotAsOf<T extends JsonObject, I extends JsonValue>(
|
|
689
|
-
token: RewindableDocFamilyToken<T, I>,
|
|
690
|
-
target: HistoricalFamilyTarget,
|
|
691
|
-
at: Id,
|
|
692
|
-
context: Context,
|
|
693
|
-
): Promise<Readonly<T> | undefined>;
|
|
694
|
-
|
|
695
|
-
documentSource<T extends JsonObject>(
|
|
696
|
-
token: DocToken<T>,
|
|
697
|
-
target: DocTarget,
|
|
698
|
-
context: Context,
|
|
699
|
-
): Promise<DocumentSource<T>>;
|
|
700
|
-
|
|
701
|
-
documentSource<T extends JsonObject, I extends JsonValue>(
|
|
702
|
-
token: DocFamilyToken<T, I>,
|
|
703
|
-
target: FamilyTarget<I>,
|
|
704
|
-
context: Context,
|
|
705
|
-
): Promise<DocumentSource<T>>;
|
|
783
|
+
snapshot<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<Readonly<T> | undefined>;
|
|
784
|
+
snapshot<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: Id, context: Context): Promise<Readonly<T> | undefined>;
|
|
785
|
+
snapshot<T extends JsonObject>(token: TaskDocToken<T>, taskId: Id, context: Context): Promise<Readonly<T> | undefined>;
|
|
786
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
787
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: Id, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
788
|
+
snapshot<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: Id, key: string, context: Context): Promise<Readonly<T> | undefined>;
|
|
789
|
+
|
|
790
|
+
snapshotAsOf<T extends JsonObject>(token: RewindableConversationDocToken<T>, conversationId: Id, at: Id, context: Context): Promise<Readonly<T> | undefined>;
|
|
791
|
+
snapshotAsOf<T extends JsonObject, I extends JsonValue>(token: RewindableConversationDocFamilyToken<T, I>, conversationId: Id, key: string, at: Id, context: Context): Promise<Readonly<T> | undefined>;
|
|
792
|
+
|
|
793
|
+
documentSource<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
794
|
+
documentSource<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: Id, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
795
|
+
documentSource<T extends JsonObject>(token: TaskDocToken<T>, taskId: Id, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
796
|
+
documentSource<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
797
|
+
documentSource<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: Id, key: string, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
798
|
+
documentSource<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: Id, key: string, context: Context): Promise<DocumentSource<T> | undefined>;
|
|
706
799
|
}
|
|
707
800
|
|
|
708
801
|
interface Tx {
|
|
709
802
|
conversation(id: Id): Promise<ConversationRecord | undefined>;
|
|
710
803
|
entry(id: Id): Promise<EntryRecord | undefined>;
|
|
711
|
-
input(id: Id): Promise<Input | undefined>;
|
|
712
804
|
task(id: Id): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
|
|
713
805
|
scanEntries(query: EntryQuery): Promise<readonly EntryRecord[]>;
|
|
714
806
|
scanTasks(query: TaskQuery): Promise<readonly TaskRecord<JsonValue, JsonValue, JsonValue>[]>;
|
|
715
807
|
|
|
716
|
-
createConversation(value: Omit<ConversationRecord, "id">): ConversationRecord
|
|
717
|
-
appendEntry(conversationId: Id, value: EntryDraft): EntryRecord
|
|
718
|
-
createInput(value: InputCreate): Input;
|
|
719
|
-
setInput(value: Input): void;
|
|
808
|
+
createConversation(value: Omit<ConversationRecord, "id">): Promise<ConversationRecord>;
|
|
809
|
+
appendEntry(conversationId: Id, value: EntryDraft): Promise<EntryRecord>;
|
|
720
810
|
createTask<I, S extends { phase: string }, R, H extends object>(
|
|
721
811
|
task: Task<I, S, R, H>, input: I, options?: TaskOptions,
|
|
722
|
-
): TaskRef<R
|
|
812
|
+
): Promise<TaskRef<R>>;
|
|
723
813
|
setTask(value: TaskRecord<JsonValue, JsonValue, JsonValue>): void;
|
|
724
814
|
|
|
725
|
-
doc<T extends JsonObject>(token:
|
|
726
|
-
doc<T extends JsonObject
|
|
727
|
-
|
|
728
|
-
|
|
815
|
+
doc<T extends JsonObject>(token: SessionDocToken<T>): Promise<Draft<T>>;
|
|
816
|
+
doc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: Id): Promise<Draft<T>>;
|
|
817
|
+
doc<T extends JsonObject>(token: TaskDocToken<T>, taskId: Id): Promise<Draft<T>>;
|
|
818
|
+
doc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, seed: I): Promise<Draft<T>>;
|
|
819
|
+
doc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: Id, key: string, seed: I): Promise<Draft<T>>;
|
|
820
|
+
doc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: Id, key: string, seed: I): Promise<Draft<T>>;
|
|
821
|
+
|
|
822
|
+
retireDoc<T extends JsonObject>(token: SessionDocToken<T>): Promise<void>;
|
|
823
|
+
retireDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: Id): Promise<void>;
|
|
824
|
+
retireDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: Id): Promise<void>;
|
|
825
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string): Promise<void>;
|
|
826
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: Id, key: string): Promise<void>;
|
|
827
|
+
retireDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: Id, key: string): Promise<void>;
|
|
729
828
|
}
|
|
730
|
-
|
|
731
|
-
type DocTarget =
|
|
732
|
-
| { readonly scope: "session" }
|
|
733
|
-
| { readonly scope: "conversation"; readonly conversationId: Id }
|
|
734
|
-
| { readonly scope: "task"; readonly taskId: Id };
|
|
735
|
-
|
|
736
|
-
type FamilyKey = DocTarget & { readonly key: string };
|
|
737
|
-
|
|
738
|
-
type FamilyTarget<I> = FamilyKey & { readonly initial: I };
|
|
739
|
-
|
|
740
|
-
type HistoricalFamilyTarget = {
|
|
741
|
-
readonly scope: "conversation";
|
|
742
|
-
readonly conversationId: Id;
|
|
743
|
-
readonly key: string;
|
|
744
|
-
};
|
|
745
829
|
```
|
|
746
830
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
831
|
+
ID-creating transaction methods are asynchronous because remote storage may allocate globally unique IDs durably. Only public typed `tx.doc()` is get-or-create. Internal fork copying may create
|
|
832
|
+
new incarnations directly from stored values without a definition. `tx.doc()`
|
|
833
|
+
receives the definition token that supplies
|
|
834
|
+
its static type, initializer, migration, and checkpoint policy. Scope-preserving
|
|
835
|
+
token overloads require callers to supply only the concrete conversation/task ID
|
|
836
|
+
and, for a family, its key and creation seed. Ordinary definitions are not
|
|
837
|
+
registered and ordinary documents are not scanned at open.
|
|
751
838
|
|
|
752
839
|
- Existing instances are reconstructed and migrated lazily according to section
|
|
753
|
-
3.6.
|
|
754
|
-
|
|
755
|
-
-
|
|
756
|
-
|
|
757
|
-
- The
|
|
758
|
-
|
|
840
|
+
3.6. A migration reached through `tx.doc()` is staged in its enclosing
|
|
841
|
+
transaction.
|
|
842
|
+
- A missing singleton calls the token's `initial()`. A missing family member calls
|
|
843
|
+
`initial(seed)`. Creation stores an initial base.
|
|
844
|
+
- The first acquisition of one logical address is memoized before awaiting. Later
|
|
845
|
+
acquisitions in that transaction return the same draft; for a missing family,
|
|
846
|
+
the first call's detached seed wins and later seeds are ignored.
|
|
847
|
+
- `snapshot()`, `snapshotAsOf()`, `documentSource()`, and `watchDoc()` never create
|
|
848
|
+
or persist migration. They return `undefined` when the requested incarnation is
|
|
849
|
+
absent and migrate a reconstructed value only in memory.
|
|
850
|
+
- Task-scoped `tx.doc()` validates against the transaction's latest candidate task
|
|
759
851
|
record, falling back to committed state. This internal validation is not a
|
|
760
852
|
caller table read and does not trigger `ReadAfterWrite`. Its conversation is
|
|
761
|
-
derived from the task record
|
|
853
|
+
derived from the task record.
|
|
854
|
+
- `retireDoc()` resolves the logical address without creating it. Retirement of
|
|
855
|
+
an acquired draft persists its final content before retirement. A later
|
|
856
|
+
`tx.doc()` at that address in the same transaction creates a new incarnation
|
|
857
|
+
with a new draft and ID.
|
|
762
858
|
- A terminal candidate rejects later task-document access. Terminal settlement
|
|
763
859
|
retires both existing task documents and task documents created earlier in the
|
|
764
860
|
same transaction.
|
|
765
861
|
- `snapshot()` returns a detached JSON copy.
|
|
766
|
-
- `documentSource()` returns an opaque committed
|
|
767
|
-
- `tx.doc()` returns
|
|
862
|
+
- `documentSource()` returns an opaque source bound to one committed incarnation.
|
|
863
|
+
- `tx.doc()` returns one revocable copy-on-write `Draft<T>` for the transaction.
|
|
768
864
|
- Historical reads never create documents in the past.
|
|
769
865
|
|
|
770
|
-
A family initializer input matters only on first creation. Later accesses must
|
|
771
|
-
supply the same logical target; their `initial` value is ignored.
|
|
772
|
-
|
|
773
866
|
`snapshotAsOf()` is available only for rewindable conversation documents. It
|
|
774
867
|
validates that `at` is visible through the requested conversation's ancestry,
|
|
775
868
|
selects the ancestor conversation that owns that entry, then finds the logical
|
|
@@ -784,51 +877,91 @@ alive at the target commit.
|
|
|
784
877
|
|
|
785
878
|
### 3.4 Mutation ownership
|
|
786
879
|
|
|
787
|
-
|
|
788
|
-
|
|
880
|
+
Pico uses Chord Delta's transaction shape directly:
|
|
881
|
+
|
|
882
|
+
```ts
|
|
883
|
+
interface Prepared<T extends object> {
|
|
884
|
+
readonly base: T;
|
|
885
|
+
readonly value: T;
|
|
886
|
+
readonly ops: readonly Op[];
|
|
887
|
+
}
|
|
888
|
+
interface Change<T extends object> {
|
|
889
|
+
readonly state: Draft<T>;
|
|
890
|
+
prepare(): Prepared<T>;
|
|
891
|
+
abort(): void;
|
|
892
|
+
}
|
|
893
|
+
interface Tracker<T extends object> {
|
|
894
|
+
readonly value: T;
|
|
895
|
+
beginChange(): Change<T>;
|
|
896
|
+
prepareReplace(value: T): Prepared<T>;
|
|
897
|
+
adopt(prepared: Prepared<T>): void;
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
One tracker has at most one open change. `prepare()` revokes the draft and returns
|
|
902
|
+
an inert branded result without changing `tracker.value`; `abort()` is
|
|
903
|
+
idempotent. `adopt()` accepts only a result from that tracker whose `base` is
|
|
904
|
+
still current. `ops.length === 0` implies `value === base`. Pico's Session line
|
|
905
|
+
ensures no tracker becomes stale between preparation, Storage settlement, and
|
|
906
|
+
adoption.
|
|
907
|
+
|
|
908
|
+
Each loaded document owns one tracker. The first `tx.doc()` acquisition calls
|
|
909
|
+
`tracker.beginChange()` and memoizes that change's draft for the rest of the
|
|
910
|
+
possibly async Session callback.
|
|
789
911
|
|
|
790
912
|
Transaction behavior:
|
|
791
913
|
|
|
792
914
|
```text
|
|
793
915
|
begin transaction
|
|
794
|
-
acquire document
|
|
795
|
-
mutate
|
|
796
|
-
callback
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
916
|
+
acquire and memoize document changes by logical address
|
|
917
|
+
mutate revocable copy-on-write drafts
|
|
918
|
+
callback settles
|
|
919
|
+
seal Tx and synchronously reject further draft/Tx use
|
|
920
|
+
if any acquisition is pending: abort open changes, reject, then drain and abort it
|
|
921
|
+
callback fails with no pending acquisition
|
|
922
|
+
abort every open change; persist and publish nothing
|
|
923
|
+
callback succeeds with no pending acquisition
|
|
924
|
+
prepare every open change -> immutable candidate + frozen Chord Op[]
|
|
925
|
+
normalize deep no-ops to the previous value + []
|
|
926
|
+
Session evaluates each required/ordinary document write exactly once
|
|
927
|
+
Storage.commit persists the atomic batch while the Session line remains held
|
|
803
928
|
storage succeeds
|
|
804
|
-
|
|
805
|
-
adopt committed baselines and enqueue value/ops publication while holding the line
|
|
929
|
+
adopt every prepared change and enqueue candidate/ops publication
|
|
806
930
|
release the line; invoke listeners later
|
|
807
|
-
|
|
808
|
-
poison Session
|
|
931
|
+
storage fails
|
|
932
|
+
poison Session and publish nothing
|
|
809
933
|
```
|
|
810
934
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
935
|
+
A pending acquisition that resolves after sealing never exposes a draft; its
|
|
936
|
+
change is aborted and its promise rejects. The Session observes every such
|
|
937
|
+
settlement before releasing the line.
|
|
938
|
+
|
|
939
|
+
`prepare()` performs all JSON, ownership, diff, and freezing work. Preparation or
|
|
940
|
+
checkpoint failure occurs before Storage admission and rolls back normally.
|
|
941
|
+
Adoption performs no diffing, allocation, or callback. The prepared candidate
|
|
942
|
+
itself becomes the immutable published value; Pico does not replay its operations
|
|
943
|
+
to construct another copy. An unchanged existing current-version document writes
|
|
944
|
+
and publishes nothing. Creation and required version transitions still write a
|
|
945
|
+
base when their prepared operation batch is empty; an equal-value version base
|
|
946
|
+
does not emit a watch update.
|
|
947
|
+
|
|
948
|
+
Values assigned into a draft are copied immediately by value. Repeated placements
|
|
949
|
+
are independent. Chord may structurally share unchanged immutable subtrees across
|
|
950
|
+
revisions. Operation tuples, paths, splice payload arrays, permutations, and the
|
|
951
|
+
outer batch are frozen before checkpoint code, Storage, or listeners can observe
|
|
952
|
+
them.
|
|
821
953
|
|
|
822
954
|
```ts
|
|
823
|
-
let escaped: LiveState
|
|
955
|
+
let escaped: Draft<LiveState>;
|
|
824
956
|
await session.commit(async tx => {
|
|
825
|
-
escaped = await tx.doc(LiveDoc,
|
|
957
|
+
escaped = await tx.doc(LiveDoc, conversationId);
|
|
826
958
|
});
|
|
827
|
-
escaped.message = message; //
|
|
959
|
+
escaped.message = message; // throws: the draft was revoked
|
|
828
960
|
```
|
|
829
961
|
|
|
830
|
-
|
|
831
|
-
|
|
962
|
+
Fire-and-forget work that mutates a draft before the owner callback settles may
|
|
963
|
+
silently enter that transaction and is unsupported. After settlement, draft and
|
|
964
|
+
`Tx` operations reject.
|
|
832
965
|
|
|
833
966
|
### 3.5 Bases and checkpoints
|
|
834
967
|
|
|
@@ -841,18 +974,19 @@ storage record is a base:
|
|
|
841
974
|
const useBase = definition.checkpointWhen?.(candidateValue, ops) ?? false;
|
|
842
975
|
```
|
|
843
976
|
|
|
844
|
-
The Session evaluates this predicate exactly once after tracker
|
|
845
|
-
and version transitions require bases and do not call it. The Session
|
|
846
|
-
Storage only the selected representation:
|
|
977
|
+
The Session evaluates this predicate exactly once after tracker preparation.
|
|
978
|
+
Creation and version transitions require bases and do not call it. The Session
|
|
979
|
+
then gives Storage only the selected representation:
|
|
847
980
|
|
|
848
981
|
```text
|
|
849
982
|
required or predicate true -> base with complete value
|
|
850
|
-
otherwise -> delta with
|
|
983
|
+
otherwise -> delta with the prepared Chord operation batch
|
|
851
984
|
```
|
|
852
985
|
|
|
853
|
-
A
|
|
854
|
-
|
|
855
|
-
|
|
986
|
+
A Chord root-replacement operation remains a delta unless the definition selected
|
|
987
|
+
a checkpoint; it does not authorize reclamation. Predicate failure aborts the
|
|
988
|
+
prepared transaction before Storage admission. Storage executes no definition
|
|
989
|
+
code and never receives an unused complete candidate with a selected delta.
|
|
856
990
|
|
|
857
991
|
- For Session, task, and latest conversation documents, a committed base permits
|
|
858
992
|
physical reclamation of older records.
|
|
@@ -887,16 +1021,16 @@ access-driven: `tx.doc()`, `snapshot()`, `snapshotAsOf()`, `documentSource()`, a
|
|
|
887
1021
|
`watchDoc()` reconstruct and migrate through the token supplied to that call.
|
|
888
1022
|
Harness open does not sweep ordinary documents.
|
|
889
1023
|
|
|
890
|
-
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
draft edits coalesce into one final required base.
|
|
1024
|
+
- Read-only access migrates only its returned in-memory value and never writes.
|
|
1025
|
+
`tx.doc()` stages migration in its enclosing transaction; callback failure
|
|
1026
|
+
persists nothing, and later draft edits coalesce into one final required base.
|
|
894
1027
|
- Rewindable history is not rewritten. Current and historical reconstructed
|
|
895
1028
|
values are migrated after replay.
|
|
896
|
-
- The first
|
|
897
|
-
current-version base
|
|
898
|
-
- A fork
|
|
899
|
-
|
|
1029
|
+
- The first `tx.doc()` transaction after any stored-version migration writes a
|
|
1030
|
+
required current-version base, even when the migrated JSON is deeply equal.
|
|
1031
|
+
- A fork obtains the selected stored value/version from Storage rather than a
|
|
1032
|
+
typed migrated tracker cache. The child copies that stored pair and migrates on
|
|
1033
|
+
later typed access.
|
|
900
1034
|
- Unaccessed documents and documents with unavailable definitions preserve their stored instances,
|
|
901
1035
|
versions, and bytes.
|
|
902
1036
|
|
|
@@ -909,7 +1043,7 @@ appended later entries. Document state at `E` is the final state of the commit
|
|
|
909
1043
|
containing `E`. Different document states require separate commits.
|
|
910
1044
|
|
|
911
1045
|
Each conversation document follows the history/fork policy persisted in its
|
|
912
|
-
|
|
1046
|
+
`DocumentRecord`:
|
|
913
1047
|
|
|
914
1048
|
| conversation setting | child value |
|
|
915
1049
|
|---|---|
|
|
@@ -933,24 +1067,23 @@ External model, process, tool, network, and human effects run outside it.
|
|
|
933
1067
|
|
|
934
1068
|
```ts
|
|
935
1069
|
await session.commit(async tx => {
|
|
936
|
-
const
|
|
937
|
-
const live = await tx.doc(LiveDoc,
|
|
1070
|
+
const task = await tx.task(taskId); // table read
|
|
1071
|
+
const live = await tx.doc(LiveDoc, conversationId);
|
|
938
1072
|
|
|
939
|
-
|
|
940
|
-
live.message
|
|
941
|
-
tx.
|
|
1073
|
+
await tx.appendEntry(conversationId, message); // first table write
|
|
1074
|
+
delete live.message; // document mutation remains valid
|
|
1075
|
+
tx.setTask(nextTask(task));
|
|
942
1076
|
}, context);
|
|
943
1077
|
```
|
|
944
1078
|
|
|
945
|
-
Mutation admission occurs on the Session line before a commit callback
|
|
946
|
-
get-or-create acquisition starts. Closing seals mutation admission and task
|
|
1079
|
+
Mutation admission occurs on the Session line before a commit callback starts. Closing seals mutation admission and task
|
|
947
1080
|
reservation. Already-admitted commits settle before storage closes. Once a
|
|
948
1081
|
commit is admitted, caller cancellation does not interrupt storage settlement or
|
|
949
1082
|
undo the commit. Cancelling a close wait does not reopen admission.
|
|
950
1083
|
|
|
951
1084
|
Table rules:
|
|
952
1085
|
|
|
953
|
-
- Tables are conversations, entries, tasks, and
|
|
1086
|
+
- Tables are conversations, entries, tasks, and submissions.
|
|
954
1087
|
- Table reads are allowed before the first table write.
|
|
955
1088
|
- Any table read after the first table write throws `ReadAfterWrite`.
|
|
956
1089
|
- Document access and read-your-writes remain available after table writes.
|
|
@@ -972,10 +1105,10 @@ Storage ownership:
|
|
|
972
1105
|
```ts
|
|
973
1106
|
type TaskOutcome<R> =
|
|
974
1107
|
| { readonly status: "completed"; readonly result: R }
|
|
975
|
-
| { readonly status: "failed"; readonly error:
|
|
1108
|
+
| { readonly status: "failed"; readonly error: TaskOutcomeError; readonly result?: R }
|
|
976
1109
|
| { readonly status: "aborted"; readonly reason?: string; readonly result?: R }
|
|
977
1110
|
| { readonly status: "orphaned"; readonly reason: string }
|
|
978
|
-
| { readonly status: "faulted"; readonly error:
|
|
1111
|
+
| { readonly status: "faulted"; readonly error: TaskOutcomeError };
|
|
979
1112
|
|
|
980
1113
|
type TaskState<S, R> =
|
|
981
1114
|
| { readonly status: "pending"; readonly checkpoint: S }
|
|
@@ -1130,7 +1263,7 @@ A terminal transition atomically:
|
|
|
1130
1263
|
1. Writes the terminal task record.
|
|
1131
1264
|
2. Appends any result entries.
|
|
1132
1265
|
3. Retires all documents scoped to that task.
|
|
1133
|
-
4. Resolves any
|
|
1266
|
+
4. Resolves any submissions settled by the task.
|
|
1134
1267
|
|
|
1135
1268
|
The execution checkpoint and memos disappear from the terminal representation.
|
|
1136
1269
|
Terminal records remain queryable for dependencies, waiters, inspection, and
|
|
@@ -1169,50 +1302,52 @@ Initial task definitions are registered before open performs live-task migration
|
|
|
1169
1302
|
and orphan reconciliation. Dynamic registration begins only after that pass.
|
|
1170
1303
|
Document migration remains access-driven. Unknown or unmigratable live task kinds
|
|
1171
1304
|
become terminal `orphaned`;
|
|
1172
|
-
affected
|
|
1305
|
+
affected input submissions become unanswered, any matching active turn control is cleared,
|
|
1173
1306
|
task-scoped documents retire, and a visible notice entry is appended in one
|
|
1174
|
-
commit. Faulting a turn task performs the same control/
|
|
1307
|
+
commit. Faulting a turn task performs the same control/submission cleanup with a
|
|
1175
1308
|
`faulted` outcome.
|
|
1176
1309
|
|
|
1177
|
-
## 6.
|
|
1310
|
+
## 6. Submissions and inbox
|
|
1178
1311
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1312
|
+
Submission records back awaitable host objects. Their record transitions use
|
|
1313
|
+
Session-private transaction operations, not the public `Tx` interface. The inbox
|
|
1314
|
+
itself is an ordered conversation document containing tagged items:
|
|
1181
1315
|
|
|
1182
1316
|
```ts
|
|
1183
1317
|
type InboxItem =
|
|
1184
|
-
| { readonly id: Id; readonly mode: "steer" | "followUp"; readonly
|
|
1318
|
+
| { readonly id: Id; readonly mode: "steer" | "followUp"; readonly message: Message }
|
|
1185
1319
|
| { readonly id: Id; readonly mode: "write"; readonly entry: EntryDraft };
|
|
1186
1320
|
```
|
|
1187
1321
|
|
|
1188
1322
|
A built-in turn-control document has an optional `active` value naming the task
|
|
1189
|
-
currently responsible for the turn and its placed input IDs. `active !==
|
|
1323
|
+
currently responsible for the turn and its placed input-submission IDs. `active !==
|
|
1190
1324
|
undefined` defines `busy`; get-or-create of the idle document does not. The
|
|
1191
1325
|
value remains active while generation, tools, and post-tools hand work to one
|
|
1192
1326
|
another.
|
|
1193
1327
|
|
|
1194
1328
|
Admission and terminal transitions:
|
|
1195
1329
|
|
|
1196
|
-
| action |
|
|
1330
|
+
| action | submission state | other writes |
|
|
1197
1331
|
|---|---|---|
|
|
1198
|
-
| idle
|
|
1199
|
-
| busy
|
|
1200
|
-
| idle
|
|
1201
|
-
| busy
|
|
1332
|
+
| idle input submission | `placed`, with user entry | create turn controller/generation |
|
|
1333
|
+
| busy input submission | `queued` | append steer/follow-up inbox item |
|
|
1334
|
+
| idle write submission | `done`, with entry | append entry; no turn |
|
|
1335
|
+
| busy write submission | `queued` | append write inbox item |
|
|
1202
1336
|
| boundary places user item | `placed`, with entry | add ID to current/successor turn |
|
|
1203
1337
|
| boundary places write | `done`, with entry | append entry |
|
|
1204
|
-
| turn answers | `done`, with answer entry | clear/hand off turn controller |
|
|
1205
|
-
| turn fails or aborts | `unanswered`, with reason | clear/hand off turn controller |
|
|
1338
|
+
| turn answers | input `done`, with required answer entry | clear/hand off turn controller |
|
|
1339
|
+
| turn fails or aborts | input `unanswered`, with reason | clear/hand off turn controller |
|
|
1206
1340
|
| withdraw queued item | `unanswered`, reason `aborted` | remove inbox item |
|
|
1207
1341
|
| stale item | `unanswered`, reason `stale` | remove inbox item |
|
|
1208
1342
|
|
|
1209
|
-
`requestId` deduplicates within one conversation before any write
|
|
1210
|
-
with `whenBusy: "reject"`
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1343
|
+
`requestId` deduplicates within one conversation before any write; reusing one
|
|
1344
|
+
for the other submission type rejects. A busy input with `whenBusy: "reject"`
|
|
1345
|
+
writes no record and reports `ConversationBusy`. Before an idle input places its
|
|
1346
|
+
own entry, it runs a final boundary to drain older eligible queued items. A
|
|
1347
|
+
`Submission` waits until `done` or `unanswered`; abort withdraws only a still-
|
|
1348
|
+
queued submission, reports `already_placed` for a placed input, and reports
|
|
1349
|
+
`settled` for any terminal submission. Conversation abort withdraws queued steer/follow-up submissions but keeps writes
|
|
1350
|
+
for later placement.
|
|
1216
1351
|
|
|
1217
1352
|
Boundary selection is deterministic by item ID:
|
|
1218
1353
|
|
|
@@ -1221,22 +1356,21 @@ Boundary selection is deterministic by item ID:
|
|
|
1221
1356
|
| `postTools` | all | first/all by mode | none |
|
|
1222
1357
|
| `final` | all | first/all by mode | first/all by mode |
|
|
1223
1358
|
|
|
1224
|
-
A queued self-head write cuts older pending user items: those
|
|
1359
|
+
A queued self-head write cuts older pending user items: those submissions become
|
|
1225
1360
|
stale, the write is placed, and the current turn terminates. Other head writes
|
|
1226
1361
|
whose target predates the caller's newest known head are stale.
|
|
1227
1362
|
|
|
1228
1363
|
At ordinary `postTools`, generation continues even with no queued trigger;
|
|
1229
1364
|
selected steer IDs join that continuation. A terminating/handoff post-tools
|
|
1230
1365
|
boundary uses final behavior instead. At `final`, the current turn's placed
|
|
1231
|
-
|
|
1366
|
+
input submissions settle first; selected user IDs start one successor generation. Writes
|
|
1232
1367
|
never trigger generation by themselves. A final boundary without continuation
|
|
1233
1368
|
or user triggers leaves the conversation idle.
|
|
1234
1369
|
|
|
1235
1370
|
Selected and stale items are removed positionally while retained item order is
|
|
1236
|
-
preserved.
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
for positional inbox semantics.
|
|
1371
|
+
preserved. Chord's revision differ must express scattered removals without
|
|
1372
|
+
carrying retained values; IDs are not substituted for positional inbox
|
|
1373
|
+
semantics.
|
|
1240
1374
|
|
|
1241
1375
|
## 7. Hooks, tools, and system sections
|
|
1242
1376
|
|
|
@@ -1279,7 +1413,7 @@ type ToolExecutionResult = {
|
|
|
1279
1413
|
|
|
1280
1414
|
interface OwnedConversation {
|
|
1281
1415
|
readonly id: Id;
|
|
1282
|
-
|
|
1416
|
+
submit(submission: InputSubmissionDraft, context: Context): Promise<Submission>;
|
|
1283
1417
|
abort(context: Context): Promise<void>;
|
|
1284
1418
|
waitForIdle(context: Context): Promise<void>;
|
|
1285
1419
|
}
|
|
@@ -1336,9 +1470,9 @@ does not merge keys. Its promise resolves after the corresponding or coalesced
|
|
|
1336
1470
|
document commit. During normal settlement, accepted output updates drain before
|
|
1337
1471
|
the tool-result entry and terminal task record commit. Abort and close obey
|
|
1338
1472
|
invocation and Session admission gates: uncommitted buffered updates may be
|
|
1339
|
-
discarded, while admitted commits settle.
|
|
1340
|
-
|
|
1341
|
-
follows the fatal Session rule.
|
|
1473
|
+
discarded, while admitted commits settle. Cancellation, callback, tracker
|
|
1474
|
+
preparation, and checkpoint failures occur before Storage admission and do not
|
|
1475
|
+
poison the Session. An uncertain Storage failure follows the fatal Session rule.
|
|
1342
1476
|
|
|
1343
1477
|
Tools are dynamically registered declarations with name, description, JSON
|
|
1344
1478
|
schema, replay policy, and execute function. A tool call is accepted only if it
|
|
@@ -1349,6 +1483,9 @@ After hooks and validation, the tool task durably records the final call and
|
|
|
1349
1483
|
resolved replay policy before execution. Recovery does not rerun `beforeTool`
|
|
1350
1484
|
and does not let a changed registry declaration alter that stored policy.
|
|
1351
1485
|
|
|
1486
|
+
An owned conversation accepts only input submissions; tools use ordinary
|
|
1487
|
+
transaction writes for passive entries.
|
|
1488
|
+
|
|
1352
1489
|
A tool executes in a durable task. It may:
|
|
1353
1490
|
|
|
1354
1491
|
- write bounded progress/output to a presentation or task-scoped document;
|
|
@@ -1369,8 +1506,8 @@ newest committed visible entry as `parent.at`; if no entry exists, it creates no
|
|
|
1369
1506
|
history parent. Conversation documents then apply section 3.7 at that entry, and
|
|
1370
1507
|
explicit model/section seeds override their inherited built-in values. With no
|
|
1371
1508
|
parent, documents initialize normally. That handle's
|
|
1372
|
-
operations reject after the invocation ends.
|
|
1373
|
-
`
|
|
1509
|
+
operations reject after the invocation ends. A `Submission` returned by its
|
|
1510
|
+
`submit()` is invocation-bound in the same way; the admitted submission remains
|
|
1374
1511
|
durable after those methods reject. Invocation-owned document watches stop when
|
|
1375
1512
|
the invocation ends.
|
|
1376
1513
|
|
|
@@ -1437,9 +1574,9 @@ existing section; `null` removes it, and a later re-addition appends it to the
|
|
|
1437
1574
|
ordered section map. Within one message, tool removals happen before additions,
|
|
1438
1575
|
so a same-name replacement gets the new declaration and position.
|
|
1439
1576
|
|
|
1440
|
-
The built-in configuration document
|
|
1441
|
-
|
|
1442
|
-
the end. Preparation compares both values and order. If values can be patched
|
|
1577
|
+
The built-in configuration document stores sections as an ordered array, never
|
|
1578
|
+
as an object whose key order must be inferred by Delta. `setSection(section,
|
|
1579
|
+
undefined)` removes the record; setting it later appends it at the end. Preparation compares both values and order. If values can be patched
|
|
1443
1580
|
without changing order, it emits the minimal patch. If effective and desired
|
|
1444
1581
|
section order differ, one commit appends two `pi.system` entries: the first
|
|
1445
1582
|
removes every effective section with `null`, and the second re-adds every desired
|
|
@@ -1541,9 +1678,46 @@ does not delete transcript history.
|
|
|
1541
1678
|
|
|
1542
1679
|
### 9.1 Document source
|
|
1543
1680
|
|
|
1681
|
+
Chord exposes this source-adoption contract from its replicated-state layer:
|
|
1682
|
+
|
|
1544
1683
|
```ts
|
|
1684
|
+
interface ReplicatedStateSourceFrame<T> {
|
|
1685
|
+
readonly cursor: number;
|
|
1686
|
+
readonly value: T;
|
|
1687
|
+
readonly ops: readonly Op[];
|
|
1688
|
+
readonly context: Context;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
interface ReplicatedStateSourceAttachment<T> {
|
|
1692
|
+
/** Fixed immutable snapshot captured at the atomic attachment boundary. */
|
|
1693
|
+
readonly snapshot: { readonly value: T; readonly cursor: number };
|
|
1694
|
+
/** Install the sole listener and synchronously drain every buffered frame. */
|
|
1695
|
+
activate(listener: (frame: ReplicatedStateSourceFrame<T>) => void): void;
|
|
1696
|
+
dispose(): void;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
interface ReplicatedStateSource<T> {
|
|
1700
|
+
/** Atomically capture a snapshot and begin buffering every later commit. */
|
|
1701
|
+
attach(): ReplicatedStateSourceAttachment<T>;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
interface ReplicatedStateSourceOptions {
|
|
1705
|
+
readonly onError?: (error: Error) => void;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
interface AttachedReplicatedState<T> extends ReplicatedState<T> {
|
|
1709
|
+
readonly value: T;
|
|
1710
|
+
dispose(): void;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function replicatedState<T>(
|
|
1714
|
+
source: ReplicatedStateSource<T>,
|
|
1715
|
+
options?: ReplicatedStateSourceOptions,
|
|
1716
|
+
): AttachedReplicatedState<T>;
|
|
1717
|
+
|
|
1545
1718
|
declare const documentSourceType: unique symbol;
|
|
1546
|
-
interface DocumentSource<T extends JsonObject>
|
|
1719
|
+
interface DocumentSource<T extends JsonObject>
|
|
1720
|
+
extends ReplicatedStateSource<Readonly<T> | null> {
|
|
1547
1721
|
readonly [documentSourceType]: T;
|
|
1548
1722
|
}
|
|
1549
1723
|
|
|
@@ -1565,52 +1739,52 @@ interface WatchHandle<T> {
|
|
|
1565
1739
|
type DocumentWatch<T extends JsonObject> = WatchHandle<Readonly<T> | null>;
|
|
1566
1740
|
|
|
1567
1741
|
interface DocumentObserver {
|
|
1568
|
-
watchDoc<T extends JsonObject>(token:
|
|
1569
|
-
watchDoc<T extends JsonObject
|
|
1742
|
+
watchDoc<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1743
|
+
watchDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: Id, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1744
|
+
watchDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: Id, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1745
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1746
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: Id, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1747
|
+
watchDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: Id, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
|
|
1570
1748
|
}
|
|
1571
1749
|
```
|
|
1572
1750
|
|
|
1573
|
-
`DocumentSource` is opaque
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
Each Chord adapter assigns its own in-memory contiguous delivery sequence; Pico
|
|
1595
|
-
does not persist or expose that sequence through `WatchHandle`. Hydration
|
|
1596
|
-
atomically captures a matching immutable value and adapter sequence before
|
|
1597
|
-
subsequent source operations are delivered. Reopen creates a new source
|
|
1598
|
-
lifetime and hydration.
|
|
1751
|
+
`DocumentSource` is an opaque Chord-recognized `ReplicatedStateSource`. A Chord
|
|
1752
|
+
replicated state adopts it directly and forwards its committed immutable value
|
|
1753
|
+
and operations without another tracker or re-diff. `attach()` is one synchronous
|
|
1754
|
+
boundary: `snapshot.value` includes every commit through `snapshot.cursor`, and
|
|
1755
|
+
the attachment buffers only later frames. `activate()` installs the sole listener
|
|
1756
|
+
and synchronously drains those frames in contiguous cursor order before returning.
|
|
1757
|
+
An operation already covered by the snapshot is never redelivered. Source-backed
|
|
1758
|
+
state is publication-only and Pico remains its sole mutator.
|
|
1759
|
+
|
|
1760
|
+
Source and watch acquisition never create; absent lookup returns `undefined`.
|
|
1761
|
+
Successful acquisition binds one concrete incarnation. Retirement publishes a
|
|
1762
|
+
JSON `null` replacement and ends that incarnation's stream. The service may then
|
|
1763
|
+
withdraw itself; if it remains exposed, consumers see `null`, never stale state.
|
|
1764
|
+
If the incarnation retires before attachment, attachment hydrates terminal
|
|
1765
|
+
`null`; it never binds a replacement incarnation. A later recreation requires
|
|
1766
|
+
acquiring a new source/watch.
|
|
1767
|
+
|
|
1768
|
+
Each adopted replicated state assigns its own in-memory contiguous delivery
|
|
1769
|
+
sequence; Pico does not persist or expose that sequence through `WatchHandle`.
|
|
1770
|
+
A live source or watch pins its incarnation's loaded tracker; eviction begins only
|
|
1771
|
+
after every attachment ends. Reopen creates a new source lifetime and hydration.
|
|
1599
1772
|
|
|
1600
1773
|
### 9.2 `watchDoc`
|
|
1601
1774
|
|
|
1602
|
-
Tasks, hooks, and tools may observe any document for which their code
|
|
1603
|
-
and
|
|
1775
|
+
Tasks, hooks, and tools may observe any existing document for which their code
|
|
1776
|
+
has a token and owner/key. There is no additional subtree permission system inside trusted
|
|
1604
1777
|
Session code.
|
|
1605
1778
|
|
|
1606
|
-
`watchDoc()` is available on task, hook, and tool APIs.
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1779
|
+
`watchDoc()` is available on task, hook, and tool APIs. On the Session line,
|
|
1780
|
+
acquisition resolves one existing concrete incarnation, captures its immutable
|
|
1781
|
+
committed value, and registers the handle for every later committed operation
|
|
1782
|
+
batch. It returns `undefined` when absent. `watch.value` is that fixed acquisition snapshot and
|
|
1610
1783
|
never changes.
|
|
1611
1784
|
|
|
1612
1785
|
```ts
|
|
1613
|
-
const watch = await api.watchDoc(JobOutputDoc,
|
|
1786
|
+
const watch = await api.watchDoc(JobOutputDoc, producerTaskId, context);
|
|
1787
|
+
if (watch === undefined) return;
|
|
1614
1788
|
let value = watch.value;
|
|
1615
1789
|
try {
|
|
1616
1790
|
await initializeConsumer(value, context);
|
|
@@ -1656,13 +1830,11 @@ accumulating an unbounded operation list. This bounds delta bookkeeping, not the
|
|
|
1656
1830
|
document value itself. This watch observes convergent committed state, not every
|
|
1657
1831
|
intermediate transition.
|
|
1658
1832
|
|
|
1659
|
-
The replacement value is the immutable
|
|
1660
|
-
to the last batch compacted into it.
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
share the immutable publication value and operation payloads. A mutable consumer
|
|
1665
|
-
must detach them before using mutable `apply()` or `track()`.
|
|
1833
|
+
The replacement value is the prepared immutable candidate corresponding exactly
|
|
1834
|
+
to the last batch compacted into it. Pico adopts and publishes that candidate
|
|
1835
|
+
after storage succeeds; it never exposes a draft or borrowed storage value.
|
|
1836
|
+
Watches may share the immutable candidate and frozen operation payloads. A
|
|
1837
|
+
mutable consumer must detach them before using mutable `apply()` or `track()`.
|
|
1666
1838
|
|
|
1667
1839
|
A watch remains bound to its original incarnation. Retirement replaces the
|
|
1668
1840
|
pending suffix with `[["r", null]]`. Before start, `value` remains the original
|
|
@@ -1674,9 +1846,8 @@ the reset follows any in-flight callback. Successful terminal delivery closes as
|
|
|
1674
1846
|
prevents another callback from starting, and signals the watch delivery context.
|
|
1675
1847
|
An in-flight callback is allowed to settle; `closed` resolves only afterward.
|
|
1676
1848
|
The acquisition `Context` governs the watch lifetime. Cancellation during
|
|
1677
|
-
acquisition cleans up any registration before rejecting
|
|
1678
|
-
|
|
1679
|
-
after successful acquisition may therefore return an already-stopped handle,
|
|
1849
|
+
acquisition cleans up any registration before rejecting. Cancellation immediately
|
|
1850
|
+
after successful acquisition may return an already-stopped handle,
|
|
1680
1851
|
whose `start()` throws and whose `closed` reports `cancelled`. Invocation
|
|
1681
1852
|
termination and Session close stop owned watches similarly. Listener rejection
|
|
1682
1853
|
is reported, discards pending work, and closes only that watch as
|
|
@@ -1707,7 +1878,7 @@ document op ["s", ["message"], value]
|
|
|
1707
1878
|
```
|
|
1708
1879
|
|
|
1709
1880
|
Entry appends/head changes and every changed mounted document are included in
|
|
1710
|
-
the same publication. The mount owns no
|
|
1881
|
+
the same publication. The mount owns no document revision producer and performs no semantic
|
|
1711
1882
|
projection. It materializes one immutable published view per batch with
|
|
1712
1883
|
`applyImmutable(previousView, mountedOps)` so every conversation watch can share
|
|
1713
1884
|
that value for reset compaction. A Chord adapter assigns a contiguous in-memory
|
|
@@ -1730,20 +1901,20 @@ compaction. It owns no tracker or persistence and emits notifications only after
|
|
|
1730
1901
|
the commit that makes them true.
|
|
1731
1902
|
|
|
1732
1903
|
The adapter protocol covers run start/settlement, committed assistant progress,
|
|
1733
|
-
message entry settlement, tool intent/progress/result,
|
|
1904
|
+
message entry settlement, tool intent/progress/result, submission queue/outcome,
|
|
1734
1905
|
retry/deferred/compaction state, configuration changes, and faults. One commit
|
|
1735
1906
|
may produce an ordered batch. Progress notifications represent Pico's throttled
|
|
1736
1907
|
durable partials, not every raw provider frame. The exact legacy `AgentEvent`
|
|
1737
1908
|
wire format is not preserved.
|
|
1738
1909
|
|
|
1739
1910
|
Notifications have no hydration or replay contract. A consumer requiring a
|
|
1740
|
-
complete lifecycle subscribes before admitting the
|
|
1911
|
+
complete lifecycle subscribes before admitting the submission; a late or reconnecting
|
|
1741
1912
|
consumer hydrates structural state and history instead. Product adapters apply
|
|
1742
1913
|
these rules:
|
|
1743
1914
|
|
|
1744
1915
|
- TUI hydrates and renders `ConversationView`, then applies structural updates;
|
|
1745
1916
|
notifications may drive transient animation but are not its authority.
|
|
1746
|
-
- Print awaits its
|
|
1917
|
+
- Print awaits its input `Submission` and prints that submission's answer.
|
|
1747
1918
|
- JSON/RPC expose correlated commands plus the ordered agent notification
|
|
1748
1919
|
protocol, with transport backpressure and disconnect policy owned by that
|
|
1749
1920
|
adapter.
|
|
@@ -1796,23 +1967,17 @@ type DocumentContent =
|
|
|
1796
1967
|
| { readonly version: number; readonly kind: "base"; readonly value: JsonObject }
|
|
1797
1968
|
| { readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
|
|
1798
1969
|
|
|
1799
|
-
type StoredDocumentRecord =
|
|
1800
|
-
| { readonly seq: Seq; readonly version: number; readonly kind: "base"; readonly value: JsonObject }
|
|
1801
|
-
| { readonly seq: Seq; readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
|
|
1802
|
-
|
|
1803
1970
|
type StoredDocument = {
|
|
1804
1971
|
readonly record: DocumentRecord;
|
|
1805
|
-
readonly
|
|
1806
|
-
|
|
1807
|
-
...Extract<StoredDocumentRecord, { kind: "delta" }>[],
|
|
1808
|
-
];
|
|
1972
|
+
readonly version: number;
|
|
1973
|
+
readonly value: JsonObject;
|
|
1809
1974
|
};
|
|
1810
1975
|
|
|
1811
1976
|
type StorageWrite =
|
|
1812
1977
|
| { readonly type: "conversation"; readonly value: ConversationRecord }
|
|
1813
1978
|
| { readonly type: "entry"; readonly value: EntryRecord }
|
|
1814
1979
|
| { readonly type: "task"; readonly value: TaskRecord<JsonValue, JsonValue, JsonValue> }
|
|
1815
|
-
| { readonly type: "
|
|
1980
|
+
| { readonly type: "submission"; readonly value: SubmissionRecord }
|
|
1816
1981
|
| {
|
|
1817
1982
|
readonly type: "document.create";
|
|
1818
1983
|
readonly record: DocumentCreate;
|
|
@@ -1828,12 +1993,13 @@ type StorageWrite =
|
|
|
1828
1993
|
/**
|
|
1829
1994
|
* Trusts the owning Session to supply semantically valid records, references,
|
|
1830
1995
|
* ancestry, and transitions. Enforces atomicity, global ID ownership, immutable
|
|
1831
|
-
* conversation/entry creation, document
|
|
1832
|
-
* values; Session serializes commits.
|
|
1996
|
+
* conversation/entry creation, document record consistency, and detached
|
|
1997
|
+
* values; Session serializes commits. Sequences strictly increase but may have
|
|
1998
|
+
* gaps. Once commit() resolves, later reads through that Storage observe it.
|
|
1833
1999
|
*/
|
|
1834
2000
|
interface Storage {
|
|
1835
2001
|
commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
|
|
1836
|
-
mintId(): Id
|
|
2002
|
+
mintId(): Promise<Id>;
|
|
1837
2003
|
|
|
1838
2004
|
conversation(id: Id, context: Context): Promise<ConversationRecord | undefined>;
|
|
1839
2005
|
scanConversations(cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<ConversationRecord, Cursor>>;
|
|
@@ -1845,8 +2011,8 @@ interface Storage {
|
|
|
1845
2011
|
task(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
|
|
1846
2012
|
scanTasks(query: TaskQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
|
|
1847
2013
|
|
|
1848
|
-
|
|
1849
|
-
|
|
2014
|
+
submission(id: Id, context: Context): Promise<SubmissionRecord | undefined>;
|
|
2015
|
+
submissionByRequest(conversationId: Id, requestId: string, context: Context): Promise<SubmissionRecord | undefined>;
|
|
1850
2016
|
|
|
1851
2017
|
findDocument(address: DocumentAddress, at: DocumentPoint, context: Context): Promise<DocumentRecord | undefined>;
|
|
1852
2018
|
document(id: Id, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined>;
|
|
@@ -1878,15 +2044,19 @@ ascending incarnation IDs. There is no ordinary open-time all-document scan.
|
|
|
1878
2044
|
Task queries support conversation, kind, live/terminal status, abort mark, and
|
|
1879
2045
|
background status.
|
|
1880
2046
|
|
|
1881
|
-
`document(id, at)`
|
|
1882
|
-
|
|
1883
|
-
`
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2047
|
+
`document(id, at)` materializes one specific incarnation and never follows a
|
|
2048
|
+
replacement at the same logical address. Callers resolve an address with
|
|
2049
|
+
`findDocument()` when they do not already hold an incarnation ID. It selects the
|
|
2050
|
+
newest applicable base, applies its ordered Chord delta tail, and returns the detached materialized value plus stored
|
|
2051
|
+
definition version. Base/delta records are backend-private. The lookup never
|
|
2052
|
+
scans unrelated documents. An unknown ID returns `undefined`. At `"current"`, a
|
|
2053
|
+
retired incarnation returns `undefined`. A numeric lookup of a rewindable
|
|
2054
|
+
conversation incarnation returns `undefined` outside its half-open lifetime and
|
|
2055
|
+
reconstructs the selected value inside it. A numeric lookup of a known current-
|
|
2056
|
+
only incarnation rejects rather than depending on reclaimed content. Metadata
|
|
2057
|
+
membership remains queryable historically. A missing required base, a version
|
|
2058
|
+
change inside a delta tail, or an operation that cannot be applied inside an
|
|
2059
|
+
addressable lifetime is storage corruption, not absence.
|
|
1890
2060
|
|
|
1891
2061
|
One normalized batch contains at most one create/change content command per
|
|
1892
2062
|
incarnation and may also retire that incarnation. Storage applies content before
|
|
@@ -1911,8 +2081,8 @@ after a committed base or retirement.
|
|
|
1911
2081
|
|
|
1912
2082
|
One SQL transaction is one Session commit. SQLite stores:
|
|
1913
2083
|
|
|
1914
|
-
- conversation, entry, task, and
|
|
1915
|
-
- document
|
|
2084
|
+
- conversation, entry, task, and submission records;
|
|
2085
|
+
- document records;
|
|
1916
2086
|
- indexed document bases/deltas by document and commit sequence.
|
|
1917
2087
|
|
|
1918
2088
|
Live task transitions replace one row. Terminal tasks remain as small records.
|
|
@@ -1933,7 +2103,7 @@ must not be exposed directly by reads. A JSONL backend cannot simply add file
|
|
|
1933
2103
|
appends around aliasing memory tables.
|
|
1934
2104
|
|
|
1935
2105
|
```text
|
|
1936
|
-
main.jsonl table writes,
|
|
2106
|
+
main.jsonl table writes, document records, and one marker per commit
|
|
1937
2107
|
doc-<id>.jsonl one document incarnation
|
|
1938
2108
|
task-<id>.jsonl live task replacements
|
|
1939
2109
|
```
|
|
@@ -1968,19 +2138,19 @@ the initial implementation.
|
|
|
1968
2138
|
|
|
1969
2139
|
These are contracts, not invitations to add defensive machinery:
|
|
1970
2140
|
|
|
1971
|
-
- **
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
mutate the original value. The tracker owns it.
|
|
2141
|
+
- **Detached draft work:** drafts and bound array methods are revoked after the
|
|
2142
|
+
Session callback settles. Fire-and-forget work that runs before settlement can
|
|
2143
|
+
still mutate the active transaction and is unsupported.
|
|
1975
2144
|
- **Read after write:** read every required table row before the first table
|
|
1976
2145
|
write. Document drafts remain usable afterward; table reads do not.
|
|
1977
2146
|
- **Long transactions:** an async commit callback holds the Session mutation
|
|
1978
2147
|
line. Never await models, tools, processes, network calls, humans, a nested
|
|
1979
2148
|
Session commit, or a Session waiter inside it. Use methods on the current `Tx`.
|
|
1980
|
-
- **
|
|
1981
|
-
|
|
1982
|
-
- **Family initialization:**
|
|
1983
|
-
|
|
2149
|
+
- **Explicit creation:** only typed `tx.doc()` creates an absent document. Snapshot,
|
|
2150
|
+
source, and watch lookup return `undefined` instead.
|
|
2151
|
+
- **Family initialization:** the first acquisition of an absent family address
|
|
2152
|
+
selects its seed. Existing instances and later calls ignore seeds; a seed is
|
|
2153
|
+
neither identity nor an update.
|
|
1984
2154
|
- **Checkpoint starvation:** if `checkpointWhen()` never returns true, replay
|
|
1985
2155
|
and current-only document storage can grow without bound while the document is
|
|
1986
2156
|
live.
|
|
@@ -2020,7 +2190,6 @@ Pico5 initially has no:
|
|
|
2020
2190
|
- whole-Session DOM;
|
|
2021
2191
|
- visible-undurable publication;
|
|
2022
2192
|
- Session-kernel semantic event journal or independently maintained event state;
|
|
2023
|
-
- transaction membrane;
|
|
2024
2193
|
- session-scoped rewindable documents;
|
|
2025
2194
|
- automatic checkpoint heuristic;
|
|
2026
2195
|
- automatic third-party view mounting;
|