@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.
Files changed (38) hide show
  1. package/README.md +29 -0
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/{memory-storage.d.ts → storage/memory.d.ts} +12 -6
  7. package/dist/storage/memory.d.ts.map +1 -0
  8. package/dist/{memory-storage.js → storage/memory.js} +236 -24
  9. package/dist/storage/memory.js.map +1 -0
  10. package/dist/storage/sqlite/database.d.ts +26 -0
  11. package/dist/storage/sqlite/database.d.ts.map +1 -0
  12. package/dist/storage/sqlite/database.js +2 -0
  13. package/dist/storage/sqlite/database.js.map +1 -0
  14. package/dist/storage/sqlite/index.d.ts +4 -0
  15. package/dist/storage/sqlite/index.d.ts.map +1 -0
  16. package/dist/storage/sqlite/index.js +3 -0
  17. package/dist/storage/sqlite/index.js.map +1 -0
  18. package/dist/storage/sqlite/migrations.d.ts +11 -0
  19. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  20. package/dist/storage/sqlite/migrations.js +104 -0
  21. package/dist/storage/sqlite/migrations.js.map +1 -0
  22. package/dist/storage/sqlite/node.d.ts +25 -0
  23. package/dist/storage/sqlite/node.d.ts.map +1 -0
  24. package/dist/storage/sqlite/node.js +97 -0
  25. package/dist/storage/sqlite/node.js.map +1 -0
  26. package/dist/storage/sqlite/storage.d.ts +45 -0
  27. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  28. package/dist/storage/sqlite/storage.js +524 -0
  29. package/dist/storage/sqlite/storage.js.map +1 -0
  30. package/dist/types.d.ts +113 -34
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/types.js.map +1 -1
  33. package/docs/pico-v5-chord-usage.md +64 -62
  34. package/docs/pico-v5-handoff.md +92 -57
  35. package/docs/pico-v5.md +519 -350
  36. package/package.json +20 -3
  37. package/dist/memory-storage.d.ts.map +0 -1
  38. 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 { Context, JsonValue } from "@earendil-works/chord";
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 StoredError = { message: string; detail?: JsonValue };
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, inputs,
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 and assigned mutable objects must not escape their
54
- transaction. This restriction is initially documented, not enforced by a
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. A failure after document flush, including checkpoint or storage failure, is
59
- fatal to the open Session. It publishes nothing and must be reopened.
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 InputBase = {
132
+ type SubmissionRecordBase = {
127
133
  readonly id: Id;
128
134
  readonly conversationId: Id;
129
135
  readonly requestId?: string;
130
136
  };
131
137
 
132
- type Input = InputBase & (
133
- | { readonly status: "queued" }
134
- | { readonly status: "placed"; readonly entry: Id }
135
- | {
136
- readonly status: "done";
137
- readonly entry: Id;
138
- readonly answer?: Id; // absent for a completed passive write
139
- }
140
- | {
141
- readonly status: "unanswered";
142
- readonly entry?: Id; // present when placement preceded failure
143
- readonly reason: string;
144
- readonly detail?: JsonValue;
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 InputCreate = Input extends infer Record
149
- ? Record extends Input
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 SendInput = {
207
- readonly content: UserInput;
255
+ type SubmissionDraft = {
208
256
  readonly requestId?: string;
209
- readonly whenBusy?: "steer" | "followUp" | "reject";
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 SettledInput = Input & {
320
+ type SettledSubmissionRecord = SubmissionRecord & {
259
321
  readonly status: "done" | "unanswered";
260
322
  };
261
323
 
262
- interface InputHandle {
324
+ interface Submission {
263
325
  readonly id: Id;
264
- result(context: Context): Promise<Input | undefined>;
265
- wait(context: Context): Promise<SettledInput>;
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
- send(input: SendInput, context: Context): Promise<InputHandle>;
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
- abortInput(
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. `write()` now returns an
379
- `InputHandle`, because a busy passive write may remain queued before it receives
380
- an entry ID.
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 admission. For a fork,
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 values, and active tool names. Its initial thinking level
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 inputs
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 non-write inputs, marks
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
- `send()` and `write()` return after durable admission, not settlement. `send`
484
- creates a user message with the admission timestamp. `whenBusy` defaults to
485
- `followUp`. `write()` uses the ordered passive path in section 6 and never starts
486
- generation. A completed passive write identifies its entry in `Input.entry`; a
487
- completed send identifies the answer in `Input.answer`. Cancelling `wait()` only
488
- cancels that wait. It does not withdraw or abort the input; `InputHandle.abort()`
489
- is the explicit queued-input withdrawal operation.
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(input: I): T;
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: DocDefinition<T>;
618
+ interface DocToken<T extends JsonObject, D extends DocDefinition<T>> {
619
+ readonly definition: D;
573
620
  readonly [docType]?: T;
574
621
  }
575
- interface RewindableDocToken<T extends JsonObject> extends DocToken<T> {
576
- readonly definition: CommonDocDefinition<T> & RewindableConversationSemantics;
577
- }
578
- interface DocFamilyToken<T extends JsonObject, I extends JsonValue> {
579
- readonly definition: DocFamilyDefinition<T, I>;
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
- interface RewindableDocFamilyToken<T extends JsonObject, I extends JsonValue>
583
- extends DocFamilyToken<T, I> {
584
- readonly definition: Omit<CommonDocDefinition<T>, "initial"> &
585
- RewindableConversationSemantics & {
586
- readonly family: true;
587
- initial(input: I): T;
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 lifecycle record:
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
- token: DocToken<T>,
671
- target: DocTarget,
672
- context: Context,
673
- ): Promise<Readonly<T>>;
674
-
675
- snapshot<T extends JsonObject, I extends JsonValue>(
676
- token: DocFamilyToken<T, I>,
677
- target: FamilyTarget<I>,
678
- context: Context,
679
- ): Promise<Readonly<T>>;
680
-
681
- snapshotAsOf<T extends JsonObject>(
682
- token: RewindableDocToken<T>,
683
- conversationId: Id,
684
- at: Id,
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: DocToken<T>, target: DocTarget): Promise<T>;
726
- doc<T extends JsonObject, I extends JsonValue>(token: DocFamilyToken<T, I>, target: FamilyTarget<I>): Promise<T>;
727
- retireDoc<T extends JsonObject>(token: DocToken<T>, target: DocTarget): void;
728
- retireDoc<T extends JsonObject, I extends JsonValue>(token: DocFamilyToken<T, I>, target: FamilyKey): void;
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
- Every normal access is get-or-create and receives the definition token that
748
- supplies its static type, initializer, migration, and checkpoint policy. Ordinary
749
- document definitions are not registered and ordinary documents are not scanned
750
- at open.
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. Standalone access may commit a required current-only migration before its
754
- method returns. `tx.doc()` instead stages that base in its enclosing transaction.
755
- - Missing instances are created in a serialized commit with an initial base.
756
- - Concurrent acquisition initializes once.
757
- - The target scope must match the token's declared scope.
758
- - Task-scoped access validates against the transaction's latest candidate task
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 rather than repeated in the target.
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 source.
767
- - `tx.doc()` returns the transaction's mutable tracked draft.
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
- Chord mutates the tracked working object immediately while retaining the
788
- previous baseline until flush.
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 drafts
795
- mutate ordinary JSON
796
- callback fails
797
- restore each changed tracker from its unchanged baseline
798
- callback succeeds
799
- flush each changed tracker -> incremental ops + candidate value
800
- Session evaluates each ordinary mutation's checkpoint predicate exactly once
801
- and gives Storage only the selected base or delta representation
802
- Storage.commit persists it while the Session line remains held
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
- materialize each changed immutable published value with applyImmutable(previous, ops)
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
- post-flush or storage failure
808
- poison Session; publish nothing; close/reopen required
931
+ storage fails
932
+ poison Session and publish nothing
809
933
  ```
810
934
 
811
- The immutable published value is separate from the tracker's mutable working
812
- object and is created once per changed document, not once per watch. Initial
813
- publication similarly applies the complete base operation to `undefined`.
814
- Storage's detached retained copy does not satisfy this Session-side ownership
815
- requirement.
816
-
817
- No defensive document copy is required solely for storage failure because the
818
- open Session cannot continue after that failure.
819
-
820
- Unsupported:
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, { scope: "conversation", conversationId });
957
+ escaped = await tx.doc(LiveDoc, conversationId);
826
958
  });
827
- escaped.message = message; // unsupported
959
+ escaped.message = message; // throws: the draft was revoked
828
960
  ```
829
961
 
830
- The same rule applies to nested proxies and mutable objects assigned into a
831
- document.
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 flush. Creation
845
- and version transitions require bases and do not call it. The Session then gives
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 incremental ops
983
+ otherwise -> delta with the prepared Chord operation batch
851
984
  ```
852
985
 
853
- A predicate failure occurs after flush and therefore poisons the open Session as
854
- described in section 1. Storage executes no definition code and never receives
855
- an unused complete candidate with a selected delta.
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
- - A standalone current-only acquisition persists the migrated value as a required
891
- current-version base before its method returns. `tx.doc()` stages the migration
892
- in the enclosing transaction; callback failure persists nothing, and later
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 mutation of a migrated rewindable value stores a required
897
- current-version base before subsequent current-version deltas.
898
- - A fork copies the selected stored value and version. Typed access in the child
899
- migrates it later when necessary.
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
- lifecycle record:
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 input = await tx.input(inputId); // table read
937
- const live = await tx.doc(LiveDoc, { scope: "conversation", conversationId });
1070
+ const task = await tx.task(taskId); // table read
1071
+ const live = await tx.doc(LiveDoc, conversationId);
938
1072
 
939
- const entry = tx.appendEntry(conversationId, message); // first table write
940
- live.message = undefined; // document mutation remains valid
941
- tx.setInput({ ...input, status: "done", answer: entry.id });
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 or
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 inputs.
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: StoredError; readonly result?: R }
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: StoredError };
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 inputs settled by the task.
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 inputs become unanswered, any matching active turn control is cleared,
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/input cleanup with a
1307
+ commit. Faulting a turn task performs the same control/submission cleanup with a
1175
1308
  `faulted` outcome.
1176
1309
 
1177
- ## 6. Inputs and inbox
1310
+ ## 6. Submissions and inbox
1178
1311
 
1179
- Input records back awaitable host handles. The inbox itself is an ordered
1180
- conversation document containing tagged items:
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 input: Message }
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 | input state | other writes |
1330
+ | action | submission state | other writes |
1197
1331
  |---|---|---|
1198
- | idle `send` | `placed`, with user entry | create turn controller/generation |
1199
- | busy `send` | `queued` | append steer/follow-up inbox item |
1200
- | idle passive `write` | `done`, with entry | append entry; no turn |
1201
- | busy passive `write` | `queued` | append write inbox item |
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. Busy send
1210
- with `whenBusy: "reject"` writes nothing and reports `ConversationBusy`. Before
1211
- an idle send places its own entry, it runs a final boundary to drain any older
1212
- eligible queued items. A handle waits until `done` or `unanswered`; abort
1213
- withdraws only a still-queued input and otherwise reports that placement already
1214
- occurred. Conversation abort withdraws queued steer/follow-up inputs but keeps
1215
- passive writes for later placement.
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 inputs become
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
- inputs settle first; selected user IDs start one successor generation. Writes
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
- Chord must encode scattered removals without retransmitting retained payloads.
1238
- The exact tracker optimization is implementation work; IDs are not substituted
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
- send(input: SendInput, context: Context): Promise<InputHandle>;
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. Pre-flush cancellation or callback
1340
- failure does not poison the Session. Post-flush, checkpoint, or storage failure
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. An `InputHandle` returned by its
1373
- `send()` is invocation-bound in the same way; the admitted input itself remains
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 preserves section insertion order.
1441
- `setSection(section, undefined)` removes the key; setting it later appends it at
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: DocToken<T>, target: DocTarget, context: Context): Promise<DocumentWatch<T>>;
1569
- watchDoc<T extends JsonObject, I extends JsonValue>(token: DocFamilyToken<T, I>, target: FamilyTarget<I>, context: Context): Promise<DocumentWatch<T>>;
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. Pico5 adds this adapter at the Chord boundary:
1574
-
1575
- ```ts
1576
- interface ReplicatedDocument<T extends JsonObject> {
1577
- readonly state: ReplicatedState<T | null>;
1578
- dispose(): void;
1579
- }
1580
-
1581
- function documentReplicatedState<T extends JsonObject>(
1582
- source: DocumentSource<T>,
1583
- context: Context,
1584
- ): Promise<ReplicatedDocument<T>>;
1585
- ```
1586
-
1587
- The adapter registers the source with Chord and exposes it without a second
1588
- tracker or re-diff. Normal source and watch acquisition are get-or-create and
1589
- bind one concrete incarnation. Retirement publishes a JSON `null` replacement
1590
- and ends that incarnation's stream. The service may then withdraw itself; if it
1591
- remains exposed, consumers see `null`, never stale state. A later recreation
1592
- requires acquiring a new source/watch.
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 has a token
1603
- and target. There is no additional subtree permission system inside trusted
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. Acquisition performs
1607
- normal get-or-create and, on the Session line, binds one concrete incarnation,
1608
- captures its immutable committed value, and registers the handle for every later
1609
- committed operation batch. `watch.value` is that fixed acquisition snapshot and
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, target, context);
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 publication value corresponding exactly
1660
- to the last batch compacted into it. After storage succeeds, Pico materializes
1661
- that value once per changed document with
1662
- `applyImmutable(previousPublishedValue, ops)`; it never retains mutable
1663
- `Tracker.state`, `Tracker.target`, or a borrowed storage candidate. Watches may
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; an already-admitted
1678
- get-or-create commit still settles and is not undone. Cancellation immediately
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 tracker and performs no semantic
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, input queue/outcome,
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 input; a late or reconnecting
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 submitted `InputHandle` and prints that input's answer.
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 records: readonly [
1806
- Extract<StoredDocumentRecord, { kind: "base" }>,
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: "input"; readonly value: Input }
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 lifecycle consistency, and detached
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
- input(id: Id, context: Context): Promise<Input | undefined>;
1849
- inputByRequest(conversationId: Id, requestId: string, context: Context): Promise<Input | undefined>;
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)` returns the newest applicable base plus its ordered delta tail
1882
- and never scans unrelated documents. An unknown ID returns `undefined`. At
1883
- `"current"`, a retired incarnation returns `undefined`. A numeric lookup of a
1884
- rewindable conversation incarnation returns `undefined` outside its half-open
1885
- lifetime and reconstructs the selected value inside it. A numeric lookup of a
1886
- known current-only incarnation rejects rather than depending on content that
1887
- reclamation may have removed. Metadata membership remains queryable historically.
1888
- A missing required base inside an addressable rewindable lifetime is storage
1889
- corruption, not absence.
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 input records;
1915
- - document lifecycle records;
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, lifecycle, and one marker per commit
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
- - **Draft escape:** `tx.doc()` values, nested proxies, and array methods are valid
1972
- only during that transaction. Retaining them can contaminate a later commit.
1973
- - **Inserted aliases:** after assigning an object or array into a draft, do not
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
- - **Get-or-create reads:** `snapshot()` and `documentSource()` can create and
1981
- commit an absent document. They are not historical or side-effect-free reads.
1982
- - **Family initialization:** `initial` input is used only for a new incarnation.
1983
- It does not update an existing instance.
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;