@earendil-works/pi-durable 0.87.0 → 0.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +72 -1
  2. package/dist/documents.d.ts +66 -0
  3. package/dist/documents.d.ts.map +1 -0
  4. package/dist/documents.js +98 -0
  5. package/dist/documents.js.map +1 -0
  6. package/dist/entries.d.ts +13 -0
  7. package/dist/entries.d.ts.map +1 -0
  8. package/dist/entries.js +18 -0
  9. package/dist/entries.js.map +1 -0
  10. package/dist/env/index.d.ts +131 -0
  11. package/dist/env/index.d.ts.map +1 -0
  12. package/dist/env/index.js +45 -0
  13. package/dist/env/index.js.map +1 -0
  14. package/dist/env/node.d.ts +45 -0
  15. package/dist/env/node.d.ts.map +1 -0
  16. package/dist/env/node.js +908 -0
  17. package/dist/env/node.js.map +1 -0
  18. package/dist/env/utils/adaptive-publisher.d.ts +24 -0
  19. package/dist/env/utils/adaptive-publisher.d.ts.map +1 -0
  20. package/dist/env/utils/adaptive-publisher.js +79 -0
  21. package/dist/env/utils/adaptive-publisher.js.map +1 -0
  22. package/dist/env/utils/output-capture.d.ts +31 -0
  23. package/dist/env/utils/output-capture.d.ts.map +1 -0
  24. package/dist/env/utils/output-capture.js +238 -0
  25. package/dist/env/utils/output-capture.js.map +1 -0
  26. package/dist/env/utils/truncate.d.ts +71 -0
  27. package/dist/env/utils/truncate.d.ts.map +1 -0
  28. package/dist/env/utils/truncate.js +294 -0
  29. package/dist/env/utils/truncate.js.map +1 -0
  30. package/dist/errors.d.ts +15 -0
  31. package/dist/errors.d.ts.map +1 -0
  32. package/dist/errors.js +24 -0
  33. package/dist/errors.js.map +1 -0
  34. package/dist/harness/config.d.ts +20 -0
  35. package/dist/harness/config.d.ts.map +1 -0
  36. package/dist/harness/config.js +18 -0
  37. package/dist/harness/config.js.map +1 -0
  38. package/dist/harness/context.d.ts +27 -0
  39. package/dist/harness/context.d.ts.map +1 -0
  40. package/dist/harness/context.js +109 -0
  41. package/dist/harness/context.js.map +1 -0
  42. package/dist/harness/generation.d.ts +36 -0
  43. package/dist/harness/generation.d.ts.map +1 -0
  44. package/dist/harness/generation.js +266 -0
  45. package/dist/harness/generation.js.map +1 -0
  46. package/dist/harness/harness.d.ts +10 -0
  47. package/dist/harness/harness.d.ts.map +1 -0
  48. package/dist/harness/harness.js +266 -0
  49. package/dist/harness/harness.js.map +1 -0
  50. package/dist/harness/live.d.ts +40 -0
  51. package/dist/harness/live.d.ts.map +1 -0
  52. package/dist/harness/live.js +41 -0
  53. package/dist/harness/live.js.map +1 -0
  54. package/dist/harness/prompt.d.ts +24 -0
  55. package/dist/harness/prompt.d.ts.map +1 -0
  56. package/dist/harness/prompt.js +88 -0
  57. package/dist/harness/prompt.js.map +1 -0
  58. package/dist/harness/registry.d.ts +7 -0
  59. package/dist/harness/registry.d.ts.map +1 -0
  60. package/dist/harness/registry.js +309 -0
  61. package/dist/harness/registry.js.map +1 -0
  62. package/dist/harness/scheduler.d.ts +63 -0
  63. package/dist/harness/scheduler.d.ts.map +1 -0
  64. package/dist/harness/scheduler.js +605 -0
  65. package/dist/harness/scheduler.js.map +1 -0
  66. package/dist/harness/submissions.d.ts +24 -0
  67. package/dist/harness/submissions.d.ts.map +1 -0
  68. package/dist/harness/submissions.js +133 -0
  69. package/dist/harness/submissions.js.map +1 -0
  70. package/dist/harness/types.d.ts +358 -0
  71. package/dist/harness/types.d.ts.map +1 -0
  72. package/dist/harness/types.js +2 -0
  73. package/dist/harness/types.js.map +1 -0
  74. package/dist/harness/util.d.ts +14 -0
  75. package/dist/harness/util.d.ts.map +1 -0
  76. package/dist/harness/util.js +55 -0
  77. package/dist/harness/util.js.map +1 -0
  78. package/dist/ids.d.ts +6 -0
  79. package/dist/ids.d.ts.map +1 -0
  80. package/dist/ids.js +9 -0
  81. package/dist/ids.js.map +1 -0
  82. package/dist/index.d.ts +13 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +11 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/forks.d.ts +10 -0
  87. package/dist/session/forks.d.ts.map +1 -0
  88. package/dist/session/forks.js +42 -0
  89. package/dist/session/forks.js.map +1 -0
  90. package/dist/session/observation.d.ts +28 -0
  91. package/dist/session/observation.d.ts.map +1 -0
  92. package/dist/session/observation.js +280 -0
  93. package/dist/session/observation.js.map +1 -0
  94. package/dist/session/session.d.ts +59 -0
  95. package/dist/session/session.d.ts.map +1 -0
  96. package/dist/session/session.js +380 -0
  97. package/dist/session/session.js.map +1 -0
  98. package/dist/session/transaction.d.ts +105 -0
  99. package/dist/session/transaction.d.ts.map +1 -0
  100. package/dist/session/transaction.js +718 -0
  101. package/dist/session/transaction.js.map +1 -0
  102. package/dist/storage/jsonl/index.d.ts +2 -0
  103. package/dist/storage/jsonl/index.d.ts.map +1 -0
  104. package/dist/storage/jsonl/index.js +2 -0
  105. package/dist/storage/jsonl/index.js.map +1 -0
  106. package/dist/storage/jsonl/node.d.ts +6 -0
  107. package/dist/storage/jsonl/node.d.ts.map +1 -0
  108. package/dist/storage/jsonl/node.js +8 -0
  109. package/dist/storage/jsonl/node.js.map +1 -0
  110. package/dist/storage/jsonl/storage.d.ts +115 -0
  111. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  112. package/dist/storage/jsonl/storage.js +681 -0
  113. package/dist/storage/jsonl/storage.js.map +1 -0
  114. package/dist/storage/memory.d.ts +60 -0
  115. package/dist/storage/memory.d.ts.map +1 -0
  116. package/dist/storage/memory.js +681 -0
  117. package/dist/storage/memory.js.map +1 -0
  118. package/dist/storage/sqlite/database.d.ts +30 -0
  119. package/dist/storage/sqlite/database.d.ts.map +1 -0
  120. package/dist/storage/sqlite/database.js +2 -0
  121. package/dist/storage/sqlite/database.js.map +1 -0
  122. package/dist/storage/sqlite/index.d.ts +4 -0
  123. package/dist/storage/sqlite/index.d.ts.map +1 -0
  124. package/dist/storage/sqlite/index.js +3 -0
  125. package/dist/storage/sqlite/index.js.map +1 -0
  126. package/dist/storage/sqlite/migrations.d.ts +11 -0
  127. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  128. package/dist/storage/sqlite/migrations.js +111 -0
  129. package/dist/storage/sqlite/migrations.js.map +1 -0
  130. package/dist/storage/sqlite/node.d.ts +25 -0
  131. package/dist/storage/sqlite/node.d.ts.map +1 -0
  132. package/dist/storage/sqlite/node.js +97 -0
  133. package/dist/storage/sqlite/node.js.map +1 -0
  134. package/dist/storage/sqlite/storage.d.ts +51 -0
  135. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  136. package/dist/storage/sqlite/storage.js +628 -0
  137. package/dist/storage/sqlite/storage.js.map +1 -0
  138. package/dist/tasks.d.ts +6 -0
  139. package/dist/tasks.d.ts.map +1 -0
  140. package/dist/tasks.js +5 -0
  141. package/dist/tasks.js.map +1 -0
  142. package/dist/testing/assertions.d.ts +5 -0
  143. package/dist/testing/assertions.d.ts.map +1 -0
  144. package/dist/testing/assertions.js +25 -0
  145. package/dist/testing/assertions.js.map +1 -0
  146. package/dist/testing/index.d.ts +6 -0
  147. package/dist/testing/index.d.ts.map +1 -0
  148. package/dist/testing/index.js +5 -0
  149. package/dist/testing/index.js.map +1 -0
  150. package/dist/testing/runner.d.ts +10 -0
  151. package/dist/testing/runner.d.ts.map +1 -0
  152. package/dist/testing/runner.js +14 -0
  153. package/dist/testing/runner.js.map +1 -0
  154. package/dist/testing/storage-benchmark.d.ts +41 -0
  155. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  156. package/dist/testing/storage-benchmark.js +351 -0
  157. package/dist/testing/storage-benchmark.js.map +1 -0
  158. package/dist/testing/storage-conformance.d.ts +4 -0
  159. package/dist/testing/storage-conformance.d.ts.map +1 -0
  160. package/dist/testing/storage-conformance.js +1122 -0
  161. package/dist/testing/storage-conformance.js.map +1 -0
  162. package/dist/testing/types.d.ts +19 -0
  163. package/dist/testing/types.d.ts.map +1 -0
  164. package/dist/testing/types.js +2 -0
  165. package/dist/testing/types.js.map +1 -0
  166. package/dist/types.d.ts +525 -78
  167. package/dist/types.d.ts.map +1 -1
  168. package/dist/types.js.map +1 -1
  169. package/docs/chord-delta-findings.md +1 -1
  170. package/docs/pico-v5-chord-usage.md +90 -102
  171. package/docs/pico-v5-handoff.md +493 -253
  172. package/docs/pico-v5.md +1901 -848
  173. package/package.json +47 -5
  174. package/dist/memory-storage.d.ts +0 -37
  175. package/dist/memory-storage.d.ts.map +0 -1
  176. package/dist/memory-storage.js +0 -322
  177. package/dist/memory-storage.js.map +0 -1
package/docs/pico-v5.md CHANGED
@@ -4,9 +4,19 @@ 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";
8
- import { applyImmutable, type Op } from "@earendil-works/chord/delta";
9
7
  import type {
8
+ AttachedReplicatedState,
9
+ Context,
10
+ Draft,
11
+ JsonRepresentation,
12
+ JsonValue,
13
+ ReplicatedState,
14
+ } from "@earendil-works/chord";
15
+ import type { Op } from "@earendil-works/chord/delta";
16
+ import type {
17
+ AssistantMessage,
18
+ CacheRetention,
19
+ DeferredHandle,
10
20
  Message,
11
21
  Models,
12
22
  ModelThinkingLevel,
@@ -14,11 +24,12 @@ import type {
14
24
  Tool,
15
25
  ToolReference,
16
26
  ToolResultMessage,
27
+ Transport,
17
28
  UserMessage,
18
29
  } from "@earendil-works/pi-ai";
19
30
 
20
31
  type JsonObject = { [key: string]: JsonValue };
21
- type StoredError = { message: string; detail?: JsonValue };
32
+ type TaskOutcomeError = { message: string; detail?: JsonValue };
22
33
  ```
23
34
 
24
35
  Pico5 targets the transcript `SystemMessage` contract from pi-ai PR
@@ -32,8 +43,8 @@ The core rule is:
32
43
 
33
44
  ## 1. Terms and invariants
34
45
 
35
- - A **Session** owns one mutation line, conversations, entries, tasks, inputs,
36
- and documents.
46
+ - A **Session** owns one mutation line, conversations, entries, tasks,
47
+ submissions, and documents.
37
48
  - A **conversation** is a transcript scope. It may fork another conversation.
38
49
  - An **entry** is an immutable transcript record.
39
50
  - A **task** is a durable state machine attached to one conversation.
@@ -42,6 +53,10 @@ The core rule is:
42
53
  - A **definition** is a typed token describing one document or document family.
43
54
  - A **source** exposes committed document changes to Chord without exposing a
44
55
  mutable object.
56
+ - A **turn** is one assistant response and the tool calls it makes. A **run**
57
+ is the sequence of turns from an admitted input to the final answer; turn
58
+ boundaries (section 6) sit between its turns. A conversation is busy while a
59
+ run is active.
45
60
 
46
61
  Required invariants:
47
62
 
@@ -50,13 +65,15 @@ Required invariants:
50
65
  3. All visible progress is durable. There is no volatile publication path.
51
66
  4. External effects do not run inside the Session mutation transaction.
52
67
  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.
68
+ 6. Document drafts are fully revoked when their transaction callback settles:
69
+ the Session synchronously prepares or aborts every open change at that point.
70
+ Values assigned into a draft are copied by value and must be strict JSON.
56
71
  7. The mutation line remains held through storage settlement and committed-state
57
- 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.
72
+ adoption. Commit/close observers run synchronously only to capture immutable
73
+ state; document-state and watch user callbacks run later, off the line.
74
+ 8. An uncertain storage failure is fatal to the open Session. It publishes
75
+ nothing and must be reopened. Preparation and checkpoint failures occur before
76
+ storage admission and roll back normally.
60
77
 
61
78
  ## 2. Core records
62
79
 
@@ -64,19 +81,37 @@ The concrete JSON representations may add bookkeeping fields, but must preserve
64
81
  these contracts.
65
82
 
66
83
  ```ts
67
- type Id = number;
68
- type Seq = number;
69
- const ROOT_CONVERSATION_ID: Id = 1;
84
+ declare const idBrand: unique symbol;
85
+ type Id<Kind extends string, Type = unknown> = number & {
86
+ readonly [idBrand]: {
87
+ readonly kind: Kind;
88
+ readonly type: Type;
89
+ };
90
+ };
91
+ type ConversationId = Id<"conversation">;
92
+ type EntryId = Id<"entry">;
93
+ type TaskId<Result = unknown> = Id<"task", Result>;
94
+ type SubmissionId = Id<"submission">;
95
+ type DocumentId = Id<"document">;
96
+
97
+ declare const seqBrand: unique symbol;
98
+ /** Strictly increases between commits; gaps are permitted. */
99
+ type Seq = number & { readonly [seqBrand]: "sequence" };
100
+ const ROOT_CONVERSATION_ID = 1 as ConversationId;
101
+
102
+ type ConversationOwnership =
103
+ | { readonly kind: "ownerless" }
104
+ | { readonly kind: "task"; readonly taskId: TaskId };
70
105
 
71
106
  type ConversationRecord = {
72
- readonly id: Id;
107
+ readonly id: ConversationId;
73
108
  readonly parent?: {
74
- readonly conversationId: Id;
75
- readonly at: Id;
109
+ readonly conversationId: ConversationId;
110
+ readonly at: EntryId;
76
111
  };
77
112
  readonly owner?: {
78
- readonly conversationId: Id;
79
- readonly taskId: Id;
113
+ readonly conversationId: ConversationId;
114
+ readonly taskId: TaskId;
80
115
  };
81
116
  };
82
117
  ```
@@ -94,68 +129,126 @@ interface SystemMessage {
94
129
  }
95
130
  ```
96
131
 
97
- `content` is the base prompt on the leading message and additional instruction
98
- text on later messages. `sections` is an ordered named patch: a string adds or
99
- replaces a section, while `null` removes it. `toolsRemoved` is applied before
100
- `toolsAdded` within one message. Replaying every system message in transcript
101
- order yields the effective prompt and tool set.
132
+ In pi-ai, `content` is the base prompt on the leading message and additional
133
+ instruction text on later messages. `sections` is an ordered named patch: a
134
+ string adds or replaces a section, while `null` removes it. `toolsRemoved` is
135
+ applied before `toolsAdded` within one message. Replaying every system message
136
+ in transcript order yields the effective prompt and tool set.
137
+
138
+ Pico always writes `content: ""`. Every prompt part, including the preamble, is
139
+ a named section produced by the registry's system prompt slot (section 7.4).
102
140
 
103
141
  ```ts
104
142
  type ContextEdit = {
105
- readonly target: Id;
143
+ readonly target: EntryId;
106
144
  } & (
107
145
  | { readonly action: "omit"; readonly messages?: never }
108
146
  | { readonly action: "replace"; readonly messages: readonly Message[] }
109
147
  );
110
148
 
111
149
  type EntryRecord = {
112
- readonly id: Id;
113
- readonly conversationId: Id;
150
+ readonly id: EntryId;
151
+ readonly conversationId: ConversationId;
114
152
  readonly kind: string;
115
153
  readonly model?: readonly Message[];
116
154
  readonly data?: JsonValue;
117
- readonly head?: Id;
155
+ readonly head?: EntryId;
118
156
  readonly edits?: readonly ContextEdit[];
119
- readonly byTaskId?: Id;
157
+ readonly byTaskId?: TaskId;
120
158
  };
121
159
 
122
160
  type EntryDraft = Omit<EntryRecord, "id" | "conversationId" | "byTaskId" | "head"> & {
123
- readonly head?: Id | "self";
161
+ readonly head?: EntryId | "self";
124
162
  };
125
163
 
126
- type InputBase = {
127
- readonly id: Id;
128
- readonly conversationId: Id;
164
+ type SubmissionRecordBase = {
165
+ readonly id: SubmissionId;
166
+ readonly conversationId: ConversationId;
129
167
  readonly requestId?: string;
130
168
  };
131
169
 
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
- );
170
+ type SubmissionRecord =
171
+ | (SubmissionRecordBase & {
172
+ readonly type: "input";
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: "placed";
183
+ readonly entry: EntryId;
184
+ readonly answer?: never;
185
+ readonly reason?: never;
186
+ readonly detail?: never;
187
+ }
188
+ | {
189
+ readonly status: "done";
190
+ readonly entry: EntryId;
191
+ readonly answer: EntryId;
192
+ readonly reason?: never;
193
+ readonly detail?: never;
194
+ }
195
+ | {
196
+ readonly status: "unanswered";
197
+ readonly entry?: EntryId;
198
+ readonly answer?: never;
199
+ readonly reason: string;
200
+ readonly detail?: JsonValue;
201
+ }
202
+ ))
203
+ | (SubmissionRecordBase & {
204
+ readonly type: "write";
205
+ } & (
206
+ | {
207
+ readonly status: "queued";
208
+ readonly entry?: never;
209
+ readonly answer?: never;
210
+ readonly reason?: never;
211
+ readonly detail?: never;
212
+ }
213
+ | {
214
+ readonly status: "done";
215
+ readonly entry: EntryId;
216
+ readonly answer?: never;
217
+ readonly reason?: never;
218
+ readonly detail?: never;
219
+ }
220
+ | {
221
+ readonly status: "unanswered";
222
+ readonly entry?: never;
223
+ readonly answer?: never;
224
+ readonly reason: string;
225
+ readonly detail?: JsonValue;
226
+ }
227
+ ));
228
+
229
+ type SubmissionSettlement =
230
+ | { readonly status: "done"; readonly answer: EntryId }
231
+ | { readonly status: "unanswered"; readonly reason: string; readonly detail?: JsonValue };
147
232
 
148
- type InputCreate = Input extends infer Record
149
- ? Record extends Input
233
+ type SubmissionCreate = SubmissionRecord extends infer Record
234
+ ? Record extends SubmissionRecord
150
235
  ? Omit<Record, "id">
151
236
  : never
152
237
  : never;
153
238
  ```
154
239
 
240
+ ID brands are compile-time only. IDs and sequences remain ordinary numbers in
241
+ memory, JSON, JSONL, and SQLite. Code applies a brand only at a trusted creation
242
+ or decoding boundary. The distinct sequence brand prevents an entity ID from
243
+ being used as a document commit point.
244
+
155
245
  Conversation history parenting and task ownership are separate:
156
246
 
157
247
  - `parent` controls inherited entries and historical documents.
158
- - `owner` controls task authorization, subtree abort, and subtree idle waits.
248
+ - `owner` records task attribution and connects scopes for subtree abort and idle traversal. It is not an access-control capability.
249
+ - Creation always supplies `ConversationOwnership` explicitly. The Session
250
+ derives the persisted owner's conversation from the named task; callers never
251
+ construct the persisted owner pair.
159
252
 
160
253
  A conversation's owner remains recorded after the owning task becomes terminal.
161
254
 
@@ -179,9 +272,11 @@ Context derivation:
179
272
  5. If `H` exists, context entries are `H` followed by non-head entries in the
180
273
  range. Otherwise they are the range.
181
274
  6. Keep every positional system message and its tool/section changes.
182
- 7. Order tool results by assistant tool-call order.
183
- 8. Synthesize missing tool results after a fork when required by the provider
184
- message protocol.
275
+ 7. Move each assistant's tool results, found before the next assistant message,
276
+ directly after it in tool-call order, ahead of any intervening user or system
277
+ message. The provider protocol requires results to follow their call.
278
+ 8. Synthesize an error result for every call without one, for example after a
279
+ fork cut or at an interrupted tail. Drop tool results with no preceding call.
185
280
  9. Exclude model-less entries and assistant messages with `aborted`, `error`, or
186
281
  `deferred` stop reasons from future provider requests.
187
282
 
@@ -203,21 +298,31 @@ type ModelRef = {
203
298
 
204
299
  type UserInput = UserMessage["content"];
205
300
 
206
- type SendInput = {
207
- readonly content: UserInput;
301
+ type SubmissionDraft = {
208
302
  readonly requestId?: string;
209
- readonly whenBusy?: "steer" | "followUp" | "reject";
210
- };
303
+ } & (
304
+ | {
305
+ readonly type: "input";
306
+ readonly content: UserInput;
307
+ readonly whenBusy?: "steer" | "followUp" | "reject";
308
+ readonly entry?: never;
309
+ }
310
+ | {
311
+ readonly type: "write";
312
+ readonly entry: EntryDraft;
313
+ readonly content?: never;
314
+ readonly whenBusy?: never;
315
+ }
316
+ );
211
317
 
212
- type SectionSeed =
213
- | { readonly key: string; readonly value: JsonValue }
214
- | { readonly key: string; readonly remove: true };
318
+ type InputSubmissionDraft = Extract<SubmissionDraft, { readonly type: "input" }>;
215
319
 
216
- type ConversationSpec = {
217
- readonly parent?: { readonly conversationId: Id; readonly at: Id };
218
- readonly model?: ModelRef;
219
- readonly sections?: readonly SectionSeed[];
220
- readonly activeTools?: readonly string[];
320
+ /** Runs inside the creating commit, after the conversation and its configuration exist. */
321
+ type ConversationInit = (tx: Tx, conversationId: ConversationId) => void | Promise<void>;
322
+
323
+ type ConversationCreateOptions = {
324
+ readonly ownership: ConversationOwnership;
325
+ readonly init?: ConversationInit;
221
326
  };
222
327
 
223
328
  type AnyTask = {
@@ -232,22 +337,58 @@ type AnyTask = {
232
337
  };
233
338
  };
234
339
 
235
- type HarnessOptions = {
340
+ type HarnessOptions<Tool extends ToolRegistration = ToolRegistration> = {
236
341
  readonly models: Models; // the pi-ai Models interface
237
- readonly tools?: readonly ToolRegistration[];
238
- readonly taskKinds?: readonly AnyTask[];
239
- readonly sections?: readonly SystemSection<JsonValue>[];
342
+ readonly registry: RegistryReader<Tool>; // section 7.1
240
343
  readonly now?: () => number;
241
- readonly root?: Omit<ConversationSpec, "parent">;
242
344
  readonly onReport?: (error: unknown) => void;
243
345
  };
244
346
 
245
- interface Entry<E extends EntryRecord = EntryRecord> {
347
+ /** Curated pi-ai request options; absent fields use pi-ai defaults. */
348
+ type ConversationStreamOptions = {
349
+ transport?: Transport;
350
+ timeoutMs?: number;
351
+ /** Provider/SDK retries inside one request attempt. */
352
+ maxRetries?: number;
353
+ maxRetryDelayMs?: number;
354
+ headers?: Record<string, string>;
355
+ metadata?: JsonObject;
356
+ cacheRetention?: CacheRetention;
357
+ deferred?: boolean | { window?: "15m" | "1h" | "24h" };
358
+ };
359
+
360
+ /** Durable generation attempt retries; the JSON shape of pi-ai `RetryPolicy`. */
361
+ type ConversationRetryPolicy = {
362
+ enabled: boolean;
363
+ maxRetries: number;
364
+ baseDelayMs: number;
365
+ maxAgentDelayMs?: number;
366
+ };
367
+
368
+ type ConversationConfigState = {
369
+ model?: ModelRef;
370
+ thinkingLevel: ModelThinkingLevel;
371
+ activeTools: string[];
372
+ streamOptions?: ConversationStreamOptions;
373
+ retry?: ConversationRetryPolicy;
374
+ };
375
+
376
+ /** Built-in rewindable configuration document; see below. */
377
+ declare const ConversationConfig: RewindableConversationDocToken<ConversationConfigState>;
378
+
379
+ /** Entry whose `data` has type `D`; `never` means the kind carries no data. */
380
+ type TypedEntry<D extends JsonValue> = Omit<EntryRecord, "data"> &
381
+ ([D] extends [never] ? { readonly data?: never } : { readonly data: D });
382
+
383
+ type TypedEntryDraft<D extends JsonValue> = Omit<EntryDraft, "kind" | "data"> &
384
+ ([D] extends [never] ? { readonly data?: never } : { readonly data: D });
385
+
386
+ interface Entry<D extends JsonValue = never> {
246
387
  readonly kind: string;
247
- is(entry: EntryRecord | undefined): entry is E;
388
+ is(entry: EntryRecord | undefined): entry is TypedEntry<D>;
248
389
  }
249
390
 
250
- function defineEntry<E extends EntryRecord>(kind: string): Entry<E>;
391
+ function defineEntry<D extends JsonValue = never>(kind: string): Entry<D>;
251
392
 
252
393
  type ContextView = {
253
394
  readonly head: EntryRecord | undefined;
@@ -255,23 +396,42 @@ type ContextView = {
255
396
  readonly messages: readonly Message[];
256
397
  };
257
398
 
258
- type SettledInput = Input & {
399
+ type SettledSubmissionRecord = SubmissionRecord & {
259
400
  readonly status: "done" | "unanswered";
260
401
  };
261
402
 
262
- interface InputHandle {
263
- 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">;
403
+ interface Submission {
404
+ readonly id: SubmissionId;
405
+ status(context: Context): Promise<SubmissionRecord>;
406
+ wait(context: Context): Promise<SettledSubmissionRecord>;
407
+ abort(context: Context): Promise<"aborted" | "already_placed" | "settled">;
269
408
  }
270
409
 
271
410
  type SettledTask<R> = TaskRecord<JsonValue, JsonValue, R> & {
272
411
  readonly state: Extract<TaskState<JsonValue, R>, { status: "terminal" }>;
273
412
  };
274
413
 
414
+ type TaskInspection = {
415
+ readonly record: TaskRecord<JsonValue, JsonValue, JsonValue>;
416
+ readonly state:
417
+ | { readonly kind: "running" }
418
+ | { readonly kind: "ready"; readonly migrates: boolean }
419
+ | { readonly kind: "waiting"; readonly on: readonly TaskId[] }
420
+ | {
421
+ readonly kind: "blocked";
422
+ readonly reason: "missing_task" | "task_too_old" | "migration_failed";
423
+ readonly error?: unknown;
424
+ };
425
+ };
426
+
427
+ type HarnessInspection = {
428
+ readonly scheduling: "paused" | "running" | "closing";
429
+ readonly tasks: readonly TaskInspection[];
430
+ /** Queued and placed submissions in ID order. */
431
+ readonly submissions: readonly SubmissionRecord[];
432
+ readonly registry: readonly RegistryFailure[];
433
+ };
434
+
275
435
  type ConversationWatch = WatchHandle<ConversationView>;
276
436
 
277
437
  type HooksOf<K> = K extends Task<infer _I, infer _S, infer _R, infer H>
@@ -279,9 +439,8 @@ type HooksOf<K> = K extends Task<infer _I, infer _S, infer _R, infer H>
279
439
  : never;
280
440
 
281
441
  interface Conversation {
282
- readonly id: Id;
283
- send(input: SendInput, context: Context): Promise<InputHandle>;
284
- write(entry: EntryDraft, context: Context): Promise<InputHandle>;
442
+ readonly id: ConversationId;
443
+ submit(submission: SubmissionDraft, context: Context): Promise<Submission>;
285
444
 
286
445
  getModel(context: Context): Promise<ModelRef | undefined>;
287
446
  setModel(model: ModelRef | undefined, context: Context): Promise<void>;
@@ -289,15 +448,10 @@ interface Conversation {
289
448
  setThinkingLevel(level: ModelThinkingLevel, context: Context): Promise<void>;
290
449
  getActiveTools(context: Context): Promise<readonly string[]>;
291
450
  setActiveTools(names: readonly string[], context: Context): Promise<void>;
292
- getSection<T extends JsonValue>(
293
- section: SystemSection<T>,
294
- context: Context,
295
- ): Promise<T | undefined>;
296
- setSection<T extends JsonValue>(
297
- section: SystemSection<T>,
298
- value: T | undefined,
299
- context: Context,
300
- ): Promise<void>;
451
+ getStreamOptions(context: Context): Promise<ConversationStreamOptions>;
452
+ setStreamOptions(options: ConversationStreamOptions, context: Context): Promise<void>;
453
+ getRetryPolicy(context: Context): Promise<ConversationRetryPolicy>;
454
+ setRetryPolicy(policy: ConversationRetryPolicy | undefined, context: Context): Promise<void>;
301
455
 
302
456
  commit<T>(
303
457
  change: (tx: Tx) => T | Promise<T>,
@@ -306,67 +460,50 @@ interface Conversation {
306
460
  context(context: Context): Promise<ContextView>;
307
461
  entries(
308
462
  query: Omit<EntryQuery, "conversationId">,
309
- cursor: Cursor | undefined,
310
463
  limit: number,
464
+ cursor: Cursor | undefined,
311
465
  context: Context,
312
466
  ): Promise<Page<EntryRecord, Cursor>>;
313
467
  fork(
314
- at: Id,
315
- spec: Omit<ConversationSpec, "parent">,
468
+ at: EntryId,
469
+ options: ConversationCreateOptions,
316
470
  context: Context,
317
471
  ): Promise<Conversation>;
318
- collapse(instructions: string | undefined, context: Context): Promise<Id>;
472
+ collapse(instructions: string | undefined, context: Context): Promise<TaskId>;
319
473
  reset(handoff: string | undefined, context: Context): Promise<void>;
320
474
  abort(context: Context): Promise<void>;
321
475
  waitForIdle(context: Context): Promise<void>;
322
- hooks<K extends AnyTask>(
323
- owner: string,
324
- task: K,
325
- handlers: Partial<HooksOf<K>>,
326
- options?: { readonly subtree?: boolean },
327
- ): () => void;
476
+ viewState(context: Context): Promise<AttachedReplicatedState<ConversationView>>;
328
477
  watch(context: Context): Promise<ConversationWatch>;
329
478
  }
330
479
 
331
480
  interface Harness extends Session {
332
481
  resume(): void;
333
- suspend(context: Context): Promise<void>;
334
- quiescent(): boolean;
335
- hold(): () => void;
336
-
337
- registerTaskKind(task: AnyTask): () => void;
338
- registerTool(tool: ToolRegistration): () => void;
339
- registerSection(section: SystemSection<JsonValue>): () => void;
340
- hooks<K extends AnyTask>(
341
- owner: string,
342
- task: K,
343
- handlers: Partial<HooksOf<K>>,
344
- ): () => void;
345
-
346
- root(context: Context): Promise<Conversation>;
347
- onConversation(listener: (conversation: Conversation) => void): () => void;
348
- conversation(id: Id, context: Context): Promise<Conversation | undefined>;
349
- createConversation(
350
- spec: ConversationSpec & { readonly input?: UserInput },
482
+
483
+ root(
351
484
  context: Context,
485
+ options?: { readonly init?: ConversationInit },
352
486
  ): Promise<Conversation>;
353
-
354
- getTask(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
355
- abortInput(
356
- id: Id,
487
+ conversation(id: ConversationId, context: Context): Promise<Conversation | undefined>;
488
+ createConversation(options: ConversationCreateOptions, context: Context): Promise<Conversation>;
489
+
490
+ getTask<R>(id: TaskId<R>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, R> | undefined>;
491
+ inspect(context: Context): Promise<HarnessInspection>;
492
+ submission(id: SubmissionId, context: Context): Promise<Submission | undefined>;
493
+ abortSubmission(
494
+ id: SubmissionId,
357
495
  context: Context,
358
- conversationId?: Id,
359
- ): Promise<"aborted" | "already_placed" | "not_found">;
360
- abortTask(id: Id, context: Context): Promise<"marked" | "terminal">;
361
- markTask(id: Id, context: Context): Promise<"marked" | "terminal">;
362
- waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
496
+ conversationId?: ConversationId,
497
+ ): Promise<"aborted" | "already_placed" | "settled" | "not_found">;
498
+ abortTask(id: TaskId, context: Context): Promise<"marked" | "terminal">;
499
+ waitForTask<R>(id: TaskId<R>, context: Context): Promise<SettledTask<R>>;
363
500
  waitForIdle(context: Context): Promise<void>;
364
501
  }
365
502
 
366
503
  declare const Harness: {
367
- open(
504
+ open<Tool extends ToolRegistration>(
368
505
  storage: Storage,
369
- options: HarnessOptions,
506
+ options: HarnessOptions<Tool>,
370
507
  context: Context,
371
508
  ): Promise<Harness>;
372
509
  };
@@ -375,79 +512,126 @@ declare const Harness: {
375
512
  This intentionally retains the useful Pico3 host shape. It removes Pico3's
376
513
  fixed `rewindable()`/`sticky()` accessors, namespace router, semantic view events,
377
514
  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.
381
-
382
- `Harness.open()` installs built-in task, tool, and section definitions followed
383
- by supplied task kinds, tools, and sections. It changes
384
- surviving `running` tasks to `pending`, migrates task records, and settles unknown
385
- or unmigratable live task kinds as `orphaned` before resolving. Any built-in
386
- document touched by that recovery migrates through its ordinary typed access
387
- path. Open does not scan or migrate other documents. IDs and names in registries must be unique. No
388
- handler dispatches during open. Dynamic registration is available after open and
389
- does not resurrect a task already settled by that pass.
390
-
391
- The root conversation always has reserved ID `ROOT_CONVERSATION_ID` (`1`). Empty
392
- storage creates that conversation from `options.root`; reopen looks it up by the
393
- reserved ID. `options.root` never overwrites existing state. `root()` returns
394
- that handle. A conversation with no configured model produces a durable
395
- `no_model` generation failure.
515
+ conversation watch replace those surfaces. `submit()` durably admits either a
516
+ user input or passive entry write and returns one `Submission` that tracks its
517
+ settlement.
518
+
519
+ `Harness.open()` binds the Session to one application-owned registry (section
520
+ 7.1). `createRegistry()` pre-registers the built-in task definitions (section 8);
521
+ they cannot be disposed or replaced, and open rejects a registry whose snapshot
522
+ lacks any of them or the built-in `pi` conversation setup.
523
+ Open changes surviving `running` tasks to `pending` and does nothing else to
524
+ task records: it never migrates or terminalizes a task because its definition
525
+ is missing or unmigratable. Such a task stays `pending` and is **blocked**: the
526
+ scheduler skips it and reconsiders it whenever the registry changes (section
527
+ 5.4). Any built-in document touched by recovery migrates through its ordinary
528
+ typed access path. Open does not scan or migrate other documents. No handler
529
+ dispatches during open. Applications should register tasks before open so
530
+ recovered work can resume immediately; registration after open also unblocks it.
531
+
532
+ The root conversation always has reserved ID `ROOT_CONVERSATION_ID` (`1`).
533
+ `root(context, { init })` creates it lazily: when the root is absent, one commit
534
+ on the Session line creates the ownerless root, its default configuration, and
535
+ runs `init(tx, rootId)`. When the root already exists, `init` is ignored and no
536
+ write occurs. Reopen finds the same root by its reserved ID. A conversation with
537
+ no configured model produces a durable `no_model` generation failure.
396
538
 
397
539
  `resume()` is idempotent while running and only enables scheduling. It does not
398
- repeat open-time reconciliation. `suspend()` is terminal for that Harness
399
- instance and follows the close semantics below; `resume()` after suspend/close
400
- rejects. `quiescent()` means no task invocation is currently executing; eligible
401
- or delayed durable tasks may still exist. `hold()` is available only while
402
- quiescent and pauses reservation until its idempotent release function runs.
403
-
404
- Runtime registration rejects duplicate IDs/names and returns an idempotent
405
- function that unregisters only that exact token. It supports declaration
406
- movement during normal operation, not replacement of executing extension code;
407
- section 7.4 governs code reload.
408
-
409
- Conversation creation atomically commits the conversation, built-in
410
- configuration and section/tool seeds, and optional input admission. For a fork,
411
- omitted model, section, and active-tool values follow their document definition's
412
- fork policy; provided values override those forked built-in values in the same
413
- commit. Other documents follow their own definitions without special handling.
414
-
415
- The built-in conversation configuration document contains the selected model,
416
- thinking level, section values, and active tool names. Its initial thinking level
417
- is `"off"`. It is rewindable with `fork: "asOf"`, so a child starts from the
418
- configuration visible at its selected entry unless explicit creation seeds
419
- override it. Conversation creation eagerly
420
- creates it, so `getModel()`, `getThinkingLevel()`, `getActiveTools()`, and
421
- `getSection()` return detached committed snapshots without a get-or-create
422
- write. Each setter performs one ordinary Session commit against that document;
423
- `setSection(section, undefined)` removes the value. A setter does not start
540
+ repeat open-time reconciliation, and it throws after close. Work that must happen
541
+ before any task runs, such as registration or seeding, happens before
542
+ `resume()`. Calls that ask for progress also enable scheduling, so they never
543
+ wait on a paused Harness: `Conversation.submit()`, `Submission.wait()`,
544
+ `Harness.waitForTask()`, `Harness.waitForIdle()`, and
545
+ `Conversation.waitForIdle()`. Recovered work starts with them. A viewer that only
546
+ reads never enables scheduling.
547
+
548
+ `createConversation({ ownership, init })` and
549
+ `fork(at, { ownership, init })` commit atomically: the conversation, its
550
+ explicitly selected ownership, its configuration, and every write made by `init`.
551
+ A first input is an ordinary `submit()` afterward. A request ID makes a retried
552
+ `submit()` to the same conversation exactly-once; a host that must survive a
553
+ crash between the two calls first finds its conversation again, for example
554
+ through a key written by `init`. Host callers must choose ownerless or task
555
+ ownership; neither the Harness nor a conversation handle infers ownership from
556
+ call context. A new independent conversation receives the default configuration
557
+ before `init` runs. A fork receives the configuration visible at `at` through
558
+ the ordinary `asOf` fork policy; `init` may then override it with
559
+ `tx.doc(ConversationConfig, id)`. Other documents follow their own definitions
560
+ without special handling.
561
+
562
+ Every Harness commit that creates or forks a conversation runs the registry's
563
+ conversation setups (section 7.1) in the same commit, whether through the
564
+ conveniences or through raw `tx.createConversation()` and `tx.forkConversation()`,
565
+ for example inside a tool commit. The built-in `pi` setup runs first: an
566
+ independent conversation gets the default configuration and a fork keeps its
567
+ `asOf` copy, and both get an empty `pi.live`. Applications register setups for
568
+ their own documents the same way. The conveniences add only `init` and its checks,
569
+ which run after every setup. A conversation created by a plain Session has no
570
+ built-in documents; its configuration reads as
571
+ `ConversationConfig.definition.initial()` until something writes it.
572
+
573
+ The built-in conversation configuration document is final at version 1:
574
+
575
+ | field | value |
576
+ |---|---|
577
+ | kind | `pi.conversation.config` |
578
+ | version | `1` (no migration) |
579
+ | scope/history/fork | conversation, `rewindable`, `asOf` |
580
+ | schema | `ConversationConfigState` |
581
+ | `initial()` | `{ thinkingLevel: "off", activeTools: [] }` |
582
+ | checkpoint | complete base on every change |
583
+ | view mount | `docs["pi.conversation.config"]` |
584
+
585
+ It has no prompt sections: prompt text is produced per request by the registry
586
+ (section 7.4). Any code may edit it with
587
+ `tx.doc(ConversationConfig, id)`, including in the same commit as a create or
588
+ fork. The getters return immutable committed values, falling back to
589
+ `initial()` when the document is absent; they never write. `getStreamOptions()`
590
+ returns `{}` and `getRetryPolicy()` returns the default policy
591
+ `{ enabled: true, maxRetries: 3, baseDelayMs: 2000, maxAgentDelayMs: 60000 }`
592
+ when the field is absent; `setRetryPolicy(undefined)` removes the field.
593
+ `streamOptions` are forwarded to every generation request of the conversation;
594
+ `streamOptions.maxRetries` are provider retries inside one request, while `retry`
595
+ governs durable generation attempts (section 8). Each setter performs
596
+ one ordinary Session commit against the document. A setter does not start
424
597
  generation or append a system entry. Request preparation later compares the
425
- desired committed configuration with transcript history and appends the required
598
+ desired configuration with transcript history and appends the required
426
599
  positional system baseline or delta.
427
600
 
428
- Tool declarations and executable functions are process-local Harness registry
429
- entries, supplied through `HarnessOptions.tools` or `registerTool()`. The durable
430
- configuration document stores only active tool names. A new independent
431
- conversation defaults to every tool registered when its creation is admitted;
432
- `ConversationSpec.activeTools` overrides that default. A fork with no explicit
433
- `activeTools` uses the configuration document's `asOf` value.
434
-
435
- `registerTool()` changes only the runtime registry. It does not activate the tool
436
- in existing conversations or write a document. `setActiveTools()` is the only
437
- host operation that replaces one existing conversation's durable active set.
438
- Explicit `ConversationSpec.activeTools` and `setActiveTools()` values must contain
439
- unique names that are registered when their commit is admitted; otherwise the
440
- whole operation rejects without a write. Inherited historical names are not
441
- revalidated during a fork and may later be unavailable after registry movement.
442
- Unregistering a tool does not rewrite any conversation.
443
-
444
- If request preparation finds unavailable active names, it performs no provider
445
- request. It atomically terminalizes the generation task as `failed` with
446
- `detail: { code: "missing_active_tool", names }`, makes its placed inputs
447
- `unanswered` with reason `missing_active_tool`, clears matching turn control, and
448
- appends a visible model-less diagnostic entry. It does not silently change the
449
- durable loadout. Historical system entries remain replayable because they store
450
- the exact declarations actually offered to prior requests.
601
+ Default active tools for a root or independent conversation, including one
602
+ created through `tx.createConversation()` in a Harness commit, are the names of
603
+ every tool registered when its creation commit runs, in registry order.
604
+ `setActiveTools()` rejects the whole operation without a write when a name is
605
+ duplicated or when a name that was not already active in that conversation is
606
+ not registered. The create/fork conveniences check only one thing about `init`
607
+ writes: names that `init` newly activates must be registered. Names that were
608
+ already active are never revalidated, so stale unregistered names survive every
609
+ edit. All other raw document writes are trusted and unchecked, so other writers,
610
+ such as post-tools `addTools`, never fault because of registry movement. Readers tolerate what raw
611
+ writes can produce: request preparation offers the first occurrence of a
612
+ duplicated name, and post-tools `addTools` appends only names not already
613
+ active.
614
+
615
+ `init` runs after the conversation creation, which is a table write, so table
616
+ reads inside it throw `ReadAfterWrite` (section 4). Document access remains
617
+ available.
618
+
619
+ The configuration document records the desired loadout; the registry records
620
+ what this process can execute now. An active name may therefore be unregistered,
621
+ for example during extension reload, after restart before registration, or in a
622
+ fork opened by a process that lacks an extension. The document is never
623
+ rewritten because of registry movement, so a re-registered tool becomes available
624
+ again without host action. Hosts derive current availability as active names
625
+ that are currently registered.
626
+
627
+ A missing tool implementation never fails a request. Request preparation offers
628
+ only active names that are currently registered. If the replayed tool state still
629
+ offers an unregistered name, the appended system delta lists it in
630
+ `toolsRemoved`; when the name is registered again, a later delta adds its current
631
+ declaration. When the model calls a tool that is not offered or whose
632
+ implementation is unavailable at execution time, the tool task appends an error
633
+ tool result with `details: { code: "tool_unavailable" }` stating that the tool is
634
+ not available, and the run continues so the model can react.
451
635
 
452
636
  A `Conversation.commit()` is a Session commit bound to that conversation.
453
637
  `tx.createTask()` defaults `TaskOptions.conversationId` to the bound conversation.
@@ -456,57 +640,74 @@ fork-aware stored history; callers cannot substitute another conversation ID.
456
640
  Generic Session-wide document operations remain available directly on `Harness`
457
641
  because `Harness extends Session`.
458
642
 
459
- `fork()` requires a concrete visible parent entry and applies section 3.7.
643
+ `fork()` requires a concrete visible parent entry and explicit ownership, then
644
+ applies section 3.7.
460
645
  `collapse()` returns the newly admitted background collapse task ID, not its
461
646
  future summary entry. `reset()` durably admits a passive self-head reset or
462
647
  handoff write and then resolves; while busy, placement follows section 6 and may
463
648
  occur later. Observe its placement through the conversation watch. An idle wait
464
649
  does not guarantee placement of queued passive writes.
465
650
 
466
- `markTask()` only commits `abortRequested`; it neither signals nor joins an
467
- active invocation. The scheduler notices the mark on its next drain.
468
- `abortTask()` also signals and joins an active run before starting the abort
469
- invocation. `Conversation.abort()` withdraws queued non-write inputs, marks
470
- non-background tasks in the conversation and owned subtree, signals them, and
471
- resolves only after that subtree is ordinarily idle. Passive writes and
472
- background tasks survive. Conversation idle means no non-background live task
473
- in that conversation or its ownership subtree; Harness idle applies the same
474
- rule Session-wide. Pending dependency- or deadline-blocked work is still live
475
- and therefore not idle. Cancelling an idle wait aborts only that waiter.
476
-
477
- `onConversation()` synchronously visits the currently loaded committed
478
- conversations in ascending ID order, then reports each later creation after its
479
- commit. Listener failures go to `onReport` and do not stop other listeners. Its
480
- idempotent disposer removes the listener; Harness close removes all remaining
481
- listeners.
482
-
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.
651
+ `abortTask()` commits `abortRequested` and the durable foreground-subtree
652
+ cascade, then signals and joins the active run; the scheduler then starts the
653
+ abort invocation. Marking without signalling is internal: the cascade marks
654
+ owned tasks in the same commit. `Conversation.abort()` withdraws queued
655
+ input submissions, marks non-background tasks selected by ordinary ownership
656
+ traversal, signals them, and resolves only after that scope is ordinarily idle.
657
+ Passive writes and background subtrees survive. Conversation idle means no live
658
+ non-background task selected from that conversation. Harness idle applies the
659
+ same traversal from every ownerless conversation root. Pending dependency- or
660
+ deadline-blocked work is still live and therefore not idle. Cancelling an idle
661
+ wait aborts only that waiter.
662
+
663
+ Conversation handles are stateless; compare them by `id`. Hosts discover
664
+ conversations through lookups and scans. An activity view that lists active
665
+ conversations and reports conversations becoming active or idle is specified
666
+ with run control (Package 17); there is no creation listener.
667
+
668
+ `submit()` returns after durable admission, not settlement. An input submission
669
+ creates a user message with the admission timestamp; `whenBusy` defaults to
670
+ `followUp`. A write submission uses the ordered passive path in section 6 and
671
+ never starts generation. `Submission.wait()` settles an input only after its
672
+ answer or terminal failure; it settles a write when the entry is placed or the
673
+ write becomes terminally unplaceable. Cancelling `wait()` only cancels that wait.
674
+ It does not withdraw the submission; `Submission.abort()` is the explicit queued
675
+ withdrawal operation. `Harness.submission()` reacquires a submission after
676
+ reopen; records remain queryable after settlement.
490
677
 
491
678
  `abortTask()` durably requests cancellation and returns `marked` after the mark
492
679
  is committed, any active run invocation has joined, and an abort invocation has
493
- been scheduled; it does not await terminal settlement. `waitForTask()` observes
680
+ been scheduled, or a blocked task has been settled as `orphaned` (section 5.4);
681
+ it does not await terminal settlement. `waitForTask()` observes
494
682
  the terminal receipt. Aborting an already terminal task returns `terminal`; an
495
683
  unknown ID rejects. Explicit task abort includes a background task. Cancelling a
496
684
  task or idle wait does not abort work.
497
685
 
498
- `Conversation.watch()` atomically captures an immutable structural view and
499
- registers for later complete Session commits. Its `WatchHandle` uses the same
500
- serialized asynchronous update and reset-compaction contract as `watchDoc()` in
501
- section 9.2. It carries no semantic events and owns no second tracker.
502
-
503
- `close()` is equivalent to `suspend()` for v1. It seals mutation admission and
504
- task reservation, signals invocations, and stops watches. Outside the Session
505
- line it lets already-admitted storage commits settle, joins task/tool/hook
506
- invocations and in-flight watch callbacks, then closes sources and storage. It
507
- writes no task outcome. Handles belong to that open Harness and must be
508
- reacquired after reopen. A non-cooperative watch callback can delay graceful
509
- close just like a non-cooperative task invocation.
686
+ `inspect()` returns live work at one point on the Session line, for recovery
687
+ decisions after open, viewers, and diagnostics. It writes nothing, does not
688
+ enable scheduling, and runs no task code. Each pending or running task carries
689
+ its state under the current registry: `running` with an active invocation;
690
+ `ready` when the next scheduling pass would reserve it, with `migrates` when its
691
+ definition is newer and has `migrate`; `waiting` on unfinished dependencies; or
692
+ `blocked` (section 5.4). A migration shows as failed only after the scheduler
693
+ tried it, or when the newer definition has no `migrate`; inspection never runs
694
+ one to find out. Blocked reasons are derived, never stored. Queued and placed
695
+ submissions and the registry's wrapper failures complete the view. Finished
696
+ tasks, settled submissions, transcripts, and documents use their own APIs.
697
+
698
+ `Conversation.viewState()` returns its current structural mount as a disposable
699
+ read-only Chord state. `Conversation.watch()` atomically captures that immutable
700
+ revision and registers for later exact complete-commit frames. Its `WatchHandle`
701
+ uses the same serialized asynchronous, bounded-buffer contract as `watchDoc()`
702
+ in section 9.2. Neither carries semantic events or owns a second persistence
703
+ authority.
704
+
705
+ `close()` seals mutation admission and
706
+ task reservation, signals invocations, and stops future watch deliveries. Outside
707
+ the Session line it lets already-admitted storage commits settle, joins
708
+ task/tool/hook invocations, then closes states and storage. Already-running watch
709
+ callbacks remain caller-owned and may finish independently. Close writes no task
710
+ outcome. Handles belong to that open Harness and must be reacquired after reopen.
510
711
 
511
712
  ## 3. Documents
512
713
 
@@ -534,12 +735,17 @@ type DocumentSemantics =
534
735
  | RewindableConversationSemantics
535
736
  | { readonly scope: "task" };
536
737
 
738
+ type CheckpointInfo = {
739
+ /** Deltas already stored after the newest base, excluding this change. */
740
+ readonly deltasSinceBase: number;
741
+ };
742
+
537
743
  type CommonDocDefinition<T extends JsonObject> = {
538
744
  readonly kind: string;
539
745
  readonly version: number;
540
746
  initial(): T;
541
747
  migrate?(value: JsonObject, fromVersion: number): T;
542
- checkpointWhen?(value: Readonly<T>, ops: readonly Op[]): boolean;
748
+ checkpointWhen?(value: Readonly<T>, ops: readonly Op[], info: CheckpointInfo): boolean;
543
749
  };
544
750
 
545
751
  type DocDefinition<T extends JsonObject> =
@@ -548,45 +754,100 @@ type DocDefinition<T extends JsonObject> =
548
754
  type DocFamilyDefinition<T extends JsonObject, I extends JsonValue> =
549
755
  Omit<CommonDocDefinition<T>, "initial"> & DocumentSemantics & {
550
756
  readonly family: true;
551
- initial(input: I): T;
757
+ initial(seed: I): T;
552
758
  };
553
759
 
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
760
  declare const docType: unique symbol;
571
- interface DocToken<T extends JsonObject> {
572
- readonly definition: DocDefinition<T>;
761
+ interface DocToken<T extends JsonObject, D extends DocDefinition<T>> {
762
+ readonly definition: D;
573
763
  readonly [docType]?: T;
574
764
  }
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>;
765
+ interface DocFamilyToken<
766
+ T extends JsonObject,
767
+ I extends JsonValue,
768
+ D extends DocFamilyDefinition<T, I>,
769
+ > {
770
+ readonly definition: D;
580
771
  readonly [docType]?: T;
581
772
  }
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
- }
773
+
774
+ type SessionDocToken<T extends JsonObject> = DocToken<
775
+ T,
776
+ CommonDocDefinition<T> & { readonly scope: "session" }
777
+ >;
778
+ type ConversationDocToken<T extends JsonObject> = DocToken<
779
+ T,
780
+ CommonDocDefinition<T> & (LatestConversationSemantics | RewindableConversationSemantics)
781
+ >;
782
+ type RewindableConversationDocToken<T extends JsonObject> = DocToken<
783
+ T,
784
+ CommonDocDefinition<T> & RewindableConversationSemantics
785
+ >;
786
+ type TaskDocToken<T extends JsonObject> = DocToken<
787
+ T,
788
+ CommonDocDefinition<T> & { readonly scope: "task" }
789
+ >;
790
+
791
+ type SessionDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
792
+ T,
793
+ I,
794
+ DocFamilyDefinition<T, I> & { readonly scope: "session" }
795
+ >;
796
+ type ConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
797
+ T,
798
+ I,
799
+ DocFamilyDefinition<T, I> & (LatestConversationSemantics | RewindableConversationSemantics)
800
+ >;
801
+ type RewindableConversationDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
802
+ T,
803
+ I,
804
+ DocFamilyDefinition<T, I> & RewindableConversationSemantics
805
+ >;
806
+ type TaskDocFamilyToken<T extends JsonObject, I extends JsonValue> = DocFamilyToken<
807
+ T,
808
+ I,
809
+ DocFamilyDefinition<T, I> & { readonly scope: "task" }
810
+ >;
811
+
812
+ function defineDoc<T extends JsonObject>(
813
+ definition: CommonDocDefinition<T> & { readonly scope: "session" },
814
+ ): SessionDocToken<T>;
815
+ function defineDoc<T extends JsonObject>(
816
+ definition: CommonDocDefinition<T> & LatestConversationSemantics,
817
+ ): ConversationDocToken<T>;
818
+ function defineDoc<T extends JsonObject>(
819
+ definition: CommonDocDefinition<T> & RewindableConversationSemantics,
820
+ ): RewindableConversationDocToken<T>;
821
+ function defineDoc<T extends JsonObject>(
822
+ definition: CommonDocDefinition<T> & { readonly scope: "task" },
823
+ ): TaskDocToken<T>;
824
+
825
+ function defineDocFamily<T extends JsonObject, I extends JsonValue>(
826
+ definition: Omit<CommonDocDefinition<T>, "initial"> & {
827
+ readonly family: true;
828
+ readonly scope: "session";
829
+ initial(seed: I): T;
830
+ },
831
+ ): SessionDocFamilyToken<T, I>;
832
+ function defineDocFamily<T extends JsonObject, I extends JsonValue>(
833
+ definition: Omit<CommonDocDefinition<T>, "initial"> & LatestConversationSemantics & {
834
+ readonly family: true;
835
+ initial(seed: I): T;
836
+ },
837
+ ): ConversationDocFamilyToken<T, I>;
838
+ function defineDocFamily<T extends JsonObject, I extends JsonValue>(
839
+ definition: Omit<CommonDocDefinition<T>, "initial"> & RewindableConversationSemantics & {
840
+ readonly family: true;
841
+ initial(seed: I): T;
842
+ },
843
+ ): RewindableConversationDocFamilyToken<T, I>;
844
+ function defineDocFamily<T extends JsonObject, I extends JsonValue>(
845
+ definition: Omit<CommonDocDefinition<T>, "initial"> & {
846
+ readonly family: true;
847
+ readonly scope: "task";
848
+ initial(seed: I): T;
849
+ },
850
+ ): TaskDocFamilyToken<T, I>;
590
851
  ```
591
852
 
592
853
  Validation rules:
@@ -612,25 +873,25 @@ case model, tool, inbox, or presentation state.
612
873
 
613
874
  ### 3.2 Records and lifetimes
614
875
 
615
- A persisted document instance has one lifecycle record:
876
+ A persisted document instance has one `DocumentRecord`:
616
877
 
617
878
  ```ts
618
879
  type DocumentRecord = {
619
- readonly id: Id; // unique incarnation
880
+ readonly id: DocumentId; // unique incarnation
620
881
  readonly kind: string; // stable definition kind
621
882
  readonly key?: string; // families only
622
883
  readonly createdAt: Seq; // stamped by the committing storage
623
884
  readonly retiredAt?: Seq;
624
885
  } & (
625
886
  | { readonly scope: { readonly kind: "session" } }
626
- | ({ readonly scope: { readonly kind: "conversation"; readonly conversationId: Id } } & (
887
+ | ({ readonly scope: { readonly kind: "conversation"; readonly conversationId: ConversationId } } & (
627
888
  | { readonly history: "latest"; readonly fork: "current" | "initial" }
628
889
  | {
629
890
  readonly history: "rewindable";
630
891
  readonly fork: "asOf" | "current" | "initial";
631
892
  }
632
893
  ))
633
- | { readonly scope: { readonly kind: "task"; readonly taskId: Id } }
894
+ | { readonly scope: { readonly kind: "task"; readonly taskId: TaskId } }
634
895
  );
635
896
 
636
897
  type DocumentCreate = DocumentRecord extends infer Record
@@ -659,117 +920,121 @@ There is no mutable `session.document()` API.
659
920
 
660
921
  ```ts
661
922
  interface Session extends DocumentObserver {
662
- commit<T>(
663
- change: (tx: Tx) => T | Promise<T>,
664
- context: Context,
665
- ): Promise<T>;
666
-
923
+ commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
667
924
  close(context: Context): Promise<void>;
668
-
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>>;
925
+ subscribeCommits(listener: (publication: CommitPublication, context: Context) => void): () => void;
926
+ subscribeClose(listener: () => void): () => void;
927
+
928
+ snapshot<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<Readonly<T> | undefined>;
929
+ snapshot<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<Readonly<T> | undefined>;
930
+ snapshot<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<Readonly<T> | undefined>;
931
+ snapshot<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<Readonly<T> | undefined>;
932
+ snapshot<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<Readonly<T> | undefined>;
933
+ snapshot<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<Readonly<T> | undefined>;
934
+
935
+ snapshotAsOf<T extends JsonObject>(token: RewindableConversationDocToken<T>, conversationId: ConversationId, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
936
+ snapshotAsOf<T extends JsonObject, I extends JsonValue>(token: RewindableConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, at: EntryId, context: Context): Promise<Readonly<T> | undefined>;
937
+
938
+ documentState<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentState<T> | undefined>;
939
+ documentState<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<DocumentState<T> | undefined>;
940
+ documentState<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<DocumentState<T> | undefined>;
941
+ documentState<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentState<T> | undefined>;
942
+ documentState<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
943
+ documentState<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentState<T> | undefined>;
706
944
  }
707
945
 
708
946
  interface Tx {
709
- conversation(id: Id): Promise<ConversationRecord | undefined>;
710
- entry(id: Id): Promise<EntryRecord | undefined>;
711
- input(id: Id): Promise<Input | undefined>;
712
- task(id: Id): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
713
- scanEntries(query: EntryQuery): Promise<readonly EntryRecord[]>;
714
- scanTasks(query: TaskQuery): Promise<readonly TaskRecord<JsonValue, JsonValue, JsonValue>[]>;
715
-
716
- createConversation(value: Omit<ConversationRecord, "id">): ConversationRecord;
717
- appendEntry(conversationId: Id, value: EntryDraft): EntryRecord;
718
- createInput(value: InputCreate): Input;
719
- setInput(value: Input): void;
947
+ conversation(id: ConversationId): Promise<ConversationRecord | undefined>;
948
+ entry(id: EntryId): Promise<EntryRecord | undefined>;
949
+ /** Undefined when the entry is absent or has another kind. */
950
+ entry<D extends JsonValue>(token: Entry<D>, id: EntryId): Promise<TypedEntry<D> | undefined>;
951
+ task(id: TaskId): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
952
+ scanConversations(query: ConversationQuery, limit: number, cursor?: Cursor): Promise<Page<ConversationRecord, Cursor>>;
953
+ scanEntries(query: EntryQuery, limit: number, cursor?: Cursor): Promise<Page<EntryRecord, Cursor>>;
954
+ scanTasks(query: TaskQuery, limit: number, cursor?: Cursor): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
955
+
956
+ createConversation(options: { readonly ownership: ConversationOwnership }): Promise<ConversationRecord>;
957
+ forkConversation(
958
+ parentConversationId: ConversationId,
959
+ at: EntryId,
960
+ options: { readonly ownership: ConversationOwnership },
961
+ ): Promise<ConversationRecord>;
962
+ appendEntry(conversationId: ConversationId, value: EntryDraft): Promise<EntryRecord>;
963
+ /** The token supplies `kind` and types `data`. */
964
+ appendEntry<D extends JsonValue>(
965
+ token: Entry<D>, conversationId: ConversationId, value: TypedEntryDraft<D>,
966
+ ): Promise<TypedEntry<D>>;
720
967
  createTask<I, S extends { phase: string }, R, H extends object>(
721
968
  task: Task<I, S, R, H>, input: I, options?: TaskOptions,
722
- ): TaskRef<R>;
723
- setTask(value: TaskRecord<JsonValue, JsonValue, JsonValue>): void;
724
-
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;
969
+ ): Promise<TaskId<R>>;
970
+ /** Settle a queued or placed submission; only a placed input can be answered. A settled one stays unchanged. */
971
+ settleSubmission(id: SubmissionId, settlement: SubmissionSettlement): void;
972
+
973
+ doc<T extends JsonObject>(token: SessionDocToken<T>): Promise<Draft<T>>;
974
+ doc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId): Promise<Draft<T>>;
975
+ doc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId): Promise<Draft<T>>;
976
+ doc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, seed: I): Promise<Draft<T>>;
977
+ doc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, seed: I): Promise<Draft<T>>;
978
+ doc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, seed: I): Promise<Draft<T>>;
979
+
980
+ retireDoc<T extends JsonObject>(token: SessionDocToken<T>): Promise<void>;
981
+ retireDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId): Promise<void>;
982
+ retireDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId): Promise<void>;
983
+ retireDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string): Promise<void>;
984
+ retireDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string): Promise<void>;
985
+ retireDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string): Promise<void>;
729
986
  }
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
987
  ```
746
988
 
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.
989
+ ID-creating transaction methods are asynchronous because remote storage may
990
+ allocate globally unique numeric IDs durably. Conversation creation always
991
+ requires explicit ownership; no transaction wrapper injects the executing task.
992
+ For task ownership, the caller supplies only a typed task ID. The Session derives
993
+ the persisted owner conversation from the task's final candidate record.
994
+
995
+ An owner task may be committed or staged earlier in the same transaction. Before
996
+ Storage admission, the Session rejects a missing, terminal, or abort-marked owner,
997
+ including one made terminal or abort-marked later in that transaction. Existing
998
+ owner edges remain valid when their owners terminalize afterward. Conversation
999
+ creation returns an inert record, never an operational handle.
1000
+ `forkConversation()` additionally validates one visible entry and applies section
1001
+ 3.7. Only public typed `tx.doc()` is get-or-create. Internal fork copying may create
1002
+ new incarnations directly from stored values without a definition. `tx.doc()`
1003
+ receives the definition token that supplies
1004
+ its static type, initializer, migration, and checkpoint policy. Scope-preserving
1005
+ token overloads require callers to supply only the concrete conversation/task ID
1006
+ and, for a family, its key and creation seed. Ordinary definitions are not
1007
+ registered and ordinary documents are not scanned at open.
751
1008
 
752
1009
  - 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
1010
+ 3.6. A migration reached through `tx.doc()` is staged in its enclosing
1011
+ transaction.
1012
+ - A missing singleton calls the token's `initial()`. A missing family member calls
1013
+ `initial(seed)`. Creation stores an initial base.
1014
+ - The first acquisition of one logical address is memoized before awaiting. Later
1015
+ acquisitions in that transaction return the same draft; for a missing family,
1016
+ the first call's detached seed wins and later seeds are ignored.
1017
+ - `snapshot()`, `snapshotAsOf()`, `documentState()`, and `watchDoc()` never create
1018
+ or persist migration. They return `undefined` when the requested incarnation is
1019
+ absent and migrate a reconstructed value only in memory.
1020
+ - Task-scoped `tx.doc()` validates against the transaction's latest candidate task
759
1021
  record, falling back to committed state. This internal validation is not a
760
1022
  caller table read and does not trigger `ReadAfterWrite`. Its conversation is
761
- derived from the task record rather than repeated in the target.
1023
+ derived from the task record.
1024
+ - `retireDoc()` resolves the logical address without creating it. Retirement of
1025
+ an acquired draft persists its final content before retirement. A later
1026
+ `tx.doc()` at that address in the same transaction creates a new incarnation
1027
+ with a new draft and ID.
762
1028
  - A terminal candidate rejects later task-document access. Terminal settlement
763
1029
  retires both existing task documents and task documents created earlier in the
764
1030
  same transaction.
765
- - `snapshot()` returns a detached JSON copy.
766
- - `documentSource()` returns an opaque committed source.
767
- - `tx.doc()` returns the transaction's mutable tracked draft.
1031
+ - `snapshot()` returns the current shareable immutable revision. Mutation of it
1032
+ or any retained descendant is unsupported; callers that need mutable ownership
1033
+ must copy it first.
1034
+ - `documentState()` returns a disposable read-only Chord state bound to one committed incarnation.
1035
+ - `tx.doc()` returns one revocable Astra overlay `Draft<T>` for the transaction.
768
1036
  - Historical reads never create documents in the past.
769
1037
 
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
1038
  `snapshotAsOf()` is available only for rewindable conversation documents. It
774
1039
  validates that `at` is visible through the requested conversation's ancestry,
775
1040
  selects the ancestor conversation that owns that entry, then finds the logical
@@ -784,51 +1049,119 @@ alive at the target commit.
784
1049
 
785
1050
  ### 3.4 Mutation ownership
786
1051
 
787
- Chord mutates the tracked working object immediately while retaining the
788
- previous baseline until flush.
1052
+ Pico uses Chord Delta's Astra-immutable transaction shape directly:
1053
+
1054
+ ```ts
1055
+ interface Prepared<T extends object> {
1056
+ readonly base: T;
1057
+ readonly value: T;
1058
+ readonly ops: readonly Op[];
1059
+ readonly baseRevision: number;
1060
+ abort(): void;
1061
+ }
1062
+ interface Change<T extends object> {
1063
+ readonly state: Draft<T>;
1064
+ prepare(): Prepared<T>;
1065
+ abort(): void;
1066
+ }
1067
+ interface Tracker<T extends object> {
1068
+ readonly value: T;
1069
+ readonly revision: number;
1070
+ beginChange(): Change<T>;
1071
+ prepareReplace(value: T): Prepared<T>;
1072
+ adopt(prepared: Prepared<T>): void;
1073
+ }
1074
+ ```
1075
+
1076
+ Each loaded document owns one tracker whose `value` is its current immutable
1077
+ revision. Immutability is a trusted ownership contract, not runtime freezing:
1078
+ a revision and its descendants must never be mutated after transfer to the
1079
+ tracker. `prepare()` revokes the draft, emits detached self-contained
1080
+ operations, and computes `value` with the optimized immutable applier before
1081
+ Storage admission. Unchanged subtrees are structurally shared with `base`.
1082
+ Operation placement payloads may also be shared with `value`; mutating either a
1083
+ prepared operation or an immutable revision is unsupported. Astra preserves an
1084
+ empty operation batch for changes it proves are no-ops, but Pico performs no
1085
+ additional whole-value equality pass for nonempty structural batches.
1086
+
1087
+ `abort()` is idempotent. `adopt()` accepts only a prepared result from that
1088
+ tracker at its current revision, then performs only a synchronous pointer swap
1089
+ to the already-computed immutable `value`. It performs no diffing, application,
1090
+ allocation, or callback. The Session line permits at most one open change per
1091
+ tracker and ensures no result becomes stale between preparation, Storage
1092
+ settlement, and adoption.
1093
+
1094
+ The first `tx.doc()` acquisition calls `tracker.beginChange()` and memoizes that
1095
+ change's draft for the rest of the possibly async Session callback.
789
1096
 
790
1097
  Transaction behavior:
791
1098
 
792
1099
  ```text
793
1100
  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
1101
+ acquire and memoize document changes by logical address
1102
+ mutate revocable Astra overlay drafts
1103
+ callback settles
1104
+ seal Tx; synchronously prepare or abort every open change, revoking every draft
1105
+ if any acquisition is pending: abort open changes, reject, then drain and abort it
1106
+ callback fails with no pending acquisition
1107
+ abort every open change; persist and publish nothing
1108
+ callback succeeds with no pending acquisition
1109
+ prepare every open change -> immutable next revision + self-contained Chord Op[]
1110
+ Session evaluates each required/ordinary document write exactly once
1111
+ prepare every affected loaded conversation mount revision
1112
+ Storage.commit persists the atomic batch while the Session line remains held
803
1113
  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
1114
+ adopt every prepared change by pointer swap and enqueue immutable revision/ops publication
806
1115
  release the line; invoke listeners later
807
- post-flush or storage failure
808
- poison Session; publish nothing; close/reopen required
1116
+ storage fails
1117
+ abort every prepared change, poison Session, and publish nothing
809
1118
  ```
810
1119
 
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:
1120
+ A pending acquisition that resolves after sealing never exposes a draft; its
1121
+ change is aborted and its promise rejects. The Session observes every such
1122
+ settlement before releasing the line. Initializer, migration, and replacement
1123
+ roots come from caller code: the Session copies each one into exclusive kernel
1124
+ ownership, rejecting any value that is not strict JSON, before it becomes an
1125
+ immutable tracker revision. Loaded and fork-copy roots are already detached
1126
+ strict JSON from Storage and enter the tracker without another copy. Chord's
1127
+ `track()` and `prepareReplace()` take ownership in O(1) without traversal, so
1128
+ every root they receive must come from one of these sources. Migration callbacks
1129
+ never receive a live tracker revision.
1130
+
1131
+ Preparation, validation, checkpoint, or mounted-view preparation failure occurs
1132
+ before Storage admission and rolls back normally. The Session performs no
1133
+ strict-JSON walk of prepared operations or selected bases: roots are checked on
1134
+ entry and Chord checks every draft placement, so every revision, operation
1135
+ payload, and base is strict JSON by construction. Tracker branding and `baseRevision` enforce ownership and staleness; the
1136
+ Session never substitutes caller-created prepared values. The prepared immutable
1137
+ candidate itself becomes the adopted and published value. Storage receives that complete value only when the
1138
+ Session selects a base; otherwise it receives only the prepared operation batch.
1139
+ An existing current-version document with an empty batch writes and publishes
1140
+ nothing. A nonempty structural batch whose final value is deeply equal to its
1141
+ base remains a valid durable change and publication. Creation and required
1142
+ version transitions still write a base when their prepared batch is empty; an
1143
+ equal-value version base does not emit a watch update.
1144
+
1145
+ Values assigned into a draft are copied immediately by value. Repeated
1146
+ placements are independent. Chord checks each placement while copying it and
1147
+ throws at the offending assignment, before the draft changes, when the value is
1148
+ not strict JSON: `undefined` array elements or nested object values, non-finite
1149
+ numbers, functions, symbols, bigints, accessors, symbol keys, sparse arrays, or
1150
+ objects whose prototype is neither `Object.prototype` nor `null`. Assigning
1151
+ `undefined` directly to an object property deletes that property. Draft reads, draft writes, and all `Tx` operations
1152
+ reject after the callback settles. The prepared immutable value remains readable
1153
+ by Session-owned checkpoint and Storage preparation.
821
1154
 
822
1155
  ```ts
823
- let escaped: LiveState;
1156
+ let escaped: Draft<LiveState>;
824
1157
  await session.commit(async tx => {
825
- escaped = await tx.doc(LiveDoc, { scope: "conversation", conversationId });
1158
+ escaped = await tx.doc(LiveDoc, conversationId);
826
1159
  });
827
- escaped.message = message; // unsupported
1160
+ escaped.message = message; // throws: the draft was revoked
828
1161
  ```
829
1162
 
830
- The same rule applies to nested proxies and mutable objects assigned into a
831
- document.
1163
+ Fire-and-forget work that mutates a draft before the owner callback settles may
1164
+ silently enter that transaction and is unsupported.
832
1165
 
833
1166
  ### 3.5 Bases and checkpoints
834
1167
 
@@ -838,21 +1171,26 @@ For an ordinary later mutation, the definition alone decides whether the
838
1171
  storage record is a base:
839
1172
 
840
1173
  ```ts
841
- const useBase = definition.checkpointWhen?.(candidateValue, ops) ?? false;
1174
+ const useBase = definition.checkpointWhen?.(candidateValue, ops, { deltasSinceBase }) ?? false;
842
1175
  ```
843
1176
 
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:
1177
+ The Session evaluates this predicate exactly once after tracker preparation.
1178
+ `deltasSinceBase` counts the deltas already stored after the incarnation's newest
1179
+ base, excluding the change being evaluated. Storage reports it when it
1180
+ materializes the current value; the Session advances it after each adopted write
1181
+ and resets it after every base, so evaluation performs no Storage read.
1182
+ Creation and version transitions require bases and do not call it. The Session
1183
+ then gives Storage only the selected representation:
847
1184
 
848
1185
  ```text
849
1186
  required or predicate true -> base with complete value
850
- otherwise -> delta with incremental ops
1187
+ otherwise -> delta with the prepared Chord operation batch
851
1188
  ```
852
1189
 
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.
1190
+ A Chord root-replacement operation remains a delta unless the definition selected
1191
+ a checkpoint; it does not authorize reclamation. Predicate failure aborts the
1192
+ prepared transaction before Storage admission. Storage executes no definition
1193
+ code and never receives an unused complete candidate with a selected delta.
856
1194
 
857
1195
  - For Session, task, and latest conversation documents, a committed base permits
858
1196
  physical reclamation of older records.
@@ -863,6 +1201,12 @@ an unused complete candidate with a selected delta.
863
1201
  - Storage does not count encoded bytes, compare against `initial()`, or invent
864
1202
  checkpoints.
865
1203
 
1204
+ A definition can bound replay directly:
1205
+
1206
+ ```ts
1207
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 31
1208
+ ```
1209
+
866
1210
  A high-churn live document can checkpoint when it becomes empty:
867
1211
 
868
1212
  ```ts
@@ -883,20 +1227,26 @@ no migrate -> reject older stored version
883
1227
  ```
884
1228
 
885
1229
  `migrate()` is pure and returns a complete current-version value. Migration is
886
- access-driven: `tx.doc()`, `snapshot()`, `snapshotAsOf()`, `documentSource()`, and
1230
+ access-driven: `tx.doc()`, `snapshot()`, `snapshotAsOf()`, `documentState()`, and
887
1231
  `watchDoc()` reconstruct and migrate through the token supplied to that call.
888
1232
  Harness open does not sweep ordinary documents.
889
1233
 
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.
1234
+ - Read-only access migrates only in memory and never writes. It may cache a
1235
+ tracker over that migrated immutable revision together with the older stored
1236
+ version marker; the next successful `tx.doc()` access still writes the required
1237
+ current-version base. A cached tracker serves only tokens of the version its value was
1238
+ materialized for. Access with any other version, such as a token from reloaded
1239
+ extension code, drops it and reloads the stored value, so each token migrates
1240
+ from the stored version or rejects a newer one. Migration always starts from a detached stored value.
1241
+ `tx.doc()` stages migration in its enclosing transaction; callback failure
1242
+ persists nothing, and later draft edits coalesce into one final required base.
894
1243
  - Rewindable history is not rewritten. Current and historical reconstructed
895
1244
  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.
1245
+ - The first `tx.doc()` transaction after any stored-version migration writes a
1246
+ required current-version base, even when the migrated JSON is deeply equal.
1247
+ - A fork obtains the selected stored value/version from Storage rather than a
1248
+ typed migrated tracker cache. The child copies that stored pair and migrates on
1249
+ later typed access.
900
1250
  - Unaccessed documents and documents with unavailable definitions preserve their stored instances,
901
1251
  versions, and bytes.
902
1252
 
@@ -909,12 +1259,12 @@ appended later entries. Document state at `E` is the final state of the commit
909
1259
  containing `E`. Different document states require separate commits.
910
1260
 
911
1261
  Each conversation document follows the history/fork policy persisted in its
912
- lifecycle record:
1262
+ `DocumentRecord`:
913
1263
 
914
1264
  | conversation setting | child value |
915
1265
  |---|---|
916
1266
  | `fork: "asOf"` | parent value at `E`'s commit |
917
- | `fork: "current"` | parent value when the fork commit runs |
1267
+ | `fork: "current"` | committed parent value selected when the fork commit runs |
918
1268
  | `fork: "initial"` | no copied instance; initializer on first child access |
919
1269
 
920
1270
  `current` and `asOf` copy logically present conversation singleton and family
@@ -924,33 +1274,65 @@ values become independent child instances with new IDs and initial bases.
924
1274
  supplied definition. Task documents and tasks are never copied. Session documents
925
1275
  remain shared and are not rewindable.
926
1276
 
1277
+ Fork copying reads committed stored values rather than typed tracker caches. A
1278
+ transaction that creates a fork therefore rejects if it also writes one of the
1279
+ parent's `fork: "current"` documents; commit the parent change first so the fork
1280
+ has one unambiguous stored source revision.
1281
+
1282
+ Forks stage backend-side `document.copy` commands carrying the child create
1283
+ record and an exact source incarnation/point. Storage materializes each source
1284
+ and persists its stored value/version as the child's independent initial base;
1285
+ remote Storage performs this server-side. Every copy reads committed pre-batch
1286
+ source state independent of write-array order. A selected source may not be
1287
+ created, changed, or retired in the same batch. Copying remains atomic with the
1288
+ conversation, overrides, registry writes, and other mutations.
1289
+
1290
+ An unaccessed copy retains only its descriptor in Session memory. Typed access
1291
+ inside the creating transaction lazily reads the detached source, migrates when
1292
+ required, and replaces the copy with one ordinary child create containing the
1293
+ final prepared value. Definition-free copies publish explicit `document.copy`
1294
+ metadata rather than a value. That metadata announces Storage-backed initial
1295
+ state and is never interpreted as a document value. Document states, watches,
1296
+ and mounted views hydrate by capturing their baseline and subscription
1297
+ atomically on the Session line: a later commit already present becomes the
1298
+ baseline, while one committed after registration is delivered. Publications
1299
+ and watches are convergence mechanisms, not audit streams. A mounted aggregate
1300
+ must acquire all of its document baselines and commit subscription in one
1301
+ Session-line operation so it never exposes a mixture from one commit.
1302
+
927
1303
  ## 4. Transactions and storage ownership
928
1304
 
929
1305
  A Session commit callback may be asynchronous. It owns the Session mutation
930
1306
  line through callback execution, preparation, storage settlement, committed
931
- baseline adoption, and publication enqueue. Listener callbacks run later.
1307
+ baseline adoption, and publication enqueue. Commit observers run on the
1308
+ line; document-state and watch user callbacks run later.
932
1309
  External model, process, tool, network, and human effects run outside it.
1310
+ `subscribeCommits()` observes complete immutable publications synchronously on
1311
+ the line after adoption. `subscribeClose()` observes close synchronously when it
1312
+ begins, after admission is sealed; the Harness stops watches and signals task
1313
+ invocations there. Both return idempotent disposers; their listeners must not
1314
+ throw, block, or call Session APIs. Document-state subscribers and watch
1315
+ listeners still run later, off the line.
933
1316
 
934
1317
  ```ts
935
1318
  await session.commit(async tx => {
936
- const input = await tx.input(inputId); // table read
937
- const live = await tx.doc(LiveDoc, { scope: "conversation", conversationId });
1319
+ const conversation = await tx.conversation(conversationId); // table read
1320
+ const live = await tx.doc(LiveDoc, conversationId);
938
1321
 
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 });
1322
+ await tx.appendEntry(conversationId, message); // first table write
1323
+ delete live.message; // document mutation remains valid
1324
+ await tx.createTask(Follow, { after: message.id }); // further table writes are fine
942
1325
  }, context);
943
1326
  ```
944
1327
 
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
1328
+ Mutation admission occurs on the Session line before a commit callback starts. Closing seals mutation admission and task
947
1329
  reservation. Already-admitted commits settle before storage closes. Once a
948
1330
  commit is admitted, caller cancellation does not interrupt storage settlement or
949
1331
  undo the commit. Cancelling a close wait does not reopen admission.
950
1332
 
951
1333
  Table rules:
952
1334
 
953
- - Tables are conversations, entries, tasks, and inputs.
1335
+ - Tables are conversations, entries, tasks, and submissions.
954
1336
  - Table reads are allowed before the first table write.
955
1337
  - Any table read after the first table write throws `ReadAfterWrite`.
956
1338
  - Document access and read-your-writes remain available after table writes.
@@ -972,10 +1354,10 @@ Storage ownership:
972
1354
  ```ts
973
1355
  type TaskOutcome<R> =
974
1356
  | { readonly status: "completed"; readonly result: R }
975
- | { readonly status: "failed"; readonly error: StoredError; readonly result?: R }
1357
+ | { readonly status: "failed"; readonly error: TaskOutcomeError; readonly result?: R }
976
1358
  | { readonly status: "aborted"; readonly reason?: string; readonly result?: R }
977
1359
  | { readonly status: "orphaned"; readonly reason: string }
978
- | { readonly status: "faulted"; readonly error: StoredError };
1360
+ | { readonly status: "faulted"; readonly error: TaskOutcomeError };
979
1361
 
980
1362
  type TaskState<S, R> =
981
1363
  | { readonly status: "pending"; readonly checkpoint: S }
@@ -983,12 +1365,12 @@ type TaskState<S, R> =
983
1365
  | { readonly status: "terminal"; readonly outcome: TaskOutcome<R> };
984
1366
 
985
1367
  type TaskRecord<I, S, R> = {
986
- readonly id: Id;
987
- readonly conversationId: Id;
1368
+ readonly id: TaskId<R>;
1369
+ readonly conversationId: ConversationId;
988
1370
  readonly kind: string;
989
1371
  readonly version: number;
990
1372
  readonly input: I;
991
- readonly after: readonly Id[];
1373
+ readonly after: readonly TaskId[];
992
1374
  readonly background: boolean;
993
1375
  readonly abortRequested: boolean;
994
1376
  } & (
@@ -1006,6 +1388,9 @@ type RunningTask<I, S, R> = TaskRecord<I, S, R> & {
1006
1388
  readonly state: Extract<TaskState<S, R>, { status: "running" }>;
1007
1389
  };
1008
1390
 
1391
+ /** State a task commits for itself: a replacement checkpoint or its terminal outcome. */
1392
+ type NextTaskState<S, R> = Extract<TaskState<S, R>, { status: "running" | "terminal" }>;
1393
+
1009
1394
  interface HookRunner<H extends object> {
1010
1395
  each<K extends keyof H>(name: K, invoke: (handler: H[K]) => void | Promise<void>): Promise<void>;
1011
1396
  }
@@ -1016,19 +1401,32 @@ type PhaseHandler<I, P, S, R, H extends object> = (
1016
1401
  context: Context,
1017
1402
  ) => Promise<void>;
1018
1403
 
1019
- interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver {
1020
- readonly taskId: Id;
1021
- readonly conversationId: Id;
1404
+ interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver, DocumentReader {
1405
+ readonly taskId: TaskId<R>;
1406
+ readonly conversationId: ConversationId;
1022
1407
  readonly signal: AbortSignal;
1408
+ /** Registry snapshot of the current phase; refreshed at every phase boundary. */
1409
+ readonly registry: RegistrySnapshot<ToolRegistration>;
1410
+ readonly models: Models;
1023
1411
  readonly hooks: HookRunner<H>;
1024
1412
 
1025
1413
  commit(
1026
- change: (tx: Tx, current: RunningTask<I, S, R>) => void | Promise<void>,
1414
+ change: (
1415
+ tx: Tx,
1416
+ current: RunningTask<I, S, R>,
1417
+ ) => NextTaskState<S, R> | undefined | Promise<NextTaskState<S, R> | undefined>,
1027
1418
  context: Context,
1028
1419
  ): Promise<void>;
1029
1420
 
1030
1421
  memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
1031
1422
  memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
1423
+ conversation(id: ConversationId, context: Context): Promise<ConversationHandle | undefined>;
1424
+ /** Committed raw active transcript and model context, optionally cut off at `at`. */
1425
+ context(conversationId: ConversationId, context: Context, at?: EntryId): Promise<ContextView>;
1426
+ /** The Harness clock. */
1427
+ now(): number;
1428
+ /** Forward a non-fatal failure to `HarnessOptions.onReport`. */
1429
+ report(error: unknown): void;
1032
1430
  sleep(until: number, context: Context): Promise<void>;
1033
1431
  }
1034
1432
 
@@ -1039,7 +1437,7 @@ type TaskDefinition<I, S extends { phase: string }, R, H extends object> = {
1039
1437
  readonly phases: {
1040
1438
  [P in S["phase"]]: PhaseHandler<I, Extract<S, { phase: P }>, S, R, H>;
1041
1439
  };
1042
- abort(task: TaskRecord<I, S, R>, runtime: TaskRuntime<I, S, R, H>, context: Context): Promise<void>;
1440
+ abort(task: RunningTask<I, S, R>, runtime: TaskRuntime<I, S, R, H>, context: Context): Promise<void>;
1043
1441
  migrate?(input: JsonValue, checkpoint: JsonValue, fromVersion: number): {
1044
1442
  input: I;
1045
1443
  checkpoint: S;
@@ -1051,11 +1449,9 @@ interface Task<I, S extends { phase: string }, R, H extends object> {
1051
1449
  readonly definition: TaskDefinition<I, S, R, H>;
1052
1450
  }
1053
1451
 
1054
- declare const taskResultType: unique symbol;
1055
- type TaskRef<R> = { readonly id: Id; readonly [taskResultType]?: R };
1056
1452
  type TaskOptions = {
1057
- readonly conversationId?: Id;
1058
- readonly after?: readonly Id[];
1453
+ readonly conversationId?: ConversationId;
1454
+ readonly after?: readonly TaskId[];
1059
1455
  readonly background?: boolean;
1060
1456
  };
1061
1457
 
@@ -1064,34 +1460,98 @@ function defineTask<I, S extends { phase: string }, R, H extends object = {}>(
1064
1460
  ): Task<I, S, R, H>;
1065
1461
  ```
1066
1462
 
1463
+ `TaskId<R>` is the numeric task ID itself with an erased result-type brand. It
1464
+ replaces a separate task-reference wrapper: `createTask()` returns `TaskId<R>`,
1465
+ typed waits infer `R` from that ID, and result-agnostic operations accept
1466
+ `TaskId<unknown>`. A task's `conversationId` is immutable after creation; a
1467
+ replacement that moves an existing task to another conversation rejects before
1468
+ Storage admission so persisted conversation-owner edges cannot become stale.
1469
+
1067
1470
  The phase map is exhaustive and phase-narrowed. A handler may perform several
1068
1471
  commits around one effect, but each durable checkpoint is a full replacement.
1069
1472
  `TaskRuntime.commit()` rereads and gates the current durable task on the Session
1070
- line before invoking its callback. Transaction methods replace its checkpoint
1071
- or write its terminal outcome.
1473
+ line before invoking its callback. It rejects when the invocation has ended, the
1474
+ Harness is closing, the task is terminal, or a run invocation's task carries an
1475
+ abort mark. Its `tx.createTask()` defaults to the task's conversation, and every
1476
+ entry it appends records the task as `byTaskId`. When the
1477
+ callback returns a state, the runtime replaces the task's state in the same
1478
+ commit, so the checkpoint or outcome is atomic with the callback's entries,
1479
+ documents, and child tasks and is type-checked against the task's checkpoint and
1480
+ result types. Returning nothing leaves the state unchanged. A terminal state
1481
+ drops the memos. `pending` is never returned; only reconciliation and handover
1482
+ write it.
1483
+
1484
+ `Tx` has no task replacement operation. A task changes only its own state,
1485
+ through its runtime. The scheduler owns reservation, reconciliation, handover,
1486
+ faults, and orphaning; `abortTask()` owns abort marks. Other code
1487
+ stops a task with `abortTask()` and reads its result with `waitForTask()`.
1488
+
1489
+ ```ts
1490
+ // Intent, effect, outcome.
1491
+ prepare: async (task, runtime, context) => {
1492
+ await runtime.commit(() => ({ status: "running", checkpoint: { phase: "charge", key: newKey() } }), context);
1493
+ },
1494
+ charge: async (task, runtime, context) => {
1495
+ const receipt = await payments.charge(task.state.checkpoint.key); // idempotent by key
1496
+ await runtime.commit(async (tx, current) => {
1497
+ const entry = await tx.appendEntry(current.conversationId, receiptEntry(receipt));
1498
+ return { status: "terminal", outcome: { status: "completed", result: { entryId: entry.id } } };
1499
+ }, context);
1500
+ },
1501
+ // The abort handler decides the outcome; returning without one faults the task.
1502
+ abort: async (task, runtime, context) => {
1503
+ await payments.cancel(task.state.checkpoint);
1504
+ await runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted", reason: "user" } }), context);
1505
+ },
1506
+ ```
1507
+ `memo(name, candidate)` is one gated commit; `memo(name)` reads the committed
1508
+ record. `sleep(until)` compares against the Harness `now` clock and rejects when
1509
+ the invocation is signalled or its context is cancelled. Watches acquired through
1510
+ the runtime stop when the invocation ends. `snapshot()`/`snapshotAsOf()` read
1511
+ committed documents, for example to supply `PromptInput.read` (section 7.4).
1512
+ `context()` captures its bounds on the Session line and derives the view from
1513
+ immutable entries off the line, like `Conversation.context()`. Like every runtime
1514
+ operation, these reject after the invocation ends.
1072
1515
 
1073
1516
  Reservation durably changes `pending` to `running`. One invocation runs phase
1074
- handlers in sequence; checkpoint commits retain `running`. After a handler
1075
- settles, the scheduler rereads the task and applies the first matching rule:
1517
+ handlers in sequence; checkpoint commits retain `running`. Before every phase,
1518
+ including the first, the scheduler runs one step callback on the Session line.
1519
+ It reads the committed task and synchronously applies the first matching rule
1520
+ below to the phase that just returned. It writes any fault or handover in the
1521
+ same commit, together with the Harness cleanup for a scheduler-written outcome
1522
+ (section 5.4), which may await document access inside that callback. It ends
1523
+ the invocation there when a rule stops it. A runtime commit the
1524
+ invocation queued earlier therefore either lands before the step and counts, or
1525
+ reaches the line after it and rejects. Before the first phase only rules 1–3
1526
+ apply:
1076
1527
 
1077
1528
  1. Terminal: stop.
1078
1529
  2. Session closing: stop; preserve the checkpoint and any abort mark for reopen.
1079
1530
  3. Run mode with a durable abort mark: end and join the run invocation, then
1080
1531
  dispatch a fresh abort invocation.
1081
1532
  4. Uncaught error: write terminal `faulted`.
1082
- 5. Checkpoint changed, including progress within the same phase: invoke its
1083
- phase handler in the same task invocation.
1533
+ 5. Checkpoint changed, including progress within the same phase: refresh the
1534
+ registry snapshot and either hand over (section 5.4) or invoke the phase
1535
+ handler in the same task invocation.
1084
1536
  6. Checkpoint unchanged: write terminal `faulted` because no durable progress
1085
1537
  was made.
1086
1538
 
1539
+ An abort invocation runs its abort handler once. After it settles, a step
1540
+ applies rules 1, 2, and 4; a handler that returns without a terminal outcome
1541
+ faults the task. When Storage rejects a step's fault or handover write, the
1542
+ invocation still ends; the task stays `running` and the next reservation runs it
1543
+ again.
1544
+
1087
1545
  On open, running-task reconciliation changes surviving `running` tasks back to
1088
- `pending`, preserving their checkpoint and abort mark. Task migration then runs
1089
- before dispatch. One callback handles every supported older version; newer or
1090
- unmigratable live tasks become orphaned.
1546
+ `pending`, preserving their checkpoint and abort mark. Task migration runs at
1547
+ reservation, atomically with `pending -> running`. One callback handles every
1548
+ supported older version. A task whose definition is missing, older than the
1549
+ stored version, or fails migration stays `pending` and blocked until a fitting
1550
+ definition is registered or the task is aborted (section 5.4).
1091
1551
  `close()` marks the runtime closing, seals admission and reservation, signals
1092
1552
  invocations, and stops watches. Outside the Session line it settles admitted
1093
- commits and joins invocations plus in-flight watch callbacks before closing
1094
- storage. Later runtime commits reject, and close writes no task outcome. Closing
1553
+ commits and joins invocations before closing storage. Already-running watch callbacks remain
1554
+ caller-owned. Later runtime commits reject, and close writes no task outcome. Closing
1095
1555
  starts no fresh phase or abort invocation. It does not set abort marks,
1096
1556
  terminalize tasks, retire task documents, or publish document retirement. The
1097
1557
  hosting layer withdraws services and
@@ -1130,12 +1590,13 @@ A terminal transition atomically:
1130
1590
  1. Writes the terminal task record.
1131
1591
  2. Appends any result entries.
1132
1592
  3. Retires all documents scoped to that task.
1133
- 4. Resolves any inputs settled by the task.
1593
+ 4. Resolves any submissions settled by the task.
1134
1594
 
1135
1595
  The execution checkpoint and memos disappear from the terminal representation.
1136
1596
  Terminal records remain queryable for dependencies, waiters, inspection, and
1137
1597
  reopen. A normal run becomes eligible when every `after` task is terminal. An abort
1138
- mark bypasses dependencies so pending work can always reach its abort handler.
1598
+ mark bypasses dependencies so pending work can always reach its abort handler,
1599
+ or its `orphaned` settlement when its definition is unavailable.
1139
1600
 
1140
1601
  ### 5.4 Scheduler, abort, and ownership
1141
1602
 
@@ -1160,59 +1621,143 @@ the durable mark on reopen. Cancelling one caller's `Context` only cancels
1160
1621
  that call or wait; it does not durably abort shared work unless the invoked API
1161
1622
  commits an abort mark.
1162
1623
 
1163
- A task may create owned conversations. Abort and idle operations traverse the
1164
- conversation ownership tree. History parents are irrelevant to this traversal.
1165
- Background tasks do not block ordinary idle waits and survive ordinary
1166
- conversation abort unless explicitly included.
1167
-
1168
- Initial task definitions are registered before open performs live-task migration
1169
- and orphan reconciliation. Dynamic registration begins only after that pass.
1170
- Document migration remains access-driven. Unknown or unmigratable live task kinds
1171
- become terminal `orphaned`;
1172
- affected inputs become unanswered, any matching active turn control is cleared,
1173
- 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
1175
- `faulted` outcome.
1624
+ A task may create owned conversations. Conversations are durable scopes; tasks
1625
+ are the units of live work counted by idle and marked by abort. History parents
1626
+ are irrelevant to ownership traversal.
1627
+
1628
+ Ordinary traversal starts at an explicitly addressed conversation, visits its
1629
+ tasks, and follows conversations owned by each non-background task. It follows
1630
+ owner edges after the owner becomes terminal, but a background owner is a
1631
+ boundary: ordinary traversal skips that task and its complete owned subtree.
1632
+ Direct conversation operations start inside that conversation regardless of its
1633
+ owner. Directly aborting a live background task includes that task and follows
1634
+ its ordinary owned subtree; nested background owners remain boundaries. Full
1635
+ teardown crosses every boundary, marks every live task, and must seal new
1636
+ admission while it gathers the complete indexed ownership subtree.
1637
+
1638
+ `Conversation.abort()` withdraws queued inputs and marks live non-background
1639
+ tasks selected by ordinary traversal. `Conversation.waitForIdle()` waits until
1640
+ that traversal contains no live non-background task. Harness idle performs the
1641
+ same traversal from every ownerless conversation root rather than globally
1642
+ counting tasks, so ordinary work below a background owner does not block it.
1643
+ Explicit `waitForTask()` waits for its referenced task regardless of the task's
1644
+ background flag.
1645
+
1646
+ An abort mark atomically and idempotently cascades to foreground-owned work,
1647
+ including conversations, tasks, and submissions staged in the same transaction.
1648
+ Terminal outcomes `failed`, `faulted`, `orphaned`, and `aborted` record the same
1649
+ durable cancellation intent; `completed` does not. Conversation records and
1650
+ owner edges are never retired with the task. Active invocations are signalled
1651
+ after commit, and a terminal receipt guarantees durable cancellation intent,
1652
+ not descendant quiescence.
1653
+
1654
+ Every run invocation start resolves the task's definition by `TaskRecord.kind`
1655
+ from the scheduler's current registry snapshot:
1176
1656
 
1177
- ## 6. Inputs and inbox
1657
+ ```text
1658
+ definition missing -> stay pending, blocked: missing_task
1659
+ stored version newer than it -> stay pending, blocked: task_too_old
1660
+ stored version older than it -> migrate(input, checkpoint, storedVersion)
1661
+ failure -> stay pending, blocked: migration_failed
1662
+ success -> commit migrated record + running atomically
1663
+ equal version -> commit running
1664
+ ```
1178
1665
 
1179
- Input records back awaitable host handles. The inbox itself is an ordered
1180
- conversation document containing tagged items:
1666
+ An abort invocation resolves the definition the same way. When the definition
1667
+ can take the task, its abort handler runs; otherwise the task is orphaned as
1668
+ described below.
1669
+
1670
+ A failed migration is reported once through `onReport` and retried only after
1671
+ the registry resolves a different definition object for the task's kind.
1672
+
1673
+ The Harness never terminalizes a task merely because registry code is missing or
1674
+ incompatible, neither at open nor later. A blocked task without an abort mark
1675
+ keeps its durable record unchanged, remains live, still blocks ordinary idle
1676
+ waits, and is reconsidered whenever the registry changes. The blocked reason is derived
1677
+ runtime state, not a persisted task status. Document migration remains
1678
+ access-driven.
1679
+
1680
+ Aborting a blocked task cannot run its abort handler, because that code is
1681
+ missing or cannot take the task. The Harness therefore settles it as terminal
1682
+ `orphaned` instead of `aborted`: `aborted` means the task's own abort handler
1683
+ ran and decided the outcome, while `orphaned` means no task code ran, so external
1684
+ effects the task started may remain uncleaned. The orphaned `reason` is the
1685
+ blocked reason (`missing_task`, `task_too_old`, or `migration_failed`). When
1686
+ `abortTask()` finds no active invocation and the current snapshot
1687
+ cannot take the task, the marking commit settles it directly; otherwise the
1688
+ scheduler settles it when it would reserve the abort invocation. Only an abort (direct, by
1689
+ conversation, or by cascade) orphans a task; a missing definition alone never
1690
+ does. The orphaning commit performs the cleanup the task's code cannot: affected
1691
+ input submissions become unanswered with the reason, any matching active run
1692
+ control is cleared, and task-scoped documents retire. No transcript entry is
1693
+ written; the terminal task record and unanswered submissions carry the reason.
1694
+ Faulting a run task performs the same control/submission cleanup with a
1695
+ `faulted` outcome.
1696
+
1697
+ The scheduler knows nothing about runs or task kinds. The Harness, which owns
1698
+ submissions, run control, and the built-in tasks, gives it one hook that the
1699
+ scheduler calls in the same commit for every terminal outcome it writes itself
1700
+ (`faulted` and `orphaned`). The hook ignores tasks whose kind is not a built-in
1701
+ run kind, so it never creates `pi.live` elsewhere. It settles the run when
1702
+ `pi.live.run` names the task (section 8): its inputs become `unanswered` with reason `faulted`
1703
+ (detail: the error message) or the blocked reason, and `run` and `generation`
1704
+ are removed. Outcomes a task commits for itself do their own settlement.
1705
+
1706
+ At every normal phase boundary (section 5.1, rule 5), the step refreshes the
1707
+ invocation's registry snapshot. If the task definition resolved by name is a different object
1708
+ than the one the invocation started with and the new definition can reserve the
1709
+ task (same version, or a higher version with `migrate`), the invocation hands
1710
+ over: the step commits the task back to `pending` with its checkpoint, memos,
1711
+ and abort mark and ends the invocation, and the next reservation starts a fresh invocation under the new
1712
+ definition, applying the reservation rules above. When the definition is missing
1713
+ or cannot reserve the task, the invocation keeps running under its old
1714
+ definition, reports once through `onReport` per resolved definition, and
1715
+ reconsiders at its next boundary. A handler that
1716
+ never settles never hands over.
1717
+
1718
+ ## 6. Submissions and inbox
1719
+
1720
+ Submission records back awaitable host objects. Admission is Harness-internal;
1721
+ run tasks settle the inputs they answer with `tx.settleSubmission()`. The inbox
1722
+ itself is an ordered conversation document containing tagged items:
1181
1723
 
1182
1724
  ```ts
1183
1725
  type InboxItem =
1184
- | { readonly id: Id; readonly mode: "steer" | "followUp"; readonly input: Message }
1185
- | { readonly id: Id; readonly mode: "write"; readonly entry: EntryDraft };
1726
+ | { readonly id: SubmissionId; readonly mode: "steer" | "followUp"; readonly message: Message }
1727
+ | { readonly id: SubmissionId; readonly mode: "write"; readonly entry: EntryDraft };
1186
1728
  ```
1187
1729
 
1188
- 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 !==
1190
- undefined` defines `busy`; get-or-create of the idle document does not. The
1191
- value remains active while generation, tools, and post-tools hand work to one
1192
- another.
1730
+ Run control lives in the built-in live document `pi.live` (section 8). Its
1731
+ optional `run` value names the task currently responsible for the run and its
1732
+ placed input-submission IDs. `run !== undefined` defines `busy`; get-or-create
1733
+ of the idle document does not. The value remains while generation, tools, and
1734
+ post-tools hand work to one another. The ID list is mutable state because a
1735
+ boundary adds placed steering inputs to an active run; every terminal path
1736
+ settles exactly the listed inputs.
1193
1737
 
1194
1738
  Admission and terminal transitions:
1195
1739
 
1196
- | action | input state | other writes |
1740
+ | action | submission state | other writes |
1197
1741
  |---|---|---|
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 |
1202
- | boundary places user item | `placed`, with entry | add ID to current/successor turn |
1742
+ | idle input submission | `placed`, with user entry | create run controller/generation |
1743
+ | busy input submission | `queued` | append steer/follow-up inbox item |
1744
+ | idle write submission | `done`, with entry | append entry; no run |
1745
+ | busy write submission | `queued` | append write inbox item |
1746
+ | boundary places user item | `placed`, with entry | add ID to current/successor run |
1203
1747
  | 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 |
1748
+ | run answers | input `done`, with required answer entry | clear/hand off run controller |
1749
+ | run fails or aborts | input `unanswered`, with reason | clear/hand off run controller |
1206
1750
  | withdraw queued item | `unanswered`, reason `aborted` | remove inbox item |
1207
1751
  | stale item | `unanswered`, reason `stale` | remove inbox item |
1208
1752
 
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.
1753
+ `requestId` deduplicates within one conversation before any write; reusing one
1754
+ for the other submission type rejects. A busy input with `whenBusy: "reject"`
1755
+ writes no record and reports `ConversationBusy`. Before an idle input places its
1756
+ own entry, it runs a final boundary to drain older eligible queued items. A
1757
+ `Submission` waits until `done` or `unanswered`; abort withdraws only a still-
1758
+ queued submission, reports `already_placed` for a placed input, and reports
1759
+ `settled` for any terminal submission. Conversation abort withdraws queued steer/follow-up submissions but keeps writes
1760
+ for later placement.
1216
1761
 
1217
1762
  Boundary selection is deterministic by item ID:
1218
1763
 
@@ -1221,35 +1766,234 @@ Boundary selection is deterministic by item ID:
1221
1766
  | `postTools` | all | first/all by mode | none |
1222
1767
  | `final` | all | first/all by mode | first/all by mode |
1223
1768
 
1224
- A queued self-head write cuts older pending user items: those inputs become
1225
- stale, the write is placed, and the current turn terminates. Other head writes
1769
+ A queued self-head write cuts older pending user items: those submissions become
1770
+ stale, the write is placed, and the current run terminates. Other head writes
1226
1771
  whose target predates the caller's newest known head are stale.
1227
1772
 
1228
1773
  At ordinary `postTools`, generation continues even with no queued trigger;
1229
1774
  selected steer IDs join that continuation. A terminating/handoff post-tools
1230
- boundary uses final behavior instead. At `final`, the current turn's placed
1231
- inputs settle first; selected user IDs start one successor generation. Writes
1775
+ boundary uses final behavior instead. At `final`, the current run's placed
1776
+ input submissions settle first; selected user IDs start one successor generation. Writes
1232
1777
  never trigger generation by themselves. A final boundary without continuation
1233
1778
  or user triggers leaves the conversation idle.
1234
1779
 
1235
1780
  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.
1781
+ preserved. Chord's Astra operation generator must express scattered removals
1782
+ without carrying retained values; IDs are not substituted for positional inbox
1783
+ semantics.
1784
+
1785
+ ## 7. Registry, hooks, tools, and system prompt
1786
+
1787
+ ### 7.1 Registry
1240
1788
 
1241
- ## 7. Hooks, tools, and system sections
1789
+ Extension code reaches the Harness through one application-owned registry. The
1790
+ registry is process-local, may outlive a Harness, and is the only place tools,
1791
+ tool wrappers, hooks, tasks, and the system prompt are registered. Nothing
1792
+ in it is persisted; durable state stays in conversations, entries, tasks, and
1793
+ documents.
1242
1794
 
1243
- ### 7.1 Hooks
1795
+ ```ts
1796
+ type DocumentReader = Pick<Session, "snapshot" | "snapshotAsOf">;
1797
+
1798
+ interface Registration {
1799
+ /** Idempotent; removes exactly the registrations this token covers. */
1800
+ dispose(): void;
1801
+ }
1802
+
1803
+ type ToolWrapper<Tool extends ToolRegistration> = (tool: Tool) => Tool;
1804
+
1805
+ type HookScope = {
1806
+ readonly conversationId: ConversationId;
1807
+ /** Also match conversations owned, transitively, by tasks of this conversation. */
1808
+ readonly subtree?: boolean;
1809
+ };
1810
+
1811
+ type PromptInput<Tool extends ToolRegistration> = {
1812
+ readonly conversationId: ConversationId;
1813
+ /** Active and registered tools in configured order, as offered in this request. */
1814
+ readonly tools: readonly Tool[];
1815
+ /** Sections already in effect after replaying the active transcript. */
1816
+ readonly shown: Readonly<Record<string, string>>;
1817
+ readonly model?: ModelRef;
1818
+ readonly thinkingLevel: ModelThinkingLevel;
1819
+ /** Committed document reads. */
1820
+ readonly read: DocumentReader;
1821
+ };
1822
+
1823
+ type PromptSection<Tool extends ToolRegistration> = {
1824
+ readonly key: string;
1825
+ render(
1826
+ input: PromptInput<Tool>,
1827
+ context: Context,
1828
+ ): string | undefined | Promise<string | undefined>;
1829
+ /** Default true: wrap the text as `<key>\n...\n</key>`. */
1830
+ readonly tag?: boolean;
1831
+ };
1832
+
1833
+ type PromptSectionWrapper<Tool extends ToolRegistration> = (
1834
+ section: PromptSection<Tool>,
1835
+ ) => PromptSection<Tool>;
1836
+
1837
+ /**
1838
+ * Stages the documents every new conversation gets, inside the creating commit, after fork copies and before host
1839
+ * `init`. Table reads throw; a fork (`conversation.parent`) already holds its copied documents. A throw fails the
1840
+ * creation.
1841
+ */
1842
+ type ConversationSetup = (
1843
+ tx: Tx,
1844
+ conversation: ConversationRecord,
1845
+ registry: RegistrySnapshot<ToolRegistration>,
1846
+ ) => void | Promise<void>;
1847
+
1848
+ type RegistryFailure = {
1849
+ readonly kind: "tool" | "section";
1850
+ /** Tool name or section key. */
1851
+ readonly name: string;
1852
+ readonly error: unknown;
1853
+ };
1854
+
1855
+ interface RegistryReader<Tool extends ToolRegistration = ToolRegistration> {
1856
+ /** Immutable view of the whole current registry. */
1857
+ snapshot(): RegistrySnapshot<Tool>;
1858
+ /** Called synchronously after every publication; wakes the scheduler to reconsider blocked tasks. */
1859
+ subscribe(listener: () => void): () => void;
1860
+ }
1861
+
1862
+ interface RegistrySnapshot<Tool extends ToolRegistration> {
1863
+ /** Composed tools in registry order; a tool whose wrapper failed is absent. */
1864
+ tools(): readonly Tool[];
1865
+ tool(name: string): Tool | undefined;
1866
+ /** Base tool names in registry order, including tools whose wrappers fail. */
1867
+ toolNames(): readonly string[];
1868
+ task(name: string): AnyTask | undefined;
1869
+ /** Hooks registered for tasks with `task`'s name, in registry order. */
1870
+ hooks<K extends AnyTask>(task: K): readonly {
1871
+ readonly handlers: Partial<HooksOf<K>>;
1872
+ readonly scope?: HookScope;
1873
+ }[];
1874
+ /** Composed sections in registry order; a section whose wrapper failed is absent. */
1875
+ sections(): readonly PromptSection<Tool>[];
1876
+ /** Wrapper failures of this state; the Harness reports them where it uses them. */
1877
+ failures(): readonly RegistryFailure[];
1878
+ /** Conversation setups in registry order, the built-in `pi` setup first. */
1879
+ conversationSetups(): readonly { readonly key: string; readonly setup: ConversationSetup }[];
1880
+ }
1881
+
1882
+ interface Registry<Tool extends ToolRegistration = ToolRegistration> extends RegistryReader<Tool> {
1883
+ readonly tools: {
1884
+ add(tool: Tool): Registration;
1885
+ wrap(name: string, key: string, wrapper: ToolWrapper<Tool>): Registration;
1886
+ list(): readonly Tool[];
1887
+ };
1888
+ readonly hooks: {
1889
+ add<K extends AnyTask>(
1890
+ task: K,
1891
+ handlers: Partial<HooksOf<K>>,
1892
+ options?: { readonly scope?: HookScope; readonly key?: string },
1893
+ ): Registration;
1894
+ };
1895
+ readonly tasks: {
1896
+ add(task: AnyTask): Registration;
1897
+ list(): readonly AnyTask[];
1898
+ };
1899
+ readonly conversations: {
1900
+ setup(key: string, setup: ConversationSetup): Registration;
1901
+ };
1902
+ readonly systemPrompt: {
1903
+ section(
1904
+ key: string,
1905
+ render: PromptSection<Tool>["render"],
1906
+ options?: { readonly tag?: boolean },
1907
+ ): Registration;
1908
+ wrap(key: string, wrapperKey: string, wrapper: PromptSectionWrapper<Tool>): Registration;
1909
+ sections(): readonly PromptSection<Tool>[];
1910
+ };
1911
+ batch(register: () => void): Registration;
1912
+ }
1913
+
1914
+ function createRegistry<Tool extends ToolRegistration = ToolRegistration>(): Registry<Tool>;
1915
+ ```
1916
+
1917
+ The `Tool` parameter lets applications attach metadata such as prompt snippets to
1918
+ their tools; the Harness only relies on `ToolRegistration`. Pi-ai declarations
1919
+ derived from a registered tool keep only pi-ai `Tool` fields (`toToolDeclaration`),
1920
+ so application metadata never enters the transcript.
1921
+
1922
+ Registration rules:
1923
+
1924
+ - `createRegistry()` starts with the built-in task definitions of section 8 and
1925
+ the built-in `pi` conversation setup registered first. They have no
1926
+ `Registration`, so they cannot be disposed, and the duplicate rule rejects
1927
+ another task with a built-in name or another `pi` setup. `tasks.list()`
1928
+ includes them. Hooks register against built-in tasks like any other task.
1929
+ Setup keys are unique and ordered like section keys.
1930
+ - Tool names, section keys, and task names are unique among published
1931
+ registrations. Tool wrapper keys are unique per tool name, section wrapper
1932
+ keys per section key, and hook keys per task name. Duplicates reject.
1933
+ Section keys match `^[a-z][a-z0-9_-]*$`.
1934
+ - Keyed slots are ordered by the position at which their key was first
1935
+ registered. The registry remembers that position forever, so re-registering an
1936
+ existing key, as a reload does, keeps its position; a new key appends. Hooks
1937
+ without a key append.
1938
+ - Registered objects are immutable and wrappers are pure: a wrapper returns a
1939
+ new object and never mutates its input.
1940
+ - Every call outside `batch()` publishes immediately. Publication is synchronous,
1941
+ invokes no extension callback, and notifies subscribers.
1942
+
1943
+ `batch(register)` stages every registration and disposal made while `register`
1944
+ runs and publishes once when it returns. It validates only the final staged
1945
+ state, so order inside the block does not matter: disposing an old tool and
1946
+ adding its replacement is a gapless reload. If `register` throws, returns a
1947
+ thenable, or the final state is invalid, nothing is published, staged disposals
1948
+ are rolled back, and `batch()` throws. Only registrations made synchronously
1949
+ inside `register` belong to the batch; a registration made later by a detached
1950
+ continuation is an ordinary immediate registration. Calling `batch()` inside a
1951
+ batch is a programming error and throws. The returned `Registration` covers every
1952
+ registration added in the batch that was not disposed inside it.
1953
+
1954
+ A snapshot is a plain immutable value; nothing is released. The scheduler takes
1955
+ one per phase-handler invocation and passes it through the runtime; handlers and
1956
+ hooks read that snapshot and never take their own. At every normal phase boundary
1957
+ the scheduler takes a fresh one (section 5.4). A tool task keeps the composed tool
1958
+ it pinned until execution settles. Different phases may observe different
1959
+ registry states; nothing requires one run to see a single registry state across
1960
+ its generation, tool, and post-tools tasks. Host operations, such as the create
1961
+ conveniences, take one snapshot inside their commit.
1962
+
1963
+ ```ts
1964
+ // ToolTask is the built-in tool task's token; its hooks are listed in section 7.2.
1965
+ const registration = registry.batch(() => {
1966
+ registry.tools.add(grep);
1967
+ registry.hooks.add(ToolTask, { beforeTool: audit }, { key: "audit" });
1968
+ });
1969
+ // later, reload: publish the replacement at once
1970
+ registry.batch(() => {
1971
+ registration.dispose();
1972
+ registry.tools.add(grepV2);
1973
+ registry.hooks.add(ToolTask, { beforeTool: audit2 }, { key: "audit" });
1974
+ });
1975
+ ```
1976
+
1977
+ The registry does not track which running work still uses a disposed
1978
+ registration. Work that already started keeps using the code it took, so an
1979
+ extension that frees resources right after `dispose()` can make a still-running
1980
+ call fail with an ordinary error result. Extensions that need graceful disposal
1981
+ manage their resources' lifetime themselves, for example by reference counting.
1982
+
1983
+ ### 7.2 Hooks
1244
1984
 
1245
1985
  A hook is a typed question asked by a task before it commits a decision. Hooks
1246
- are declared by task kind and registered in registration order Session-wide or
1247
- for a conversation and its owned subtree. They run off the line; a crash before
1248
- the consuming commit may rerun them. Abort errors always propagate.
1986
+ are declared by task definition and registered through `registry.hooks.add()`
1987
+ Session-wide or, with `scope`, for one conversation and optionally its owned
1988
+ subtree. Subtree matching follows conversation ownership, not history parents.
1989
+ Registration is typed by the task token, but dispatch matches the task's name,
1990
+ so hooks survive a reload of their task; keeping hook signatures compatible
1991
+ across task versions is the task author's responsibility. Hooks run in registry
1992
+ order off the line; a crash before the consuming commit may rerun them. Abort
1993
+ errors always propagate.
1249
1994
 
1250
1995
  | hook | composition | ordinary throw |
1251
1996
  |---|---|---|
1252
- | system instructions | all; draft changes compose; last tool override wins | roll back that handler, report, continue |
1253
1997
  | `beforeRequest` | replacement chain | report, continue |
1254
1998
  | `afterResponse` | all observers | report, continue |
1255
1999
  | `onYield` | first continuation wins | report, continue |
@@ -1261,7 +2005,7 @@ the consuming commit may rerun them. Abort errors always propagate.
1261
2005
  Hooks use task memos for durable first-writer-wins decisions. There is no public
1262
2006
  semantic event channel; current UI status is document state.
1263
2007
 
1264
- ### 7.2 Tools
2008
+ ### 7.3 Tools
1265
2009
 
1266
2010
  ```ts
1267
2011
  type ToolControl = {
@@ -1277,20 +2021,20 @@ type ToolExecutionResult = {
1277
2021
  readonly control?: ToolControl;
1278
2022
  };
1279
2023
 
1280
- interface OwnedConversation {
1281
- readonly id: Id;
1282
- send(input: SendInput, context: Context): Promise<InputHandle>;
2024
+ interface ConversationHandle {
2025
+ readonly id: ConversationId;
2026
+ submit(submission: InputSubmissionDraft, context: Context): Promise<Submission>;
1283
2027
  abort(context: Context): Promise<void>;
1284
2028
  waitForIdle(context: Context): Promise<void>;
1285
2029
  }
1286
2030
 
1287
- interface ToolExecutionApi extends DocumentObserver {
1288
- readonly taskId: Id;
1289
- readonly conversationId: Id;
2031
+ interface ToolExecutionApi extends DocumentObserver, DocumentReader {
2032
+ readonly taskId: TaskId;
2033
+ readonly conversationId: ConversationId;
1290
2034
  readonly callId: string;
1291
- stream(chunk: string | Uint8Array): void;
2035
+ output(chunk: string | Uint8Array): void;
2036
+ details(value: JsonValue, context: Context): Promise<void>;
1292
2037
  commit<T>(change: (tx: Tx) => T | Promise<T>, context: Context): Promise<T>;
1293
- progress(value: JsonObject, context: Context): Promise<void>;
1294
2038
  memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
1295
2039
  memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
1296
2040
  createTask<I, S extends { phase: string }, R, H extends object>(
@@ -1298,18 +2042,15 @@ interface ToolExecutionApi extends DocumentObserver {
1298
2042
  input: I,
1299
2043
  options: Omit<TaskOptions, "conversationId">,
1300
2044
  context: Context,
1301
- ): Promise<TaskRef<R>>;
1302
- getTask<R>(ref: TaskRef<R>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, R> | undefined>;
1303
- waitForTask<R>(ref: TaskRef<R>, context: Context): Promise<SettledTask<R>>;
1304
- createConversation(
1305
- spec: Omit<ConversationSpec, "parent"> & { readonly inherit?: boolean },
1306
- context: Context,
1307
- ): Promise<OwnedConversation>;
2045
+ ): Promise<TaskId<R>>;
2046
+ getTask<R>(id: TaskId<R>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, R> | undefined>;
2047
+ waitForTask<R>(id: TaskId<R>, context: Context): Promise<SettledTask<R>>;
2048
+ conversation(id: ConversationId, context: Context): Promise<ConversationHandle | undefined>;
1308
2049
  }
1309
2050
 
1310
2051
  type ToolRegistration = Tool & {
1311
2052
  readonly replay?: "safe" | "unsafe";
1312
- readonly output?: {
2053
+ readonly outputLimits?: {
1313
2054
  readonly maxBytes?: number;
1314
2055
  readonly maxLines?: number;
1315
2056
  readonly retain?: "head" | "tail";
@@ -1322,57 +2063,138 @@ type ToolRegistration = Tool & {
1322
2063
  };
1323
2064
  ```
1324
2065
 
1325
- Omitted `replay` is `unsafe`. Omitted output bounds are 64 KiB, 200 lines, and
1326
- `retain: "head"`. `stream()` synchronously accepts UTF-8 output into that
1327
- invocation-owned bounded buffer and throws after invocation end. Throttled
1328
- commits publish the retained output and dropped byte/line counts in the tool
1329
- presentation document. If `execute()` omits `content`, the final retained stream
1330
- becomes one text content item; no stream becomes an empty content list. Explicit
2066
+ Omitted `replay` is `unsafe`. Omitted `outputLimits` are 50 KiB, 2,000 lines,
2067
+ and `retain: "head"`.
2068
+
2069
+ A running tool reports two things to the UI, mirroring the two halves of its
2070
+ final result:
2071
+
2072
+ - `output(chunk)` appends running text output, like stdout. If `execute()` omits
2073
+ `content`, the final retained output becomes one text content item; no output
2074
+ becomes an empty content list.
2075
+ - `details(value)` replaces the running details with a complete JSON value; it
2076
+ does not merge keys. If `execute()` omits `details`, the last value becomes the
2077
+ final `details`, so a renderer handles one details shape from the first
2078
+ update through the final result.
2079
+
2080
+ Neither is sent to the model while the tool runs. `output()` synchronously
2081
+ accepts UTF-8 output into that invocation-owned bounded buffer and throws after
2082
+ invocation end. Throttled commits publish the retained output, dropped
2083
+ byte/line counts, and the current details in the tool presentation document. Explicit
1331
2084
  text in explicit result content is bounded by the same limits before transcript
1332
2085
  persistence; non-text content is retained as declared by its pi-ai type.
1333
2086
 
1334
- `progress(value)` replaces the invocation's complete JSON `progress` payload; it
1335
- does not merge keys. Its promise resolves after the corresponding or coalesced
1336
- document commit. During normal settlement, accepted output updates drain before
2087
+ `output()` never spills complete output to a file because spilling requires a
2088
+ filesystem, which may be remote or unavailable. A tool that must preserve
2089
+ complete output spills through the `ExecutionEnv` or `FileSystem` it was given,
2090
+ such as shell execution with spill capture, and reports the resulting path in its
2091
+ details.
2092
+
2093
+ The `details()` promise resolves after the corresponding or coalesced document
2094
+ commit. During normal settlement, accepted output updates drain before
1337
2095
  the tool-result entry and terminal task record commit. Abort and close obey
1338
2096
  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.
1342
-
1343
- Tools are dynamically registered declarations with name, description, JSON
1344
- schema, replay policy, and execute function. A tool call is accepted only if it
1345
- was offered in the request's effective system/tool history. Arguments are
1346
- validated before and after `beforeTool` hooks.
2097
+ discarded, while admitted commits settle. Cancellation, callback, tracker
2098
+ preparation, and checkpoint failures occur before Storage admission and do not
2099
+ poison the Session. An uncertain Storage failure follows the fatal Session rule.
2100
+
2101
+ Tools are registry entries (section 7.1) with name, description, JSON schema,
2102
+ replay policy, and execute function. `registry.tools.wrap(name, key, wrapper)`
2103
+ decorates a tool without replacing it. Each snapshot composes the current base
2104
+ tool with its wrappers in registry order; a wrapper never captures a base, so
2105
+ reloading the base keeps its wrappers. A wrapper that throws or returns a tool
2106
+ with a different name makes that tool absent from the snapshot (fail closed:
2107
+ not offered, and calls produce `tool_unavailable`) and is reported. A wrapper
2108
+ without a base contributes nothing. The composite supplies the declaration,
2109
+ argument validation, replay policy, and execution.
2110
+
2111
+ A tool call is accepted only if it was offered in the request's effective
2112
+ system/tool history. A call to a tool that is not offered or has no registered
2113
+ implementation produces the `tool_unavailable` error result described in section
2114
+ 2.2 instead of failing the run. The tool task resolves the composed tool once
2115
+ from its snapshot before argument validation and pins it until execution
2116
+ settles, even across later snapshot refreshes. Arguments must satisfy both the offered
2117
+ declaration and the pinned implementation's schema; they are validated before
2118
+ and after `beforeTool` hooks.
1347
2119
 
1348
2120
  After hooks and validation, the tool task durably records the final call and
1349
2121
  resolved replay policy before execution. Recovery does not rerun `beforeTool`
1350
- and does not let a changed registry declaration alter that stored policy.
2122
+ and passes the same stored arguments to `execute()`. A replay-safe tool may
2123
+ reconstruct a submission from those arguments when that transformation is pure.
2124
+ Random values, timestamps, mutable document/configuration reads, or other derived
2125
+ inputs that must remain stable are first captured in a durable memo, checkpoint,
2126
+ or task input. A background supervisor receives the final submission draft in
2127
+ its own durable input so it can finish independently. Recovery does not let a
2128
+ changed registry declaration alter the stored replay policy.
2129
+
2130
+ A tool-acquired conversation handle accepts only input submissions; tools use
2131
+ ordinary transaction writes for passive entries.
1351
2132
 
1352
2133
  A tool executes in a durable task. It may:
1353
2134
 
1354
- - write bounded progress/output to a presentation or task-scoped document;
2135
+ - publish bounded running output and details to its presentation document;
1355
2136
  - commit memos;
1356
2137
  - create and wait for tasks;
1357
- - create owned conversations;
2138
+ - atomically create or fork explicitly owned conversations through `commit()`;
1358
2139
  - observe documents for which it has a token/reference;
1359
2140
  - mutate authorized documents through `commit()`;
1360
2141
  - return bounded model content and separate diagnostic details.
1361
2142
 
1362
- Tool operations use the invoking task's admission and
1363
- invocation-lifetime gates. Task creation defaults to that task's conversation.
1364
- Trusted document access follows sections 3.3 and 9.2; there is no additional
1365
- document subtree authorization layer. `createConversation()` records the
1366
- invoking task as owner and returns an invocation-bound handle. `inherit` defaults
1367
- to `false`. When true, creation atomically selects the invoking conversation's
1368
- newest committed visible entry as `parent.at`; if no entry exists, it creates no
1369
- history parent. Conversation documents then apply section 3.7 at that entry, and
1370
- explicit model/section seeds override their inherited built-in values. With no
1371
- 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
1374
- durable after those methods reject. Invocation-owned document watches stop when
1375
- the invocation ends.
2143
+ Tool operations use the invoking task's admission and invocation-lifetime gates.
2144
+ Task creation defaults to that task's conversation. Trusted document and
2145
+ conversation access follows sections 3.3 and 9.2; there is no additional subtree
2146
+ authorization layer. Possession of a Session-global typed ID is sufficient in
2147
+ trusted code.
2148
+
2149
+ Tools have no conversation-creation convenience method. They create or fork a
2150
+ conversation inside `commit()`, state its ownership explicitly, atomically stage
2151
+ any related document/task writes, and receive only the inert record. After that
2152
+ commit settles, `conversation(id)` acquires an invocation-bound operational
2153
+ handle for submission, explicit abort, and idle waits without changing
2154
+ ownership. A commit callback must not call `conversation()` or use a previously
2155
+ acquired handle: nested Session operations and external effects are forbidden
2156
+ while the mutation line is held. A handle's operations reject after the
2157
+ invocation ends. A `Submission` returned by its `submit()` is invocation-bound in
2158
+ the same way; the admitted submission remains durable after those methods reject.
2159
+ Invocation-owned document watches stop when the invocation ends.
2160
+
2161
+ A foreground subagent conversation is explicitly owned by its tool task. One
2162
+ transaction creates or forks the child and records its durable registry mapping;
2163
+ after settlement the tool reacquires the child, submits with the registered
2164
+ request ID, and waits for that submission's result. Aborting or abnormally
2165
+ terminalizing the tool task cascades through the owned scope.
2166
+
2167
+ A background subagent is provisioned in one transaction. After deduplicating by
2168
+ its durable registry key, the tool stages a background supervisor task `B`, a
2169
+ child conversation `C` explicitly owned by `B`, and the registry mapping from the
2170
+ semantic name to `C` plus its stable request ID. `B` may be staged earlier in the
2171
+ same transaction and used immediately as `C`'s owner. Its durable input contains
2172
+ the exact submission draft and the registry location/key; it need not contain
2173
+ `C`'s not-yet-created ID. Once scheduled, `B` resolves `C` from the mapping,
2174
+ verifies the immutable owner edge, and performs normal `Conversation.submit()`.
2175
+ A crash before admission makes `B` retry; a crash after admission returns the
2176
+ existing request-ID-deduplicated `Submission`. The initiating tool may race the
2177
+ same submit for lower latency and wait only for the durable admission receipt.
2178
+ The supervisor may complete after setup: its terminal record retains
2179
+ `background`, so ancestor ordinary abort and idle traversal continue to stop at
2180
+ its owned scope.
2181
+
2182
+ Applications may maintain a conversation document mapping semantic subagent
2183
+ names to durable conversation IDs and application-minted submission request IDs.
2184
+ Such a live registry uses `fork: "initial"` so children do not inherit the
2185
+ parent's agent list and its update is not a selected fork source. Conversation
2186
+ creation/forking, supervisor creation when applicable, and the registry mapping
2187
+ commit atomically. The supervisor input, tool's durable final arguments, or a
2188
+ durable checkpoint must retain enough information to reconstruct the exact
2189
+ submission draft; the registry itself need not duplicate that payload. Initial
2190
+ or later input then uses the registered stable request ID with the full
2191
+ `Conversation.submit()` state machine. A crash before admission leaves a durable
2192
+ request to submit; a crash after admission retries the same request and receives
2193
+ the existing `Submission`. A read-only lookup by conversation/request ID can
2194
+ report absence or return the durable queued/placed/done/unanswered receipt.
2195
+ Submission admission therefore needs no private transaction shortcut and does
2196
+ not move onto `Tx`. Kernel ownership indexes independently drive abort and idle
2197
+ traversal.
1376
2198
 
1377
2199
  A tool result may request `addTools`, `terminate`, or `handoff`. Post-tools
1378
2200
  applies added tool names to configured loadout, uses a final boundary for
@@ -1380,27 +2202,19 @@ terminate/handoff, and writes a headed handoff entry when requested.
1380
2202
 
1381
2203
  On reopen, a tool reruns only when both its stored intent policy and the current
1382
2204
  registered declaration say `safe`. A current `unsafe` declaration may veto a
1383
- stored-safe replay; a current-safe declaration never upgrades stored unsafe.
1384
- Every other orphaned effect produces an interrupted result containing the
2205
+ stored-safe replay; a current-safe declaration never upgrades stored unsafe. A
2206
+ tool with no current registration is treated as `unsafe`. Every other orphaned
2207
+ effect produces an interrupted result containing the
1385
2208
  durable partial output. Completed, failed, and aborted
1386
2209
  tool terminal outcomes retain their tool-result entry ID for post-tools.
1387
2210
 
1388
- ### 7.3 System sections and dynamic tools
1389
-
1390
- ```ts
1391
- interface SystemSection<T extends JsonValue = JsonValue> {
1392
- readonly key: string;
1393
- render(value: T): string;
1394
- }
1395
-
1396
- function defineSystemSection<T extends JsonValue>(definition: {
1397
- readonly key: string;
1398
- render(value: T): string;
1399
- }): SystemSection<T>;
1400
- ```
2211
+ ### 7.4 System prompt and dynamic tools
1401
2212
 
1402
- Pico stores prompt and tool changes directly as PR #9548 `SystemMessage` values
1403
- at their transcript positions:
2213
+ Pico has no durable prompt sections. The registry's system prompt slot produces
2214
+ the desired sections for each request; the transcript's `pi.system` entries are
2215
+ the only durable record of what the model saw. Pico stores prompt and tool
2216
+ changes directly as PR #9548 `SystemMessage` values at their transcript
2217
+ positions, always with empty `content`:
1404
2218
 
1405
2219
  ```ts
1406
2220
  type SystemEntry = EntryRecord & {
@@ -1410,8 +2224,8 @@ type SystemEntry = EntryRecord & {
1410
2224
 
1411
2225
  const baseline: SystemMessage = {
1412
2226
  role: "system",
1413
- content: basePrompt,
1414
- sections: { persona: renderedPersona, cwd: renderedCwd },
2227
+ content: "",
2228
+ sections: { preamble: renderedPreamble, cwd: renderedCwd },
1415
2229
  toolsAdded: allEffectiveTools,
1416
2230
  timestamp: now,
1417
2231
  };
@@ -1426,10 +2240,51 @@ const delta: SystemMessage = {
1426
2240
  };
1427
2241
  ```
1428
2242
 
1429
- System sections are registered by stable, non-integer-like key. Generation
1430
- prepares the desired rendered section values and effective tool roster, compares
1431
- them with the state obtained by replaying the active transcript, and appends a
1432
- positional `pi.system` baseline or delta.
2243
+ Generation preparation takes these steps against its phase snapshot:
2244
+
2245
+ 1. Read the committed configuration and replay the active transcript's system
2246
+ messages into the shown sections and offered tools.
2247
+ 2. Compute the desired tools: active names that the snapshot resolves, in
2248
+ configured order, as composed by their wrappers.
2249
+ 3. Render every registered section, in registry order, with `PromptInput`: the
2250
+ conversation, those tools, the shown sections, the configured model and
2251
+ thinking level, and a `DocumentReader` for committed documents (the task
2252
+ runtime). The results are the desired sections.
2253
+ 4. Compare desired sections and tool declarations with the replayed state and
2254
+ append one positional `pi.system` entry when they differ.
2255
+
2256
+ Preparation does not recheck the transcript before appending: only the Harness
2257
+ writes to a busy conversation, through submissions, run tasks, and boundaries,
2258
+ so the transcript it read is still current (section 12).
2259
+
2260
+ Model and thinking level are request options, not prompt state.
2261
+
2262
+ Registered sections are the only source of prompt text; there is no separate
2263
+ builder. `systemPrompt.section(key, render, { tag })` adds a section, and
2264
+ `systemPrompt.wrap(key, wrapperKey, wrapper)` decorates one, composed per
2265
+ snapshot like tool wrappers. Sections render in registry order: the position at
2266
+ which each key was first registered. A section whose `render` returns
2267
+ `undefined` is omitted, which is how a section varies by conversation. With
2268
+ `tag` omitted or true, text is wrapped as `<key>\n...\n</key>`. A section that
2269
+ throws keeps its shown text, if any, and is reported; the request is still sent.
2270
+ Errors thrown after the generation invocation is cancelled propagate; an abort
2271
+ error of the section's own, such as its fetch timing out, is an ordinary failure. With no registered sections, the desired section set is
2272
+ empty.
2273
+
2274
+ A minimal prompt is one untagged section:
2275
+
2276
+ ```ts
2277
+ registry.systemPrompt.section("preamble", () => "You are a helpful assistant.", { tag: false });
2278
+ ```
2279
+
2280
+ Sections read per-conversation data through `input.read`. For example, a
2281
+ coding agent keeps its own conversation document with the agent kind, preamble,
2282
+ and working directory; its preamble and cwd sections render from that document,
2283
+ and its AGENTS.md and skills sections return `undefined` for subagent
2284
+ conversations.
2285
+ Renderers must be deterministic for equal inputs: any change in rendered text,
2286
+ such as an embedded timestamp, appends a system delta and invalidates provider
2287
+ prompt caches.
1433
2288
 
1434
2289
  Replay applies messages in transcript order. Non-empty `content` appends
1435
2290
  instructions. A section string adds or replaces that name without moving an
@@ -1437,74 +2292,64 @@ existing section; `null` removes it, and a later re-addition appends it to the
1437
2292
  ordered section map. Within one message, tool removals happen before additions,
1438
2293
  so a same-name replacement gets the new declaration and position.
1439
2294
 
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
1443
- without changing order, it emits the minimal patch. If effective and desired
1444
- section order differ, one commit appends two `pi.system` entries: the first
1445
- removes every effective section with `null`, and the second re-adds every desired
1446
- section in desired order. This makes order-only changes and deletion/re-addition
1447
- between requests replay exactly; merely restating equal values is insufficient.
2295
+ Preparation compares both values and order. If values can be patched without
2296
+ changing order, it emits the minimal patch. If shown and desired section order
2297
+ differ, one commit appends two `pi.system` entries: the first removes every
2298
+ shown section with `null`, and the second re-adds every desired section in
2299
+ desired order. This makes order-only changes and deletion/re-addition between
2300
+ requests replay exactly; merely restating equal values is insufficient.
1448
2301
 
1449
2302
  A PR #9548 `SystemMessage` is always a patch, not a reset: it cannot remove
1450
2303
  previous `content` or restore section order merely by restating current values.
1451
- Therefore, when a head removes the previous request-visible baseline, the new
2304
+ Therefore, when a head removes the previous request-visible baseline, which is
2305
+ the case when the active context has a head marker and no `pi.system` entry
2306
+ follows that marker, the new
1452
2307
  `pi.system` entry adds `ContextEdit` omissions for every earlier `pi.system`
1453
2308
  entry still retained after the cut. Its own message is then a complete baseline
1454
- containing the base `content`, every desired section in order, and every effective
1455
- tool declaration. Model-context replay sees the new baseline instead of the
1456
- omitted retained deltas. Head rebaselining takes precedence over ordinary
1457
- order/value patching. Without a head cut, an order mismatch uses the two-entry
1458
- remove/re-add sequence above; only when order already matches does preparation
1459
- emit the minimal changed values, `null` removals, and tool additions/removals.
1460
- Same-name tool replacements remove before adding. Registry or document changes
1461
- while preparation hooks run cause preparation to retry against a new snapshot.
1462
-
1463
- Conversation creation may seed section values or explicit removals. Preparation
1464
- uses a mutable section draft with get/set/delete/wrap; each throwing hook loses
1465
- only its own draft changes. The rendered strings stored in historical
1466
- `SystemMessage.sections` remain authoritative even if the current renderer
1467
- changes. Pi-ai decides whether to send the messages positionally to a capable
1468
- provider or fold them into one leading system message; Pico does not rewrite its
1469
- stored transcript for provider compatibility.
1470
-
1471
- ### 7.4 Host extension reload
1472
-
1473
- A host extension generation is the Session-side code implementing its task kinds,
1474
- hooks, tools, sections, and document definitions. Registry APIs may
1475
- change registered declarations during normal product operation, but they do not
1476
- make replacement of that implementation code safe while its callbacks are
1477
- running. Document definitions are passed explicitly to typed access rather than
1478
- registered.
1479
-
1480
- In v1, changing host extension code is a Harness generation boundary:
1481
-
1482
- 1. Stop new admission and task reservation.
1483
- 2. Close the Harness, signalling and joining its active task, tool, and hook
1484
- invocations plus in-flight watch callbacks without writing abort marks or
1485
- terminal outcomes.
1486
- 3. Dispose the old facets and registrations.
1487
- 4. Construct a new Harness over the same storage and new document tokens.
1488
- 5. Register the complete task definition set before open performs live-task
1489
- migration and orphan reconciliation. Ordinary documents migrate on later
1490
- typed access.
1491
- 6. Resume scheduling from the durable checkpoints.
1492
-
1493
- A durable task does not need to become terminal before this restart; only its
1494
- current invocation must settle. A task definition must increase its version when
1495
- the meaning of persisted input or checkpoint state changes and migrate supported
1496
- older state. Uncommitted hook work may rerun under the new generation; committed
1497
- memos remain part of the live task.
1498
-
1499
- If old extension code ignores cancellation and never settles, graceful in-process
1500
- reload cannot complete. The host must terminate that isolated worker/process
1501
- before opening the Session under the new generation. Old and new generations
1502
- must never own the same Session concurrently.
1503
-
1504
- Generation-pinned registries that drain old callbacks while routing new work to
1505
- new code, plus explicit compatible task takeover, are possible future work. Safe
1506
- forced takeover of arbitrary non-cooperative JavaScript requires worker/process
1507
- isolation and is not a v1 promise.
2309
+ containing every desired section in order and every effective tool declaration.
2310
+ Model-context replay sees the new baseline instead of the omitted retained
2311
+ deltas. Preparation writes this baseline even when it restates the replayed
2312
+ values, so every later preparation finds a `pi.system` entry after the marker.
2313
+ Head rebaselining takes precedence over ordinary order/value patching.
2314
+ Without a head cut, an order mismatch uses the two-entry remove/re-add sequence
2315
+ above; only when order already matches does preparation emit the minimal changed
2316
+ values, `null` removals, and tool additions/removals. A changed tool declaration
2317
+ is removed and re-added in the same message. When the offered tool order differs
2318
+ from the desired order, the message removes every offered tool and re-adds the
2319
+ desired tools in order.
2320
+
2321
+ The rendered strings stored in historical `SystemMessage.sections` remain
2322
+ authoritative even if the current renderer changes. Pi-ai decides whether to
2323
+ send the messages positionally to a capable provider or fold them into one
2324
+ leading system message; Pico does not rewrite its stored transcript for provider
2325
+ compatibility.
2326
+
2327
+ ### 7.5 Extension reload
2328
+
2329
+ Extension code is reloaded in process through the registry. The host publishes
2330
+ the new registrations and disposes the old ones in one `batch()`. The Harness
2331
+ keeps running; no close or reopen is required.
2332
+
2333
+ - New phase invocations and tool tasks use the new registrations immediately;
2334
+ running invocations see them at their next phase boundary.
2335
+ - Work already running keeps its snapshot until its phase handler settles, and a
2336
+ pinned tool keeps its implementation until its execution settles. Replacement
2337
+ never signals or interrupts it. Resources the old code uses are the
2338
+ extension's responsibility (section 7.1).
2339
+ - A task definition replaced by name hands over at its next normal phase boundary
2340
+ (section 5.4). A task definition must increase its version when the meaning
2341
+ of persisted input or checkpoint state changes and migrate supported older
2342
+ state; a failed migration blocks the task rather than terminalizing it.
2343
+ - Keyed registrations re-registered under the same key keep their position, so a
2344
+ reload does not reorder hooks, tools, or sections.
2345
+ - Document definitions are passed explicitly to typed access and need no
2346
+ registration; changed tokens take effect on their next access.
2347
+
2348
+ If old extension code ignores cancellation and never settles, new work already
2349
+ uses the replacement. Harness close still joins every invocation. Safe forced termination of arbitrary
2350
+ non-cooperative JavaScript requires worker/process isolation; that host
2351
+ terminates the process and reopens the Session from durable state. Old and new
2352
+ Harness instances must never own the same Session concurrently.
1508
2353
 
1509
2354
  ## 8. Built-in tasks
1510
2355
 
@@ -1512,10 +2357,10 @@ The initial implementation provides:
1512
2357
 
1513
2358
  | kind | responsibility |
1514
2359
  |---|---|
1515
- | generation | prepare system/loadout, request or poll model, retry, classify response |
1516
- | tool | validate, hook, execute, persist progress, append result |
1517
- | post-tools | wait for tools, apply controls, run boundary, continue generation |
1518
- | collapse | select a transcript range, summarize, append a headed summary |
2360
+ | `pi.generation` | prepare system/loadout, request or poll model, retry, classify response |
2361
+ | `pi.tool` | validate, hook, execute, persist output and details, append result |
2362
+ | `pi.post-tools` | wait for tools, apply controls, run boundary, continue generation |
2363
+ | `pi.collapse` | select a transcript range, summarize, append a headed summary |
1519
2364
 
1520
2365
  Generation uses `HarnessOptions.models` without a Pico-specific model adapter. It
1521
2366
  resolves `models.getModel(ref.provider, ref.modelId)`, builds a pi-ai `Context`
@@ -1536,152 +2381,284 @@ Bounded output records whether content was truncated and any retained file path.
1536
2381
  Compaction changes model context by appending a summary entry with a head. It
1537
2382
  does not delete transcript history.
1538
2383
 
2384
+ ### 8.1 Built-in entries
2385
+
2386
+ Built-in entry kinds carry no `data`; each is exported as an `Entry` token.
2387
+
2388
+ | kind | `model` | written by |
2389
+ |---|---|---|
2390
+ | `pi.user` | `[UserMessage]`, timestamp from the Harness clock at admission or placement | submissions |
2391
+ | `pi.assistant` | `[AssistantMessage]` with any stop reason | generation |
2392
+ | `pi.system` | `[SystemMessage]` with `content: ""` (section 7.4) | generation preparation |
2393
+ | `pi.tool-result` | `[ToolResultMessage]` | tool tasks |
2394
+
2395
+ Every provider result becomes a `pi.assistant` entry: answers, failed attempts
2396
+ with their error text and usage, and converted partials with stop reason
2397
+ `aborted`. Context derivation (section 2.1, rule 9) keeps failed and aborted
2398
+ messages out of later requests, so no separate usage or notice kind exists.
2399
+
2400
+ ### 8.2 Live document
2401
+
2402
+ ```ts
2403
+ type LiveState = {
2404
+ /** Run control (section 6); present exactly while the conversation is busy. */
2405
+ run?: { taskId: TaskId; inputs: SubmissionId[] };
2406
+ /** Presentation of the current generation attempt. */
2407
+ generation?: {
2408
+ attempt: number;
2409
+ /** Committed throttled partial of the in-flight response. */
2410
+ message?: JsonRepresentation<AssistantMessage>;
2411
+ /** Durable backoff before the next attempt. */
2412
+ retry?: { at: number; error: string };
2413
+ /** Provider-side deferred response being polled. */
2414
+ deferred?: { pollAt: number };
2415
+ };
2416
+ };
2417
+ ```
2418
+
2419
+ | field | value |
2420
+ |---|---|
2421
+ | kind | `pi.live` |
2422
+ | version | `1` |
2423
+ | scope/history/fork | conversation, `latest`, `initial` |
2424
+ | `initial()` | `{}` |
2425
+ | checkpoint | complete base whenever `generation` is absent |
2426
+ | view mount | `docs["pi.live"]` |
2427
+ | created | with every Harness conversation (section 2.2) |
2428
+
2429
+ Nothing is in flight at every turn boundary and while idle, so the stored delta
2430
+ chain spans at most one generation, including its retries and deferred polls, or
2431
+ one tool round, and each base is small. Tool progress adds its own condition with the tool task.
2432
+
2433
+ Tool progress and compaction status join this document with the tool and
2434
+ collapse tasks. Partials are normalized to strict JSON before assignment. Every
2435
+ terminal path of a run task removes `run` and `generation` in the commit that
2436
+ settles the run's inputs. `tx.settleSubmission()` stages each input's new status
2437
+ and resolves the transaction's latest candidate submission record, falling back
2438
+ to committed state, during assembly, like task-document validation (section 3.3),
2439
+ so it is not a caller table read and works after the commit's first table write.
2440
+
2441
+ ### 8.3 Generation
2442
+
2443
+ ```ts
2444
+ type GenerationInput = {};
2445
+ type GenerationCheckpoint =
2446
+ | { phase: "prepare"; attempt: number }
2447
+ | {
2448
+ phase: "request";
2449
+ attempt: number;
2450
+ model: ModelRef;
2451
+ thinkingLevel: ModelThinkingLevel;
2452
+ streamOptions: ConversationStreamOptions;
2453
+ /** Newest entry included in the request. */
2454
+ cutoff: EntryId;
2455
+ }
2456
+ | { phase: "retry"; attempt: number; until: number }
2457
+ | { phase: "poll"; attempt: number; model: ModelRef; handle: DeferredHandle; pollAt: number };
2458
+ type GenerationResult = { entryId: EntryId };
2459
+ ```
2460
+
2461
+ `pi.generation` is version 1 and starts at `{ phase: "prepare", attempt: 1 }`.
2462
+ The run's inputs live in `pi.live.run`, not in the task input.
2463
+
2464
+ - `prepare` runs section 7.4 against the committed configuration. When no model
2465
+ is configured or `models.getModel()` does not know it, the task fails with
2466
+ `no_model`. Otherwise one commit appends the planned `pi.system` entries and
2467
+ moves to `request` with the new tail as `cutoff` and the configuration's model,
2468
+ thinking level, and stream options.
2469
+ - `request` and `poll` resolve the checkpoint's model through
2470
+ `models.getModel()`; an unknown model fails the task with `no_model`, like
2471
+ `prepare`. `request` converts a leftover partial (below) before it resolves
2472
+ the model.
2473
+ - `request` first converts a committed partial left in `pi.live` by an
2474
+ interrupted attempt into an aborted `pi.assistant` entry. It then streams the
2475
+ model context through `cutoff` with the invocation signal, the thinking level
2476
+ as `reasoning` (omitted for `off`), and the pinned `streamOptions`,
2477
+ committing throttled partials. Recovery resends the same messages with the
2478
+ same pinned model, thinking level, and stream options.
2479
+ - Before classifying, the handler stops the partial throttle and awaits any
2480
+ partial commit in flight, so no stale partial lands after the outcome. The
2481
+ terminal message is classified in one commit that also clears the partial:
2482
+ - `stop`/`length`: append the answer, settle the run's inputs `done`, remove
2483
+ `run` and `generation`, and complete with `{ entryId }`. The same commit
2484
+ applies the final boundary (section 6).
2485
+ - `toolUse`: append the assistant entry and continue through the tool chain.
2486
+ - `error` that `isRetryableAssistantError()` accepts while the conversation's
2487
+ retry policy allows another attempt (`enabled` and `attempt <= maxRetries`,
2488
+ so `maxRetries` counts retries after the first attempt): append the error entry and move to
2489
+ `retry` with `until = now + retryDelayMs(policy, attempt)`.
2490
+ - any other `error`, or `aborted` without an abort mark: append the error
2491
+ entry, settle the inputs `unanswered` with `model_error`, remove `run` and
2492
+ `generation`, and fail.
2493
+ - `deferred`: move to `poll` with `pollAt = now + (handle.pollAfterMs ?? 5000)`.
2494
+ - `retry` sleeps until `until`, then returns to `prepare` with the next attempt,
2495
+ so configuration changes made during the backoff apply.
2496
+ - `poll` sleeps until `pollAt` and calls `models.fetchDeferred()`. A still
2497
+ deferred result moves `pollAt` strictly later; any other result is classified
2498
+ as above.
2499
+ - The abort handler calls `models.cancelDeferred()` in `poll` when the model is
2500
+ known (a failure is reported), converts a committed partial, settles the inputs `unanswered` with
2501
+ `aborted`, removes `run` and `generation`, and ends `aborted`.
2502
+
2503
+ Input submissions settle `unanswered` with one of these reasons: `no_model`,
2504
+ `model_error` (detail: provider error text), `aborted`, `faulted` (detail: error
2505
+ message), or an orphaning blocked reason (section 5.4). Fault and orphan
2506
+ settlement discard a committed partial without writing a transcript entry.
2507
+
1539
2508
 
1540
2509
  ## 9. Document observation and Chord
1541
2510
 
1542
- ### 9.1 Document source
2511
+ ### 9.1 Document state
2512
+
2513
+ Chord's existing replicated-state layer exposes this source-adoption contract:
1543
2514
 
1544
2515
  ```ts
1545
- declare const documentSourceType: unique symbol;
1546
- interface DocumentSource<T extends JsonObject> {
1547
- readonly [documentSourceType]: T;
2516
+ interface ReplicatedStateSourceFrame<T> {
2517
+ readonly cursor: number;
2518
+ readonly value: T;
2519
+ readonly ops: readonly Op[];
2520
+ readonly context: Context;
1548
2521
  }
1549
2522
 
2523
+ interface ReplicatedStateSourceAttachment<T> {
2524
+ /** Fixed immutable snapshot captured at the atomic attachment boundary. */
2525
+ readonly snapshot: { readonly value: T; readonly cursor: number };
2526
+ /** Install the sole listener and synchronously drain every buffered frame. */
2527
+ activate(listener: (frame: ReplicatedStateSourceFrame<T>) => void): void;
2528
+ dispose(): void;
2529
+ }
2530
+
2531
+ interface ReplicatedStateSource<T> {
2532
+ /** Atomically capture a snapshot and begin buffering every later commit. */
2533
+ attach(): ReplicatedStateSourceAttachment<T>;
2534
+ }
2535
+
2536
+ interface ReplicatedStateSourceOptions {
2537
+ readonly onError?: (error: Error) => void;
2538
+ }
2539
+
2540
+ interface AttachedReplicatedState<T> extends ReplicatedState<T> {
2541
+ readonly value: T;
2542
+ dispose(): void;
2543
+ }
2544
+
2545
+ function replicatedState<T>(
2546
+ source: ReplicatedStateSource<T>,
2547
+ options?: ReplicatedStateSourceOptions,
2548
+ ): AttachedReplicatedState<T>;
2549
+
2550
+ type DocumentState<T extends JsonObject> =
2551
+ AttachedReplicatedState<Readonly<T> | null>;
2552
+
1550
2553
  type WatchEnd =
1551
2554
  | { readonly reason: "stopped" | "cancelled" | "session_closed" | "retired" }
1552
2555
  | { readonly reason: "listener_error"; readonly error: Error };
1553
2556
 
1554
2557
  interface WatchHandle<T> {
1555
- /** Immutable acquisition snapshot. This reference never changes. */
2558
+ /** Acquisition revision before start; latest delivered immutable revision afterward. */
1556
2559
  readonly value: T;
1557
2560
  /** Installs the sole serialized asynchronous listener. */
1558
- start(listener: (ops: readonly Op[], context: Context) => Promise<void>): void;
1559
- /** Prevents another callback from starting and signals an in-flight callback. */
1560
- stop(): void;
1561
- /** Settles after termination and any in-flight callback. */
2561
+ start(listener: (value: T, ops: readonly Op[], context: Context) => Promise<void>): void;
2562
+ /** Idempotently stops future callbacks and returns this watch's terminal result. */
2563
+ stop(): Promise<WatchEnd>;
2564
+ /** Settles when the watch terminates; an already-running callback remains caller-owned. */
1562
2565
  readonly closed: Promise<WatchEnd>;
1563
2566
  }
1564
2567
 
1565
2568
  type DocumentWatch<T extends JsonObject> = WatchHandle<Readonly<T> | null>;
1566
2569
 
1567
2570
  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>>;
2571
+ watchDoc<T extends JsonObject>(token: SessionDocToken<T>, context: Context): Promise<DocumentWatch<T> | undefined>;
2572
+ watchDoc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId, context: Context): Promise<DocumentWatch<T> | undefined>;
2573
+ watchDoc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId, context: Context): Promise<DocumentWatch<T> | undefined>;
2574
+ watchDoc<T extends JsonObject, I extends JsonValue>(token: SessionDocFamilyToken<T, I>, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
2575
+ watchDoc<T extends JsonObject, I extends JsonValue>(token: ConversationDocFamilyToken<T, I>, conversationId: ConversationId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
2576
+ watchDoc<T extends JsonObject, I extends JsonValue>(token: TaskDocFamilyToken<T, I>, taskId: TaskId, key: string, context: Context): Promise<DocumentWatch<T> | undefined>;
1570
2577
  }
1571
2578
  ```
1572
2579
 
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
- ```
2580
+ `documentState()` attaches Pico's committed document stream to Chord before
2581
+ returning. The returned state is already hydrated with the exact shareable
2582
+ immutable revision, has no mutation methods, and publishes every later exact
2583
+ committed revision and operation batch without another tracker, value copy, or
2584
+ re-diff. Disposing it unregisters only that observation; Pico remains the sole
2585
+ document mutator.
1586
2586
 
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
2587
+ State and watch acquisition never create; absent lookup returns `undefined`.
2588
+ Successful acquisition binds one concrete incarnation. Retirement publishes a
2589
+ JSON `null` root replacement and ends that incarnation's stream. If the state
1591
2590
  remains exposed, consumers see `null`, never stale state. A later recreation
1592
- requires acquiring a new source/watch.
2591
+ requires acquiring a new state or watch.
1593
2592
 
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.
2593
+ Each document state assigns its own in-memory contiguous Chord delivery sequence.
2594
+ Pico does not persist or expose that sequence through `WatchHandle`. States and watches retain immutable revisions independently of the loaded tracker
2595
+ cache. Reopen creates a new state lifetime and hydration.
2596
+
2597
+ Internally, state attachment uses Chord's synchronous atomic snapshot-and-register
2598
+ boundary: the snapshot includes every commit before attachment, and every later
2599
+ exact frame is buffered until activation. This prevents a snapshot from being
2600
+ paired with operations based on a newer unseen revision.
1599
2601
 
1600
2602
  ### 9.2 `watchDoc`
1601
2603
 
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
2604
+ Tasks, hooks, and tools may observe any existing document for which their code
2605
+ has a token and owner/key. There is no additional subtree permission system inside trusted
1604
2606
  Session code.
1605
2607
 
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
1610
- never changes.
2608
+ On the Session line, `watchDoc()` resolves one existing concrete incarnation,
2609
+ captures its current immutable tracker revision in O(1), and registers for every
2610
+ later exact committed revision and operation batch. It returns `undefined` when
2611
+ absent. Before `start()`, `watch.value` remains the acquisition revision while
2612
+ later frames buffer.
1611
2613
 
1612
2614
  ```ts
1613
- const watch = await api.watchDoc(JobOutputDoc, target, context);
1614
- let value = watch.value;
2615
+ const watch = await api.watchDoc(JobOutputDoc, producerTaskId, context);
2616
+ if (watch === undefined) return;
1615
2617
  try {
1616
- await initializeConsumer(value, context);
1617
- watch.start(async (ops, deliveryContext) => {
1618
- value = applyImmutable(value, ops);
1619
- await consume(value, deliveryContext);
2618
+ await initializeConsumer(watch.value, context);
2619
+ watch.start(async (value, ops, deliveryContext) => {
2620
+ await consume(value, ops, deliveryContext);
1620
2621
  });
1621
2622
  } catch (error) {
1622
- watch.stop();
1623
- await watch.closed;
2623
+ await watch.stop();
1624
2624
  throw error;
1625
2625
  }
1626
2626
  ```
1627
2627
 
1628
- The caller initializes from `value` before `start()`. `start()` synchronously
1629
- installs the sole listener, changes the prepared handle to active, and schedules
1630
- delivery; it never invokes the listener inline. A second `start()`, or `start()`
1631
- after stop, throws.
1632
-
1633
- One per-watch delivery line awaits each callback before starting the next, so
1634
- callbacks never overlap. Commits never wait for callback settlement: while one
1635
- batch is in flight, later batches append to the pending queue. Empty operation
1636
- batches are discarded before enqueueing and cannot consume queue bookkeeping. An
1637
- update accepted between acquisition and return, or between return and `start()`,
1638
- is therefore not lost. The listener's promise covers all work the watch
1639
- serializes; fire-and-forget work started by the listener is outside that
1640
- guarantee.
1641
-
1642
- The pending queue is bounded only by the total number of operations in its
1643
- undelivered batches. It never estimates serialized size or calls
1644
- `JSON.stringify()` to make a compaction decision. When the operation-count limit
1645
- is exceeded, the handle leaves the in-flight batch untouched and replaces the
1646
- entire pending suffix with one newly allocated root replacement batch:
1647
-
1648
- ```ts
1649
- [["r", latestImmutablePublishedValue]]
1650
- ```
1651
-
1652
- The reset counts as one pending operation regardless of the document's in-memory
1653
- size. Later deltas append behind it. If the operation-count limit is exceeded
1654
- again, the whole pending suffix is replaced with a newer reset rather than
1655
- accumulating an unbounded operation list. This bounds delta bookkeeping, not the
1656
- document value itself. This watch observes convergent committed state, not every
1657
- intermediate transition.
1658
-
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()`.
1666
-
1667
- A watch remains bound to its original incarnation. Retirement replaces the
1668
- pending suffix with `[["r", null]]`. Before start, `value` remains the original
1669
- snapshot and the terminal reset becomes the first delivered batch. While active,
1670
- the reset follows any in-flight callback. Successful terminal delivery closes as
1671
- `retired`; recreation requires another watch.
1672
-
1673
- `stop()` is idempotent, unregisters the handle, discards pending batches,
1674
- prevents another callback from starting, and signals the watch delivery context.
1675
- An in-flight callback is allowed to settle; `closed` resolves only afterward.
1676
- 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,
1680
- whose `start()` throws and whose `closed` reports `cancelled`. Invocation
1681
- termination and Session close stop owned watches similarly. Listener rejection
1682
- is reported, discards pending work, and closes only that watch as
1683
- `listener_error`. The first termination reason wins, and every late rejection is
1684
- observed.
2628
+ `start()` installs the sole listener and schedules delivery; it never invokes
2629
+ user code inline. Each callback receives the exact immutable committed value and
2630
+ the exact operation batch that produced it. Immediately before invocation,
2631
+ `watch.value` advances to that value. The watch awaits the listener before
2632
+ starting its next callback, but Session commits never wait for callback
2633
+ settlement.
2634
+
2635
+ A watch retains at most 100 pending committed frames, excluding the frame already
2636
+ being delivered. Adding frame 101 replaces the complete undelivered suffix with
2637
+ one self-contained root replacement `[["r", newestValue]]`, using the newest
2638
+ exact immutable revision and that commit's Context. Later exact frames follow
2639
+ that replacement normally. Another overflow replaces the pending suffix again.
2640
+ No serialized-byte measurement, value copy, operation replay, or re-diff occurs.
2641
+ This is convergent observation, not an audit stream; consumers requiring every
2642
+ transition must persist those facts separately.
2643
+
2644
+ `watch.value` and every previously returned revision remain stable forever under
2645
+ the trusted immutability contract. Consumers must not mutate them or retained
2646
+ descendants. The selected commit Context's values are preserved without inheriting the
2647
+ producer's cancellation. A callback owns its own asynchronous work.
2648
+
2649
+ A watch remains bound to its original incarnation. Retirement queues the exact
2650
+ terminal `[["r", null]]` frame, or folds it into an overflow replacement with
2651
+ value `null`. After that callback settles, the watch closes as `retired` and never
2652
+ follows a replacement incarnation.
2653
+
2654
+ `stop()` is idempotent. It synchronously unregisters, discards pending frames,
2655
+ prevents another callback from starting, and returns the common terminal promise.
2656
+ An already-running callback is not aborted or joined and remains caller-owned.
2657
+ The acquisition `Context` governs watch lifetime. Cancellation during acquisition
2658
+ cleans up before rejecting; later cancellation and Session close stop future
2659
+ delivery similarly. Listener failure discards pending work and closes only that
2660
+ watch as `listener_error`. The first termination reason wins, and every late
2661
+ rejection is observed.
1685
2662
 
1686
2663
  ### 9.3 Conversation view
1687
2664
 
@@ -1702,54 +2679,57 @@ through their own Chord services, not automatically mounted.
1702
2679
  The mount consumes one complete Session commit and publishes one Chord batch:
1703
2680
 
1704
2681
  ```text
1705
- document op ["s", ["message"], value]
1706
- -> view op ["s", ["docs", "pi.live", "message"], value]
2682
+ document op ["s", ["generation", "message"], value]
2683
+ -> view op ["s", ["docs", "pi.live", "generation", "message"], value]
1707
2684
  ```
1708
2685
 
1709
2686
  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
1711
- projection. It materializes one immutable published view per batch with
1712
- `applyImmutable(previousView, mountedOps)` so every conversation watch can share
1713
- that value for reset compaction. A Chord adapter assigns a contiguous in-memory
1714
- delivery sequence per view source lifetime.
1715
-
1716
- `Conversation.watch()` exposes that mount through the same immutable acquisition
1717
- snapshot, serialized asynchronous listener, and reset-compacted pending queue as
1718
- `watchDoc()`. Each non-empty operation batch represents one complete Session
1719
- commit; a commit that does not change the mounted view emits no watch batch. Chord
1720
- Session facets may forward the captured value and committed operations through
1721
- services, but that product wiring is not part of the Harness facade and must not
1722
- add another tracker or semantic event envelope.
2687
+ the same publication. Before Storage admission, the Session derives the mounted
2688
+ operation batch and prepares each affected loaded mount's next immutable revision
2689
+ with the optimized immutable applier. Failure rolls back normally. After Storage
2690
+ success, finalization only installs the prepared mount pointers/cursors and
2691
+ enqueues publication. Mounted document revisions may be structurally shared
2692
+ because they obey the same trusted immutability contract. The mount performs no
2693
+ semantic projection and owns no second persistence authority. A Chord adapter
2694
+ assigns a contiguous in-memory delivery sequence per view source lifetime.
2695
+
2696
+ `Conversation.viewState()` exposes the mount directly as a disposable read-only
2697
+ Chord state for facets and UI services. `Conversation.watch()` exposes the same
2698
+ mount through Package 12's serialized exact-frame watch with bounded pending
2699
+ frames and full-value overflow replacements. An empty mounted operation batch
2700
+ creates no revision; a redundant nonempty batch remains a real publication.
2701
+ Neither API adds another tracker, persistence authority, or semantic event
2702
+ envelope.
1723
2703
 
1724
2704
  ### 9.4 Agent-mode notifications
1725
2705
 
1726
2706
  The Session kernel and Chord structural sources do not maintain a semantic event
1727
2707
  journal. Coding-agent JSON/RPC compatibility uses a thin agent-mode adapter
1728
- derived from each uncoalesced committed publication before per-watch queue
1729
- compaction. It owns no tracker or persistence and emits notifications only after
2708
+ derived from each uncoalesced committed publication before any per-watch
2709
+ overflow replacement. It owns no tracker or persistence and emits notifications only after
1730
2710
  the commit that makes them true.
1731
2711
 
1732
2712
  The adapter protocol covers run start/settlement, committed assistant progress,
1733
- message entry settlement, tool intent/progress/result, input queue/outcome,
2713
+ message entry settlement, tool intent/progress/result, submission queue/outcome,
1734
2714
  retry/deferred/compaction state, configuration changes, and faults. One commit
1735
2715
  may produce an ordered batch. Progress notifications represent Pico's throttled
1736
2716
  durable partials, not every raw provider frame. The exact legacy `AgentEvent`
1737
2717
  wire format is not preserved.
1738
2718
 
1739
2719
  Notifications have no hydration or replay contract. A consumer requiring a
1740
- complete lifecycle subscribes before admitting the input; a late or reconnecting
2720
+ complete lifecycle subscribes before admitting the submission; a late or reconnecting
1741
2721
  consumer hydrates structural state and history instead. Product adapters apply
1742
2722
  these rules:
1743
2723
 
1744
2724
  - TUI hydrates and renders `ConversationView`, then applies structural updates;
1745
2725
  notifications may drive transient animation but are not its authority.
1746
- - Print awaits its submitted `InputHandle` and prints that input's answer.
2726
+ - Print awaits its input `Submission` and prints that submission's answer.
1747
2727
  - JSON/RPC expose correlated commands plus the ordered agent notification
1748
2728
  protocol, with transport backpressure and disconnect policy owned by that
1749
2729
  adapter.
1750
2730
 
1751
2731
  This adapter is allowed even though a public Session-kernel semantic stream is a
1752
- non-goal. It must not derive notifications from a lossy, reset-compacted watch
2732
+ non-goal. It must not derive notifications from a lossy, overflow-reset watch
1753
2733
  when complete subscribed lifecycle delivery is promised.
1754
2734
 
1755
2735
  ## 10. Storage contract
@@ -1764,20 +2744,30 @@ type Page<T, C> = {
1764
2744
 
1765
2745
  type Cursor = Readonly<Record<string, JsonValue>>;
1766
2746
 
2747
+ type ConversationQuery = {
2748
+ readonly ownerConversationId?: ConversationId;
2749
+ readonly ownerTaskId?: TaskId;
2750
+ };
2751
+
1767
2752
  type EntryQuery = {
1768
- readonly conversationId: Id;
1769
- readonly minEntryId?: Id; // inclusive
1770
- readonly maxEntryId?: Id; // inclusive
2753
+ readonly conversationId: ConversationId;
2754
+ readonly minEntryId?: EntryId; // inclusive
2755
+ readonly maxEntryId?: EntryId; // inclusive
1771
2756
  };
1772
2757
 
1773
2758
  type TaskQuery = {
1774
- readonly conversationId?: Id;
2759
+ readonly conversationId?: ConversationId;
1775
2760
  readonly kind?: string;
1776
2761
  readonly status?: "pending" | "running" | "terminal";
1777
2762
  readonly abortRequested?: boolean;
1778
2763
  readonly background?: boolean;
1779
2764
  };
1780
2765
 
2766
+ type SubmissionQuery = {
2767
+ readonly conversationId?: ConversationId;
2768
+ readonly status?: SubmissionRecord["status"];
2769
+ };
2770
+
1781
2771
  type DocumentPoint = Seq | "current";
1782
2772
 
1783
2773
  type DocumentAddress = {
@@ -1796,66 +2786,84 @@ type DocumentContent =
1796
2786
  | { readonly version: number; readonly kind: "base"; readonly value: JsonObject }
1797
2787
  | { readonly version: number; readonly kind: "delta"; readonly ops: readonly Op[] };
1798
2788
 
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
2789
  type StoredDocument = {
1804
2790
  readonly record: DocumentRecord;
1805
- readonly records: readonly [
1806
- Extract<StoredDocumentRecord, { kind: "base" }>,
1807
- ...Extract<StoredDocumentRecord, { kind: "delta" }>[],
1808
- ];
2791
+ readonly version: number;
2792
+ readonly value: JsonObject;
2793
+ /** Deltas replayed after the selected base to materialize `value`. */
2794
+ readonly deltasSinceBase: number;
1809
2795
  };
1810
2796
 
1811
2797
  type StorageWrite =
1812
2798
  | { readonly type: "conversation"; readonly value: ConversationRecord }
1813
2799
  | { readonly type: "entry"; readonly value: EntryRecord }
1814
2800
  | { readonly type: "task"; readonly value: TaskRecord<JsonValue, JsonValue, JsonValue> }
1815
- | { readonly type: "input"; readonly value: Input }
2801
+ | { readonly type: "submission"; readonly value: SubmissionRecord }
1816
2802
  | {
1817
2803
  readonly type: "document.create";
1818
2804
  readonly record: DocumentCreate;
1819
2805
  readonly content: Extract<DocumentContent, { kind: "base" }>;
1820
2806
  }
2807
+ | {
2808
+ readonly type: "document.copy";
2809
+ readonly record: DocumentCreate;
2810
+ readonly source: { readonly id: DocumentId; readonly at: DocumentPoint };
2811
+ }
1821
2812
  | {
1822
2813
  readonly type: "document.change";
1823
- readonly id: Id;
2814
+ readonly id: DocumentId;
1824
2815
  readonly content: DocumentContent;
1825
2816
  }
1826
- | { readonly type: "document.retire"; readonly id: Id };
2817
+ | { readonly type: "document.retire"; readonly id: DocumentId };
1827
2818
 
1828
2819
  /**
1829
2820
  * Trusts the owning Session to supply semantically valid records, references,
1830
2821
  * ancestry, and transitions. Enforces atomicity, global ID ownership, immutable
1831
- * conversation/entry creation, document lifecycle consistency, and detached
1832
- * values; Session serializes commits.
2822
+ * conversation/entry creation, document record consistency, and detached
2823
+ * values; Session serializes commits. Sequences strictly increase but may have
2824
+ * gaps. Once commit() resolves, later reads through that Storage observe it.
1833
2825
  */
1834
2826
  interface Storage {
1835
2827
  commit(writes: readonly StorageWrite[], context: Context): Promise<Seq>;
1836
- mintId(): Id;
2828
+ /** Allocate from the one global numeric namespace; the generic brand is compile-time only. */
2829
+ mintId<I extends Id<string>>(): Promise<I>;
1837
2830
 
1838
- conversation(id: Id, context: Context): Promise<ConversationRecord | undefined>;
1839
- scanConversations(cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<ConversationRecord, Cursor>>;
2831
+ conversation(id: ConversationId, context: Context): Promise<ConversationRecord | undefined>;
2832
+ scanConversations(query: ConversationQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<ConversationRecord, Cursor>>;
1840
2833
 
1841
- entry(id: Id, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
1842
- findLatestHeadMarker(conversationId: Id, atOrBeforeEntryId: Id | undefined, context: Context): Promise<(EntryRecord & { readonly head: Id }) | undefined>;
1843
- scanEntries(query: EntryQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<EntryRecord, Cursor>>;
2834
+ entry(id: EntryId, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
2835
+ entry(conversationId: ConversationId, id: EntryId, context: Context): Promise<{ readonly entry: EntryRecord; readonly commitSeq: Seq } | undefined>;
2836
+ findLatestHeadMarker(conversationId: ConversationId, atOrBeforeEntryId: EntryId | undefined, context: Context): Promise<(EntryRecord & { readonly head: EntryId }) | undefined>;
2837
+ scanEntries(query: EntryQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<EntryRecord, Cursor>>;
1844
2838
 
1845
- task(id: Id, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
1846
- scanTasks(query: TaskQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
2839
+ task(id: TaskId, context: Context): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
2840
+ scanTasks(query: TaskQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
1847
2841
 
1848
- input(id: Id, context: Context): Promise<Input | undefined>;
1849
- inputByRequest(conversationId: Id, requestId: string, context: Context): Promise<Input | undefined>;
2842
+ submission(id: SubmissionId, context: Context): Promise<SubmissionRecord | undefined>;
2843
+ scanSubmissions(query: SubmissionQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<SubmissionRecord, Cursor>>;
2844
+ submissionByRequest(conversationId: ConversationId, requestId: string, context: Context): Promise<SubmissionRecord | undefined>;
1850
2845
 
1851
2846
  findDocument(address: DocumentAddress, at: DocumentPoint, context: Context): Promise<DocumentRecord | undefined>;
1852
- document(id: Id, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined>;
1853
- scanDocuments(query: DocumentQuery, cursor: Cursor | undefined, limit: number, context: Context): Promise<Page<DocumentRecord, Cursor>>;
2847
+ document(id: DocumentId, at: DocumentPoint, context: Context): Promise<StoredDocument | undefined>;
2848
+ scanDocuments(query: DocumentQuery, limit: number, cursor: Cursor | undefined, context: Context): Promise<Page<DocumentRecord, Cursor>>;
1854
2849
 
1855
2850
  close(context: Context): Promise<void>;
1856
2851
  }
1857
2852
  ```
1858
2853
 
2854
+ `StorageRejected` means a batch was rejected before any durable effect and is
2855
+ guaranteed not to have committed. Session rolls such a batch back normally;
2856
+ unknown failures after Storage admission remain fatal because their commit state
2857
+ is uncertain. Backends use `StorageRejected` for deterministic `document.copy`
2858
+ source, replay, and consistency failures only when rollback is guaranteed.
2859
+
2860
+ A `document.copy` reads committed pre-batch source state independent of command
2861
+ order. The source must be an alive conversation document at the selected point,
2862
+ and kind/key/history/fork must match the child create record. Storage persists
2863
+ one independent complete child base at the source's stored version. A batch may
2864
+ not create, change, or retire a selected source. Later source changes,
2865
+ reclamation, retirement, or backend reopen cannot affect the child.
2866
+
1859
2867
  Cursors are backend-owned JSON objects. Callers only round-trip them to the same
1860
2868
  scan on the same storage; cross-storage or cross-query use is unsupported. The
1861
2869
  Session owns the mutation line, so storage implementations do not add a second
@@ -1868,8 +2876,14 @@ inclusive ID range in newest-first order while applying every conversation
1868
2876
  ancestry cap. With no bounds it pages complete visible history. To read context
1869
2877
  through entry `E`, find the marker at or before `E`, then scan from
1870
2878
  `marker?.head` through `E`. For current context the upper bound is omitted.
1871
- `entry()` combines exact global lookup with the commit sequence required by
1872
- historical document reads. `limit` is always the maximum page size.
2879
+ Conversation owner filters are indexed and conjunctive. They support ownership
2880
+ traversal without an all-conversation scan; application-maintained registries
2881
+ are not a substitute for these kernel indexes.
2882
+
2883
+ `entry(id)` combines exact global lookup with the commit sequence required by
2884
+ historical document reads. `entry(conversationId, id)` returns that pair only
2885
+ when the entry is visible through the requested conversation's ancestry. `limit`
2886
+ is always the maximum page size.
1873
2887
  `findDocument()` resolves one exact logical kind/scope/key address at current or
1874
2888
  historical membership. A missing key means the singleton, not every family
1875
2889
  member. `scanDocuments()` enumerates only the incarnations alive in one exact
@@ -1878,15 +2892,19 @@ ascending incarnation IDs. There is no ordinary open-time all-document scan.
1878
2892
  Task queries support conversation, kind, live/terminal status, abort mark, and
1879
2893
  background status.
1880
2894
 
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.
2895
+ `document(id, at)` materializes one specific incarnation and never follows a
2896
+ replacement at the same logical address. Callers resolve an address with
2897
+ `findDocument()` when they do not already hold an incarnation ID. It selects the
2898
+ newest applicable base, applies its ordered Chord delta tail, and returns the detached materialized value, stored
2899
+ definition version, and number of replayed deltas after that base. Base/delta records are backend-private. The lookup never
2900
+ scans unrelated documents. An unknown ID returns `undefined`. At `"current"`, a
2901
+ retired incarnation returns `undefined`. A numeric lookup of a rewindable
2902
+ conversation incarnation returns `undefined` outside its half-open lifetime and
2903
+ reconstructs the selected value inside it. A numeric lookup of a known current-
2904
+ only incarnation rejects rather than depending on reclaimed content. Metadata
2905
+ membership remains queryable historically. A missing required base, a version
2906
+ change inside a delta tail, or an operation that cannot be applied inside an
2907
+ addressable lifetime is storage corruption, not absence.
1890
2908
 
1891
2909
  One normalized batch contains at most one create/change content command per
1892
2910
  incarnation and may also retire that incarnation. Storage applies content before
@@ -1911,8 +2929,8 @@ after a committed base or retirement.
1911
2929
 
1912
2930
  One SQL transaction is one Session commit. SQLite stores:
1913
2931
 
1914
- - conversation, entry, task, and input records;
1915
- - document lifecycle records;
2932
+ - conversation, entry, task, and submission records;
2933
+ - document records;
1916
2934
  - indexed document bases/deltas by document and commit sequence.
1917
2935
 
1918
2936
  Live task transitions replace one row. Terminal tasks remain as small records.
@@ -1926,14 +2944,15 @@ benchmarks.
1926
2944
 
1927
2945
  ### 11.3 JSONL
1928
2946
 
1929
- JSONL uses reclaimable sidecars without exposing them to the harness. Persistence
1930
- alone does not provide the ownership boundary: any decoded indexes, materialized
1931
- values, or caches retained in memory must be detached from commit arguments and
1932
- must not be exposed directly by reads. A JSONL backend cannot simply add file
1933
- appends around aliasing memory tables.
2947
+ JSONL depends on the portable `FileSystem` capability, not the broader
2948
+ `ExecutionEnv`. It uses reclaimable sidecars without exposing them to the
2949
+ harness. Persistence alone does not provide the ownership boundary: any decoded
2950
+ indexes, materialized values, or caches retained in memory must be detached from
2951
+ commit arguments and must not be exposed directly by reads. A JSONL backend
2952
+ cannot simply add file appends around aliasing memory tables.
1934
2953
 
1935
2954
  ```text
1936
- main.jsonl table writes, lifecycle, and one marker per commit
2955
+ main.jsonl table writes, document records, and one marker per commit
1937
2956
  doc-<id>.jsonl one document incarnation
1938
2957
  task-<id>.jsonl live task replacements
1939
2958
  ```
@@ -1945,9 +2964,18 @@ Publication protocol:
1945
2964
  3. Publish in memory only after the marker write succeeds.
1946
2965
 
1947
2966
  Every commit uses this protocol; there is no standalone-sidecar fast path.
1948
- Without `fsync`, it guarantees ordinary process-crash consistency, not survival
1949
- of power, host, kernel, or filesystem failure. Durable mode flushes sidecars
1950
- before the marker.
2967
+ JSONL creation accepts an `fsync` option that defaults to `false`. Without
2968
+ `fsync`, it guarantees ordinary process-crash consistency, not survival of
2969
+ power, host, kernel, or filesystem failure. With `fsync: true`, the backend
2970
+ appends all affected sidecar records, flushes each affected sidecar, and only
2971
+ then appends the main marker. Ordinary publication does not explicitly flush
2972
+ `main.jsonl`; an acknowledged tail commit may therefore still disappear, but a
2973
+ marker that survives should not overtake its sidecar data. A main-only commit has
2974
+ no sidecars to flush. Before destructive reclamation with `fsync: true`, the
2975
+ backend flushes `main.jsonl` once so the authorizing marker cannot disappear
2976
+ while its replacement or removal survives. If that flush fails, the committed
2977
+ state remains published and reclamation is deferred. A non-empty temporary
2978
+ replacement is also flushed before rename.
1951
2979
 
1952
2980
  Recovery:
1953
2981
 
@@ -1959,28 +2987,35 @@ Recovery:
1959
2987
  record unnecessary.
1960
2988
  - Any uncertain append failure poisons the open backend.
1961
2989
 
1962
- Reclamation starts only after the authorizing base/retirement commits. It writes
1963
- a temporary replacement, renames it, and invalidates cached file descriptors so
1964
- future appends cannot target an unlinked inode. `main.jsonl` is not compacted in
1965
- the initial implementation.
2990
+ Reclamation starts only after the authorizing base/retirement commits. When no
2991
+ sidecar records remain, it removes the sidecar directly. Otherwise, it writes a
2992
+ temporary replacement, renames it, and invalidates cached file descriptors so
2993
+ future appends cannot target an unlinked inode. Flushing `main.jsonl` to
2994
+ authorize reclamation does not compact it. `main.jsonl` is not compacted in the
2995
+ initial implementation.
1966
2996
 
1967
2997
  ## 12. API footguns
1968
2998
 
1969
2999
  These are contracts, not invitations to add defensive machinery:
1970
3000
 
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.
3001
+ - **Detached draft work:** draft reads, draft writes, and all `Tx` operations
3002
+ reject after the Session callback settles. Fire-and-forget work that runs
3003
+ before callback settlement can still mutate the active transaction and is
3004
+ unsupported.
1975
3005
  - **Read after write:** read every required table row before the first table
1976
3006
  write. Document drafts remain usable afterward; table reads do not.
3007
+ - **Writing to a busy conversation:** only the Harness appends to a conversation
3008
+ with an active run. Raw entries appended by `Harness.commit()` or a custom
3009
+ task while a generation prepares its request can misplace its system prompt entries;
3010
+ use a write submission.
1977
3011
  - **Long transactions:** an async commit callback holds the Session mutation
1978
3012
  line. Never await models, tools, processes, network calls, humans, a nested
1979
3013
  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.
3014
+ - **Explicit creation:** only typed `tx.doc()` creates an absent document. Snapshot,
3015
+ state, and watch lookup return `undefined` instead.
3016
+ - **Family initialization:** the first acquisition of an absent family address
3017
+ selects its seed. Existing instances and later calls ignore seeds; a seed is
3018
+ neither identity nor an update.
1984
3019
  - **Checkpoint starvation:** if `checkpointWhen()` never returns true, replay
1985
3020
  and current-only document storage can grow without bound while the document is
1986
3021
  live.
@@ -1991,15 +3026,20 @@ These are contracts, not invitations to add defensive machinery:
1991
3026
  change requires explicit copy and retirement. Passing incompatible definition
1992
3027
  tokens that claim the same kind is unsupported caller misuse; Session does not
1993
3028
  maintain a document-definition registry to detect it.
1994
- - **Watch activation:** `watch.value` is the immutable acquisition snapshot.
1995
- Initialize the consumer from it before `start()`; queued operations are based
1996
- on exactly that value.
1997
- - **Coalesced watches:** slow or unstarted watches may replace an undelivered
1998
- suffix with a complete reset, omitting intermediate committed states. A facet
1999
- that must audit every transition must persist each fact as an immutable entry
2000
- or in its own journal and scan that history explicitly.
2001
- - **Watch self-join:** a listener may call `stop()`, but must not await its own
2002
- `closed` promise or a Session close that joins that listener.
3029
+ - **Trusted immutable revisions:** `snapshot()`, state values, watch values, and
3030
+ their descendants may share tracker-owned containers. Never mutate them; copy
3031
+ first when mutable ownership is required. Runtime freezing is not provided.
3032
+ - **Watch activation:** before `start()`, `watch.value` remains the immutable
3033
+ acquisition revision. Initialize the consumer from it first. After start, the
3034
+ property advances to each delivered immutable revision before its callback;
3035
+ every earlier reference remains stable.
3036
+ - **Buffered watches:** slow or unstarted watches retain up to 100 exact committed
3037
+ frames. Overflow replaces the undelivered suffix with one full-value root
3038
+ replacement, so intermediate committed states may be omitted. A consumer that
3039
+ must audit every transition must persist each fact in an immutable entry or
3040
+ journal and scan that history explicitly.
3041
+ - **Watch stop:** a listener may call and await `stop()`; it prevents future
3042
+ callbacks but neither aborts nor joins the callback already running.
2003
3043
  - **Durable progress cadence:** clients see only committed progress. A crash may
2004
3044
  lose the current uncommitted throttle window.
2005
3045
  - **Large terminal results:** terminal task records remain queryable. Put large
@@ -2007,6 +3047,20 @@ These are contracts, not invitations to add defensive machinery:
2007
3047
  outcome. Never reference a task-scoped document retired by that same outcome.
2008
3048
  - **Raw transcript:** view entries are not model context. Rendering edits,
2009
3049
  display-only entries, and model filtering require the appropriate reducer.
3050
+ - **Reserved `pi.` names:** task names, document kinds, and entry kinds starting
3051
+ with `pi.` belong to built-ins by convention. Nothing enforces it; reusing one
3052
+ collides with Harness behavior, such as `pi.system` entries being replayed as
3053
+ system messages.
3054
+ - **Early resource disposal:** disposing a `Registration` only stops new use.
3055
+ Freeing resources immediately can fail calls that are still running.
3056
+ - **Async batches:** `batch()` callbacks are synchronous. A callback that returns
3057
+ a promise publishes nothing and throws.
3058
+ - **Unstable prompt text:** a section renderer whose output changes without a
3059
+ real content change, for example by embedding the time, appends system deltas
3060
+ and defeats provider prompt caching.
3061
+ - **Durable stream options:** `streamOptions.headers` and `metadata` are stored
3062
+ in the conversation configuration history and copied into forks. Never put
3063
+ credentials there; `Models` resolves auth.
2010
3064
  - **Fatal storage errors:** after an uncertain storage failure the Session is
2011
3065
  poisoned. Do not catch the error and continue using it.
2012
3066
  - **JSONL durability:** default JSONL ordering handles ordinary process crashes;
@@ -2020,7 +3074,6 @@ Pico5 initially has no:
2020
3074
  - whole-Session DOM;
2021
3075
  - visible-undurable publication;
2022
3076
  - Session-kernel semantic event journal or independently maintained event state;
2023
- - transaction membrane;
2024
3077
  - session-scoped rewindable documents;
2025
3078
  - automatic checkpoint heuristic;
2026
3079
  - automatic third-party view mounting;
@@ -2028,4 +3081,4 @@ Pico5 initially has no:
2028
3081
  - SQL translation of arbitrary Chord operations;
2029
3082
  - JSONL global compaction or automatic corruption repair;
2030
3083
  - compatibility layer for removed Pico prototypes;
2031
- - in-process hot replacement of Session-side extension implementations.
3084
+ - forced termination of non-cooperative extension code inside one process.