@earendil-works/pi-durable 0.99.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +196 -74
  3. package/dist/env/index.d.ts +5 -0
  4. package/dist/env/index.d.ts.map +1 -1
  5. package/dist/env/index.js.map +1 -1
  6. package/dist/env/node.d.ts +2 -0
  7. package/dist/env/node.d.ts.map +1 -1
  8. package/dist/env/node.js +2 -0
  9. package/dist/env/node.js.map +1 -1
  10. package/dist/harness/agent.d.ts +31 -0
  11. package/dist/harness/agent.d.ts.map +1 -0
  12. package/dist/harness/agent.js +209 -0
  13. package/dist/harness/agent.js.map +1 -0
  14. package/dist/harness/compaction.d.ts.map +1 -1
  15. package/dist/harness/compaction.js +8 -11
  16. package/dist/harness/compaction.js.map +1 -1
  17. package/dist/harness/define.d.ts +18 -0
  18. package/dist/harness/define.d.ts.map +1 -0
  19. package/dist/harness/define.js +25 -0
  20. package/dist/harness/define.js.map +1 -0
  21. package/dist/harness/events.d.ts +5 -5
  22. package/dist/harness/events.d.ts.map +1 -1
  23. package/dist/harness/events.js +6 -6
  24. package/dist/harness/events.js.map +1 -1
  25. package/dist/harness/generation.d.ts +2 -4
  26. package/dist/harness/generation.d.ts.map +1 -1
  27. package/dist/harness/generation.js +39 -43
  28. package/dist/harness/generation.js.map +1 -1
  29. package/dist/harness/harness.d.ts.map +1 -1
  30. package/dist/harness/harness.js +68 -154
  31. package/dist/harness/harness.js.map +1 -1
  32. package/dist/harness/inbox.d.ts +4 -2
  33. package/dist/harness/inbox.d.ts.map +1 -1
  34. package/dist/harness/inbox.js +3 -5
  35. package/dist/harness/inbox.js.map +1 -1
  36. package/dist/harness/prompt.d.ts +1 -3
  37. package/dist/harness/prompt.d.ts.map +1 -1
  38. package/dist/harness/prompt.js +1 -11
  39. package/dist/harness/prompt.js.map +1 -1
  40. package/dist/harness/registry.d.ts +2 -3
  41. package/dist/harness/registry.d.ts.map +1 -1
  42. package/dist/harness/registry.js +66 -283
  43. package/dist/harness/registry.js.map +1 -1
  44. package/dist/harness/scheduler.d.ts +7 -2
  45. package/dist/harness/scheduler.d.ts.map +1 -1
  46. package/dist/harness/scheduler.js +41 -27
  47. package/dist/harness/scheduler.js.map +1 -1
  48. package/dist/harness/submissions.d.ts +4 -4
  49. package/dist/harness/submissions.d.ts.map +1 -1
  50. package/dist/harness/submissions.js +7 -4
  51. package/dist/harness/submissions.js.map +1 -1
  52. package/dist/harness/task-graph.d.ts +49 -0
  53. package/dist/harness/task-graph.d.ts.map +1 -0
  54. package/dist/harness/task-graph.js +163 -0
  55. package/dist/harness/task-graph.js.map +1 -0
  56. package/dist/harness/tool.d.ts +1 -1
  57. package/dist/harness/tool.d.ts.map +1 -1
  58. package/dist/harness/tool.js +9 -6
  59. package/dist/harness/tool.js.map +1 -1
  60. package/dist/harness/types.d.ts +171 -140
  61. package/dist/harness/types.d.ts.map +1 -1
  62. package/dist/harness/types.js.map +1 -1
  63. package/dist/harness/view.d.ts +1 -1
  64. package/dist/harness/view.d.ts.map +1 -1
  65. package/dist/harness/view.js +2 -7
  66. package/dist/harness/view.js.map +1 -1
  67. package/dist/index.d.ts +4 -2
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +2 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/session/session.d.ts.map +1 -1
  72. package/dist/session/session.js +5 -1
  73. package/dist/session/session.js.map +1 -1
  74. package/dist/session/transaction.d.ts +2 -2
  75. package/dist/session/transaction.d.ts.map +1 -1
  76. package/dist/session/transaction.js +7 -4
  77. package/dist/session/transaction.js.map +1 -1
  78. package/dist/storage/sqlite/database.d.ts +26 -19
  79. package/dist/storage/sqlite/database.d.ts.map +1 -1
  80. package/dist/storage/sqlite/database.js.map +1 -1
  81. package/dist/storage/sqlite/index.d.ts +1 -1
  82. package/dist/storage/sqlite/index.d.ts.map +1 -1
  83. package/dist/storage/sqlite/index.js.map +1 -1
  84. package/dist/storage/sqlite/migrations.js +6 -6
  85. package/dist/storage/sqlite/migrations.js.map +1 -1
  86. package/dist/storage/sqlite/node.d.ts +23 -7
  87. package/dist/storage/sqlite/node.d.ts.map +1 -1
  88. package/dist/storage/sqlite/node.js +129 -47
  89. package/dist/storage/sqlite/node.js.map +1 -1
  90. package/dist/storage/sqlite/storage.d.ts +12 -0
  91. package/dist/storage/sqlite/storage.d.ts.map +1 -1
  92. package/dist/storage/sqlite/storage.js +121 -129
  93. package/dist/storage/sqlite/storage.js.map +1 -1
  94. package/dist/tools/bash.d.ts +1 -1
  95. package/dist/tools/bash.d.ts.map +1 -1
  96. package/dist/tools/bash.js +3 -2
  97. package/dist/tools/bash.js.map +1 -1
  98. package/dist/tools/edit.d.ts +3 -3
  99. package/dist/tools/edit.d.ts.map +1 -1
  100. package/dist/tools/edit.js +5 -4
  101. package/dist/tools/edit.js.map +1 -1
  102. package/dist/tools/file-mutation-queue.d.ts +5 -1
  103. package/dist/tools/file-mutation-queue.d.ts.map +1 -1
  104. package/dist/tools/file-mutation-queue.js +39 -33
  105. package/dist/tools/file-mutation-queue.js.map +1 -1
  106. package/dist/tools/index.d.ts +18 -0
  107. package/dist/tools/index.d.ts.map +1 -1
  108. package/dist/tools/index.js +10 -0
  109. package/dist/tools/index.js.map +1 -1
  110. package/dist/tools/read.d.ts +3 -3
  111. package/dist/tools/read.d.ts.map +1 -1
  112. package/dist/tools/read.js +4 -3
  113. package/dist/tools/read.js.map +1 -1
  114. package/dist/tools/write.d.ts +1 -1
  115. package/dist/tools/write.d.ts.map +1 -1
  116. package/dist/tools/write.js +3 -2
  117. package/dist/tools/write.js.map +1 -1
  118. package/dist/types.d.ts +15 -4
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/types.js.map +1 -1
  121. package/package.json +25 -15
  122. package/dist/harness/config.d.ts +0 -29
  123. package/dist/harness/config.d.ts.map +0 -1
  124. package/dist/harness/config.js +0 -24
  125. package/dist/harness/config.js.map +0 -1
  126. package/docs/chord-delta-findings.md +0 -358
  127. package/docs/pico-v5-chord-usage.md +0 -373
  128. package/docs/pico-v5-handoff.md +0 -861
  129. package/docs/pico-v5.md +0 -4174
@@ -1,861 +0,0 @@
1
- # Pico5 implementation handoff
2
-
3
- `packages/durable/docs/pico-v5.md` is normative. Implement this list in order.
4
- After every package: run its tests, run `npm run check`, and stop for user review.
5
- Do not redesign later packages while implementing the current one.
6
-
7
- Pico3 is reference material only. Preserve useful behavior, not its capability
8
- facades, membranes, document routing, view projection, events, or clone chains.
9
-
10
- ## Status
11
-
12
- - Obsolete `pico` and `pico4` prototypes were removed.
13
- - `pico3` remains.
14
- - Packages 1–20 are implemented in `packages/durable`; Package 10 was already satisfied by Chord's canonical structural diff implementation.
15
-
16
- ## 1. Records, cursors, and memory tables
17
-
18
- Implement IDs, sequences, reserved root conversation ID `1`,
19
- `ConversationRecord`, `EntryRecord`, strict input/write `SubmissionRecord`
20
- values, live/terminal `TaskRecord` values, document records, storage writes, backend-opaque JSON cursors, and
21
- detached `MemoryStorage` tables.
22
- Reserve `Conversation` for the public conversation object, `Entry` for the typed
23
- entry definition, and `Task` for the typed executable definition returned by
24
- `defineTask()`.
25
-
26
- Test reserved root identity and immutable creation, mixed atomic commits,
27
- rollback, detached reads/writes, cursor boundaries,
28
- fork-aware entry scans through deep ancestor caps, head lookup,
29
- entry-to-commit lookup, full task replacement, and submission replacement/
30
- request-ID lookup.
31
-
32
- ## 2. Memory document records
33
-
34
- Add selected document base/delta writes, retirement, reincarnation,
35
- current/as-of membership, exact logical-address lookup, scoped scans, and
36
- materialized point-in-time reads. Storage keeps base/delta revisions private and
37
- returns a detached value plus its stored definition version. It applies Chord
38
- `Op[]` directly and receives no definition callbacks or unused candidate values.
39
-
40
- Test Session-, conversation-, and task-scoped documents, half-open lifetimes,
41
- create-plus-retire, retired historical membership, family queries, current-only
42
- reclamation, version boundaries, detached ownership, and no scans of unrelated
43
- document records.
44
-
45
- ## 3. SQLite backend
46
-
47
- Implement the complete storage contract with ordinary rows and indexed document
48
- records. Do not translate Chord operations into SQL JSON patches.
49
-
50
- Run the memory conformance suite after reopen. Test SQL transaction rollback,
51
- recent/ancient as-of reads, query plans, latest reclamation, WAL checkpointing,
52
- deleted-page reuse, and representative storage sizes.
53
-
54
- ## 4. JSONL publication
55
-
56
- Implement table writes in `main.jsonl`, one document sidecar per incarnation,
57
- one sidecar per live task, and one main marker per commit. Do not add a
58
- standalone-sidecar protocol.
59
-
60
- Copy, rather than import, the current `ExecutionEnv`, `FileSystem`, `Shell`, Node
61
- implementation, and their required utility files from `packages/agent/src/harness`
62
- into `packages/durable/src/env`. Copy only the environment-related slice, not
63
- agent skills, prompts, telemetry, or tool definitions. Extend the copied
64
- filesystem contract with exact-byte file truncation and file flushing. JSONL
65
- depends only on `FileSystem`, not the broader `ExecutionEnv`. Keep the portable
66
- environment and JSONL entry points free of Node built-ins; expose Node
67
- implementations only from `/env/node` and `/storage/jsonl/node`. Do not use the
68
- Pico3 implementation as source material.
69
-
70
- Refactor the current `MemoryStorage` state machinery into a two-phase prepared
71
- mutation: validation and detachment produce a candidate that can later be
72
- applied without failure. Build `MemoryStorage.commit()` on that pair, and reuse
73
- the same machinery for JSONL. JSONL must append every prepared sidecar record,
74
- append the main marker, and only then apply the prepared in-memory mutation.
75
- Serialization or preparation failure occurs before file I/O and does not poison
76
- the backend. Retained indexes/materializations remain detached from write
77
- arguments, and reads never expose backend-owned cached objects.
78
-
79
- JSONL creation has `fsync?: boolean`, defaulting to `false`. With `false`, append
80
- sidecars and then the marker without an explicit flush. With `true`, append all
81
- affected sidecars, flush each affected sidecar, and then append the main marker.
82
- Do not explicitly flush `main.jsonl` for ordinary publication. A main-only commit
83
- has no sidecars to flush. Any uncertain publication append or flush failure
84
- poisons the open backend and publishes no prepared in-memory mutation. Package 5
85
- adds the separate post-publication flush required to authorize reclamation.
86
-
87
- Fault-test torn/short sidecar writes, failures between sidecars, every marker
88
- boundary, unconfirmed tails, missing confirmed data, poisoned writes, exact-byte
89
- tail truncation, both fsync settings and their call ordering, detached retained
90
- state and reads, and browser-safe portable entry points. Run the complete storage
91
- conformance suite directly and after reopen.
92
-
93
- ## 5. JSONL reclamation
94
-
95
- Implement task-document retirement and current-only base reclamation using
96
- committed markers and descriptor invalidation. Remove a sidecar directly when no
97
- records remain; otherwise use temporary replacement and rename. With `fsync:
98
- true`, flush `main.jsonl` once before destructive reclamation so the authorizing
99
- marker cannot disappear while cleanup survives; if that flush fails, skip
100
- reclamation without failing the already-published commit. Flush a non-empty
101
- temporary replacement before rename. This is not publication flushing or main
102
- compaction.
103
-
104
- Crash-test every rewrite/rename boundary. Verify that rewindable history is
105
- never reclaimed and default no-fsync behavior matches the specification.
106
-
107
- ## 6–7. Tracker transaction core, definitions, and typed access
108
-
109
- **Prerequisite:** `@earendil-works/chord/delta` exports the canonical
110
- Astra-immutable-optimized `track`, `Tracker`, `Change`, and `Prepared`, and its
111
- draft placements reject values that are not strict JSON.
112
- Experimental variants under other Delta directories are not Pico APIs.
113
-
114
- Implement these packages as one milestone. Keep the implementation layers
115
- separate, but do not build a temporary untyped document-acquisition seam.
116
- Implement the generic `Tx` table surface these tests require: exact table reads,
117
- paginated conversation/entry/task scans, `ReadAfterWrite`, ID-creating writes,
118
- and full task replacement. Scans expose the Storage cursor and caller-selected
119
- limit; they never hide an unbounded full scan. Semantic
120
- conversation, entry, task, and scheduler behavior remains in Packages 13–17.
121
-
122
- Keep one Astra-immutable tracker per loaded document. Its trusted immutable
123
- `value` is the current shareable revision. `prepare()` emits detached
124
- self-contained operations and computes the next revision with the optimized
125
- immutable applier; operation placement payloads and that revision may share
126
- containers, and neither may be mutated. `adopt()` validates ownership and
127
- revision, then only pointer-swaps to the already-computed value.
128
-
129
- Implement scope-preserving singleton/family tokens and overloads for Session,
130
- conversation, and task owners. Only `tx.doc()` is get-or-create: singleton tokens
131
- supply `initial()`, while family calls always supply key and seed and use only the
132
- first seed when absent. Definitions are explicit typed arguments, not registered
133
- declarations; conflicting definitions claiming one persisted kind are
134
- unsupported caller misuse.
135
-
136
- On first `tx.doc()` access, memoize the acquisition promise by logical address
137
- before awaiting it, then call `tracker.beginChange()`. Repeated access returns the
138
- same overlay draft for the whole possibly async Session callback. The Session
139
- line permits only one open change per tracker. A callback that settles with an
140
- unresolved acquisition rejects: seal `Tx`, abort open changes, drain and abort
141
- the pending acquisition, and observe its failure. Callback failure aborts every
142
- change. Callback success prepares every change before Storage admission.
143
-
144
- Do not walk prepared operation payloads or selected bases for strict JSON; they
145
- are strict JSON by construction. Tracker branding and revision checks enforce
146
- ownership and staleness. Evaluate each staged document write exactly once and
147
- pass Storage only the selected base value or operation batch. Keep every previous immutable revision unchanged through Storage
148
- settlement. On success, adopt every prepared value by pointer swap and enqueue
149
- its immutable revision/operations publication before releasing the line. On
150
- Storage failure, abort prepared changes, poison the Session, and publish nothing.
151
- Preparation failures roll back normally; Package 8 adds checkpoint selection and
152
- its failure path.
153
-
154
- Initializer, migration, and replacement roots are copied into exclusive kernel
155
- ownership with a strict-JSON check before becoming trusted immutable revisions.
156
- Loaded and fork-copy roots come detached from Storage and are tracked without
157
- another copy. Chord copies and strict-JSON-checks every draft placement and
158
- throws at the offending assignment. Astra empty batches suppress
159
- ordinary writes, while replayable nonempty structural no-ops remain valid writes
160
- and publications. No runtime freezing or second operation-payload copy is
161
- required.
162
-
163
- Snapshot, source, and watch lookup never create and return `undefined` when
164
- absent. `snapshot()` returns the shareable immutable current revision; callers
165
- must copy before mutation. A read-only migration may cache its migrated immutable
166
- tracker together with the older stored-version marker, without writing; the next
167
- successful `tx.doc()` still writes the required current-version base.
168
- Transaction-staged creation or migration enters the shared cache only after its
169
- enclosing Storage commit succeeds. All cold loads run on the Session line; a
170
- loaded immutable revision may be read without copying.
171
-
172
- Test callback failure; escaped-draft revocation at callback settlement; concurrent duplicate
173
- acquisition; callback failure and success with a pending acquisition; late
174
- acquisition after sealing; concurrent initialization once; initial bases; family
175
- first-seed wins; scope/token mismatch; non-creating reads; shared immutable
176
- snapshots and stable prior revisions; empty-batch suppression and replayable
177
- redundant structural no-ops; multi-document preparation failure; uncertain
178
- Storage failure poisoning; old-revision stability through Storage settlement;
179
- pointer-swap and replacement adoption; operation/revision payload sharing under
180
- the trusted no-mutation contract; non-JSON initializer and draft-placement
181
- rejection; assignment copying and repeated-placement
182
- independence; authority and prepared-draft non-escape; terminal-task rejection;
183
- task-derived conversation identity; retirement; reincarnation-bound sources;
184
- and unload/reload. Include create-task-then-document,
185
- document-after-terminal rejection, and create-document-then-terminal settlement
186
- in one transaction; internal candidate validation must not trigger
187
- `ReadAfterWrite`.
188
-
189
- ## 8. Checkpoints and migration
190
-
191
- After tracker preparation, Session evaluates `checkpointWhen(value, ops, { deltasSinceBase })` exactly once
192
- for ordinary mutations and sends Storage only the selected base or delta.
193
- Implement required creation/version bases and lazy all-older-version migration
194
- on typed access; Harness open does not scan ordinary documents.
195
-
196
- Test read-only in-memory migration, `tx.doc()` migration rollback and coalescing
197
- with later edits, rewindable migration on current/historical read, the first
198
- successful `tx.doc()` version base even without a JSON change, newer-version
199
- rejection, migrated state/watch hydration without a write, subsequent operations
200
- against that migrated baseline, stored-version fork copying, unaccessed and unavailable-definition
201
- preservation, predicate failure rollback before Storage admission, and checkpoint
202
- starvation without backend heuristics.
203
-
204
- ## 9. Conversation document forks
205
-
206
- Using fixture conversations and entry-to-commit mappings, implement the `asOf`,
207
- `current`, and `initial` settings for singleton and family documents.
208
-
209
- Test opaque stored-version copying without definitions, retired membership, new
210
- child incarnations, later lazy migration, lazy `initial` creation, and exclusion
211
- of task- and Session-scoped documents.
212
-
213
- ## 10. Chord structural array operations
214
-
215
- **Chord-owned prerequisite/integration:** the canonical Astra-immutable operation
216
- generator must encode compact replayable array changes; Pico only verifies and
217
- consumes it.
218
-
219
- Improve the canonical generator so ordinary positional mutations encode
220
- scattered removals without carrying retained payloads. Callers must not write
221
- operations manually.
222
-
223
- Test front/tail/middle/scattered/all/no removal, retained 256 KiB and 1 MiB
224
- payloads, append plus removal, later nested/index writes, exact replay, unchanged
225
- previous immutable revisions, and equality between Astra's prepared candidate
226
- and immutable operation replay. One prepared document change remains one Session
227
- commit; no intermediate candidate is adopted or published.
228
-
229
- ## 11. Chord document state
230
-
231
- Use Chord's existing `ReplicatedStateSource` attachment contract internally, but
232
- expose one already-attached read-only `DocumentState` per acquisition. It must
233
- atomically hydrate in O(1) from the current immutable revision and publish later
234
- exact committed immutable value/operation frames without another tracker, value
235
- copy, or re-diff. Disposal unregisters that one state. Pico remains the sole
236
- document mutator.
237
-
238
- Test contiguous Chord delivery sequences, atomic hydrate/subscribe, a baseline
239
- that covers queued publication without duplicate application, exact value and
240
- operation reference sharing, retirement to `null`, incarnation-bound recreation,
241
- independent state disposal, migrated hydration, definition-free fork copies,
242
- tracker-cache unload, and trusted mutation footguns.
243
-
244
- ## 12. Document watches
245
-
246
- Implement non-creating `watchDoc` as an incarnation-bound `WatchHandle` that
247
- returns `undefined` when absent and atomically captures the current immutable
248
- revision in O(1) while registering for later exact committed frames. Before
249
- `start()`, its value remains the acquisition revision. After start, invoke one
250
- serialized asynchronous listener with each exact value and operation batch,
251
- advancing `watch.value` immediately before the callback. Preserve commit Context
252
- values without inheriting producer cancellation.
253
-
254
- Retain at most 100 pending frames, excluding the in-flight callback. Adding frame
255
- 101 replaces the complete undelivered suffix with one root replacement carrying
256
- the newest exact immutable revision and Context. Do not estimate serialized
257
- bytes, call `JSON.stringify()` for accounting, copy values, replay operations, or
258
- re-diff revisions.
259
-
260
- Test updates between acquisition/return/start; exact values and operation
261
- references; no callback overlap; listener-initiated commits; commits during an
262
- in-flight callback; overflow before start and behind an active callback;
263
- retirement folded into an overflow reset; replayable redundant structural
264
- commits; delivery Context value preservation; retained earlier revision
265
- stability; trusted mutation footguns; retirement before start and while active;
266
- recreation; idempotent stop; second-start rejection; cancellation during
267
- acquisition; cancellation/close during a callback without aborting or joining it;
268
- listener-error settlement; and invocation-owned cleanup in package 14.
269
-
270
- Packages 13–20 are vertical milestones. Each must leave one real public path
271
- working end to end; do not defer all integration to the last package.
272
-
273
- Cross-cutting constraints for these milestones:
274
-
275
- - Introduce each persisted built-in document kind once, with its final schema,
276
- history/fork policy, migration, checkpoint predicate, and public mount path.
277
- Record that concrete protocol in the normative specification when the kind
278
- lands. Do not add temporary built-in kinds or later replace a fake schema.
279
- - Keep all visible progress durable. Tests may use faux models and fake effects,
280
- but production code must use pi-ai's exported `Models` interface and the real
281
- task chain; do not add a Pico model adapter or production fake successor.
282
- - Prepare every loaded `ConversationView` revision from the complete candidate
283
- Session commit before Storage admission. Never rebuild a view by subscribing
284
- to already-committed table/document publications.
285
- - A milestone may leave later operations unimplemented, but it must not expose a
286
- temporary public facade. Extend the final §2.2 objects as later behavior lands.
287
-
288
- ## 13. Openable Harness
289
-
290
- Implement the first usable slice of the final §2.2 surface: `Harness.open/close`
291
- with a registry, `root(context, { init })`, `conversation(id)`,
292
- `createConversation({ ownership, init })`, and generic
293
- inherited Session document APIs (`Harness extends Session`). Implement the
294
- public `Entry` definition token and `Conversation` handles rather than adding an
295
- intermediate capability facade. `Conversation` provides `id`, conversation-bound
296
- `commit()`, fork-aware cursor `entries()`, `context()`, `fork(at, { ownership,
297
- init })`, and every configuration getter/setter.
298
-
299
- Implement the §7.1 registry core: `createRegistry()`, `tools.add`, `tools.wrap`
300
- composition, `batch`, keyed ordering with remembered positions, `Registration`
301
- `dispose`, immutable `snapshot()`, and list helpers. Hook,
302
- task, and system prompt registrations are stored and listed now; their dispatch
303
- lands in Packages 14–16.
304
-
305
- Bring conversation and entry semantics up to the normative contract: explicit
306
- ownership, fork-aware cursor pagination, head lookup, entry edits, and model
307
- context derivation. Context reduction includes newest-edit wins, positional PR
308
- #9548 `SystemMessage` replay, tool-result ordering, missing post-fork tool
309
- results, excluded stop reasons, and separate raw-history/model-context results.
310
-
311
- Define the final `ConversationConfig` document from §2.2. Raw document writes
312
- are trusted and unchecked. Root creation uses the internal reserved-ID
313
- bootstrap in one commit together with the default configuration and `init`; do
314
- not expose a temporary root-creation API.
315
-
316
- Open ignores live tasks in this milestone; Package 14 adds reconciliation.
317
-
318
- Acceptance: open persistent storage, obtain the root, mutate configuration and
319
- history through public handles, create and fork conversations, close, reopen,
320
- and verify stable root/conversation identity and state. Test lazy root creation
321
- with `init` run once; atomic create/fork with `init`; actual forks; deep ancestor
322
- caps; same-commit entry prefixes; cursor boundaries; self-head resolution;
323
- newest-edit wins; raw head-to-tail transcript versus model context; model-less
324
- and excluded assistant entries; replacements/omissions; multiple heads;
325
- positional system messages; tool-result ordering; missing post-fork results;
326
- every configuration getter/setter; `setActiveTools` duplicate and newly-added
327
- unregistered rejection with stale names allowed; default active tools from the
328
- registry; as-of configuration inheritance including unavailable names; registry
329
- add/dispose/batch rollback/nested-batch rejection/keyed order/wrap composition;
330
- and reopen.
331
-
332
- ## 14. Durable task runtime
333
-
334
- Implement `defineTask`, exhaustive phase maps, full checkpoint replacement,
335
- kind migration at reservation, runtime commits and memos, invocation lifetime
336
- gates, scheduler reservation, dependencies, terminal outcomes, typed waits,
337
- and joins. Complete the task-facing §2.2 methods: `resume`, `getTask`,
338
- `waitForTask`, `abortTask`, and the task-aware portion of idle waits. Task definitions come from the registry snapshot;
339
- add `RegistryReader.subscribe()` so registry changes wake the scheduler.
340
-
341
- Include the execution-critical abort core: durable direct-task marks,
342
- signal-and-join of an active run, fresh abort invocation, run-commit rejection
343
- after a mark, and close precedence. Deep owned-subtree cascading and background
344
- boundaries remain Package 18. Open now reconciles every surviving `running` task
345
- to `pending`. Missing, too-old, and unmigratable definitions leave the task pending and
346
- blocked (§5.4); registry changes wake the scheduler to reconsider them. Aborting
347
- a blocked task settles it as `orphaned` with the full cleanup; nothing else
348
- orphans a task. Implement per-phase registry snapshots, refresh at
349
- every normal phase boundary, and hand over when the task definition object changed
350
- and the new definition can reserve the task.
351
-
352
- Deferred to later packages; do not stub them in Package 14:
353
- - Orphaning writes the terminal `orphaned` record and retires task documents.
354
- Unanswered input submissions and clearing run control are added in Package
355
- 15, when submissions and run control exist.
356
- - `TaskRuntime.hooks` (the hook runner) is added in Package 16 with hook
357
- dispatch. Package 14 adds `runtime.registry` (the phase's registry snapshot)
358
- and `runtime.models`.
359
- - `TaskRuntime.conversation()` returns a `ConversationHandle`, whose `submit`
360
- needs Package 15 and whose `abort`/`waitForIdle` need ownership traversal; it
361
- is added with the invocation-bound owned APIs in Package 18.
362
- - Idle waits count live non-background tasks directly (Harness-wide or in the
363
- addressed conversation). Traversal through owned conversations and background
364
- boundaries replaces this in Package 18.
365
-
366
- Use a fake two-phase external effect to test the real runtime. Acceptance is an
367
- intent/effect/outcome task interrupted after intent, closed, reopened on the same
368
- storage, and safely resumed to a durable terminal receipt. Also test unchanged-
369
- checkpoint faulting, same-phase progress, cancellation precedence, thrown
370
- handlers, dependencies, result values and entry IDs, first-writer-wins memos,
371
- terminal checkpoint/memo removal, task-document retirement, close/reopen without
372
- abort marks or fabricated outcomes, no fresh phase/abort dispatch while closing,
373
- watch cleanup, blocked tasks unblocked
374
- by later registration, migration failure leaving the record unchanged, handover
375
- after a same-name task replacement, no handover to a missing or incompatible
376
- definition, no overlap between predecessor and successor
377
- invocations, abort of a blocked task settling as `orphaned` with full cleanup,
378
- run commits rejected after the mark, and crashes at every direct-task abort stage.
379
-
380
- Implemented design notes: every task transition is decided by one callback
381
- serialized on the Session line (reservation, abort marks, runtime commits, and the
382
- step before each phase). `Tx` has no task replacement; `runtime.commit()`
383
- callbacks return the typed next state. `suspend()`, `hold()`, `quiescent()`, and
384
- a public `markTask()` were dropped from §2.2.
385
-
386
- ## 15. First runnable no-tool chat turn
387
-
388
- Implement the smallest real input-to-answer vertical path. Extend Package 14's
389
- `orphaned` settlement to mark affected input submissions unanswered and clear
390
- matching run control in the same commit, through the Harness hook for
391
- scheduler-written outcomes (§5.4); the scheduler learns nothing about turns or
392
- task kinds. Implement the final `pi.live` document (§8.2) with `run` and
393
- `generation`, the built-in entry kinds (§8.1), and the `pi.generation` task
394
- (§8.3). The inbox document is defined in Package 17 with its first writer. Add
395
- input `Submission` admission, request-ID deduplication, reacquisition and
396
- waiting, idle placement, active-turn ownership, successful answer settlement,
397
- and terminal failure cleanup. Expose the final `SubmissionDraft` union. Idle
398
- write submissions append their entry and settle `done`. Until Package 17, every
399
- submission to a busy conversation rejects with `ConversationBusy` regardless of
400
- `whenBusy`, writing nothing. Busy steer/follow-up behavior, queued writes, and
401
- reset remain Package 17.
402
-
403
- Also implement the decided surface changes: typed entry tokens
404
- (`defineEntry<D extends JsonValue>`, token-first `tx.appendEntry()` and
405
- `tx.entry()` overloads); removal of `input` from `createConversation()`;
406
- built-in tasks pre-registered by `createRegistry()` (undisposable, not
407
- overridable, required by `Harness.open`); the `TaskRuntime` additions
408
- `snapshot`/`snapshotAsOf`, `context()`, `now()`, and `report()`; and the
409
- `streamOptions`/`retry` configuration fields with their getters and setters.
410
-
411
- Implement the §7.4 system prompt: rendering registered sections with `tag` and
412
- wrappers, and no-tool request preparation, including
413
- exact persisted rendered strings, positional system baselines/deltas with empty
414
- `content`, head-cut rebaselining with `ContextEdit` omissions, order-only
415
- two-entry rewrites, and section failure handling. No tools are offered in this
416
- package: preparation emits no tool declarations and passes `PromptInput.tools`
417
- as `[]`. Until Package 16, a `toolUse` response settles as the answer.
418
- Implement the ordinary generation phases needed for one response: preparation,
419
- request intent, durable throttled partials, attempts/retry classification,
420
- deferred handle polling/cancellation, assistant entry settlement, and input-
421
- submission completion.
422
-
423
- Call pi-ai only through its exported `Models` methods: `getModel()`,
424
- `streamSimple()`, `fetchDeferred()`, and `cancelDeferred()`. The test double must
425
- implement that same interface; production code gets no adapter. A missing configured model produces
426
- the durable `no_model` failure. All progress exposed to observers is committed
427
- state, never raw provider frames.
428
-
429
- Acceptance: `Harness.open → root → resume → submit(input) → Submission.wait →
430
- durable assistant answer → close/reopen`, using faux Models in tests. Exercise
431
- interruption and reopen before/after every implemented generation phase,
432
- aborted-partial conversion, deferred polling/cancellation, retryable and
433
- terminal model errors, no-visible-undurable updates, exact section order/value
434
- patches, complete post-head baselines, retained system deltas on both sides of a
435
- head marker, sections reading conversation documents through `input.read`,
436
- throwing sections, preparation
437
- rerun after a concurrent tail/config commit, fault and orphan turn cleanup,
438
- typed entry tokens, and durable submission settlement. This is also the first print-mode smoke path: print
439
- awaits its own input submission rather than global idle.
440
-
441
- ## 16. First coding-agent tool turn
442
-
443
- Implement hook dispatch and `TaskRuntime.hooks`, deferred from Package 14
444
- (Session-wide and scoped to a conversation or its owned subtree, §7.2), and wire
445
- the real generation → tool tasks → post-tools → generation chain (§8.3–8.5).
446
- Neither side uses a production fake successor.
447
-
448
- - Generation: tool declarations in preparation (§7.4: additions/removals,
449
- same-name replacement, order rewrite, complete baseline after a head cut,
450
- duplicate active names offered once, wrapped and failing wrappers,
451
- `PromptInput.tools`); `toolExecution` pinned in `request`/`poll`; the tool
452
- round commit (offered check against the committed context through `cutoff`,
453
- unaffected by `beforeRequest`; `tool_unavailable`
454
- results for calls not offered, parallel or `after`-chained sequential tool
455
- tasks, post-tools, `pi.live.tools`, run handed to post-tools);
456
- `beforeRequest`, `afterResponse`, and `onYield` continuations.
457
- - `pi.tool` (§8.4): input `{ assistant, callId }`, resolve/validate/
458
- `beforeTool`/validate/intent/execute/`afterTool`/result in one `call` handler,
459
- recovery-only `execute` with the stored/current replay rule, bounded output and
460
- details in the `pi.live.tools` slot, the terminal commit as final flush,
461
- diagnostics (`api.diagnostic()`, result diagnostics, Harness truncation and
462
- error codes, the `<harness>` block, `pi.tool-result` `data: { diagnostics }`),
463
- and the abort handler.
464
- - `pi.post-tools` (§8.5): `afterTools`, `addTools`, all-results `terminate`, the
465
- `postTools`/`final` boundaries (no inbox placement until Package 17), and the
466
- next generation. The `handoff` control moves to Package 17 with `reset()`,
467
- which defines the headed entry both write.
468
- - `pi.live.tools` and the checkpoint rule (§8.2); the scheduler cleanup hook for
469
- `pi.post-tools` (ends the run) and `pi.tool` (marks the slot `done`).
470
- - Surface: `ToolExecutionApi` without `conversation()`, which the `src` type
471
- drops until Package 18 adds it with `ConversationHandle`; `api.env` and `HarnessOptions.env`,
472
- `ToolRegistration.executionMode`, the `toolExecution` configuration field with
473
- its getter/setter, `TaskRuntime.getTask()` and `entry()`, `HookApi`, and the
474
- exported `GenerationTask`, `ToolTask`, and `PostToolsTask` tokens.
475
- - `@earendil-works/pi-durable/tools`: copy `read`, `bash`, `edit`, and `write`
476
- with their helpers from `packages/agent/src/harness/tools` and adapt them to
477
- `ToolRegistration` (`api.env`, `api.output`, `api.details`). Image reading in
478
- `read` is deferred; note it where the tool rejects or skips images. The env
479
- shell streams raw output (`onOutput`) and spills past thresholds; `output()` is
480
- the only place output is bounded, sanitized, and throttled. `prepareArguments`
481
- restores the edit tool's argument repair.
482
-
483
- Acceptance: input → model tool call → registered local read/bash/edit operation →
484
- tool result → model answer → durable submission settlement. Run that path once
485
- normally and once interrupted/reopened. Test recovery from every tool and post-
486
- tools phase; offered-set enforcement, including a tool deactivated after
487
- preparation still executing; before/after hook rules, hook scopes, and hook
488
- memos; `onYield` continuations; both stored/current replay-policy directions;
489
- default and overridden `outputLimits`; output content fallback; details
490
- replacement, last-details fallback, and coalesced commit settlement; the terminal
491
- commit as final flush; abort/close with buffered output; invocation-bound
492
- watches; parallel and sequential rounds, including a per-tool sequential mode;
493
- unregistered active tools that are removed from the offered set, re-added after
494
- re-registration, and produce `tool_unavailable` results when called, without
495
- failing the request; a tool replaced mid-call finishing under the implementation
496
- it resolved; hooks surviving a task reload; order-only tool changes; atomic
497
- assistant/tool/post-tools commits; `terminate` only when every result requests
498
- it; `addTools`; tool fault/orphan leaving a synthesized context result;
499
- diagnostic ordering, the `<harness>` block, and entry `data`; and all
500
- positional tool-history cases. Also test the ported tools against
501
- `NodeExecutionEnv`, and a wrapper supplying a different `api.env`.
502
-
503
- Document in the durable README that the package root loads TypeBox through the
504
- tool task's argument validation (pi-ai `validateToolArguments()`): about 23 MB of
505
- peak RSS unbundled, about 4 MB in a tree-shaken bundle.
506
-
507
- Tool output benchmark (`test/*.bench.ts`, memory, SQLite, and JSONL): drive the
508
- real tool task, adaptive throttle, and `pi.live` commits with low (a line every
509
- few seconds), normal (a compiler or test run), and high (continuous `cat` of a
510
- large file) output rates, head and tail retention, one tool and several parallel
511
- tools, over one long round. Report commits, operation bytes written, stored
512
- size before and after the round's base, reclamation, heap and RSS, commit
513
- latency, and reopen/replay time mid-round. Assert that head output commits as
514
- Chord appends and that a sliding tail of non-repetitive output within 64 KiB
515
- commits as trim plus append; report how often repetitive output falls back to a
516
- full window set. The checkpoint rule stays "base whenever nothing runs" (§8.2) with
517
- no delta-count bound; the benchmark decides whether that holds. Assign
518
- `slot.output` as one string field so Chord can diff it; replacing the slot
519
- object records a full set.
520
-
521
- Throughput target: a plain `cat` of a 1 GiB file of unique lines through the
522
- ported bash tool and the real Harness path (env capture and spill, `api.output`,
523
- throttled `pi.live` commits, result entry) completes in about 0.4 s, like the
524
- mini coding agent. Also drive `api.output()` directly with the same 1 GiB in
525
- 64 KiB chunks: accepting a chunk must not cost work proportional to the
526
- retained window.
527
-
528
- ## 17. Live UI and product state
529
-
530
- Complete submissions and the inbox (§6): the `pi.inbox` document created by the
531
- built-in setup; busy `steer`/`followUp`/`reject`; queued writes; admission to an
532
- idle conversation with a non-empty inbox (queue, then a final boundary in the
533
- same commit); withdrawal removing the item; `tx.placeSubmission()`; ordered
534
- `postTools`/`final` selection by the new `steeringMode`/`followUpMode`
535
- configuration fields (with getters/setters), writes before user items; stale
536
- head writes; a reset at `postTools` ending the run with `reset`; successor runs;
537
- and `onYield` applying only when the final boundary selected no user item and no
538
- reset. Only an answer, `terminate`, or `handoff` applies the final boundary;
539
- failure, a run task's abort, fault, and orphan leave the inbox alone.
540
- Successful inputs still require an answer; writes settle on placement and never
541
- start generation by themselves.
542
-
543
- Define the `pi.reset` entry (§8.1), written by `Conversation.reset(handoff)`
544
- through a write submission and by the post-tools `handoff` control.
545
-
546
- Usage (§8.6): the `pi.usage` document created by the built-in setup, updated in
547
- the same commit as every assistant entry (`appendAssistant()`) and every tool
548
- result with `usage` (`ToolExecutionResult.usage`, `appendToolResult()`), and
549
- `Harness.usage()` summing every conversation's document.
550
-
551
- `ConversationView`, `viewState()`, and `watch()` (§9.3): one mount per
552
- conversation, built lazily on the Session line, advanced from
553
- `subscribeCommits()` publications, and dropped with its last observer.
554
-
555
- Last: the experimental `watchEvents()` adapter (§9.4) with the snapshot event,
556
- translated message and tool deltas, and overflow-to-snapshot.
557
-
558
- Examples next to the existing ones in `test/examples/`: a print demo awaiting
559
- its own `Submission`, a JSON demo with two modes, `--events` (adapter
560
- events as JSON lines) and `--ops` (raw `ConversationView` frames), an inbox
561
- demo, and a late-join demo attaching a view and an event stream mid-run. An
562
- interactive TUI demo and coding-agent integration are later work.
563
-
564
- Table-test every submission transition, cross-type request-ID conflicts,
565
- interleaved queue selection under both queue modes, writes placed before user
566
- items, stale head writes, a reset at each boundary, handoff, `onYield` with and
567
- without queued items, compact positional removal of large payloads, withdrawal,
568
- reopen waits, queued items surviving a failed run and drained by the next
569
- submission, and orphan/fault cleanup. Test minimal Chord deltas for `pi.inbox`
570
- and `pi.usage` changes. Test one view revision per touching commit; atomic
571
- entry/document publication; fork-aware active entries and head cuts; mounted
572
- create/retire; empty-batch suppression; stable paths; structural sharing; watch
573
- overflow; and dropping the mount. Test the event translation of every
574
- `MessageChange` and output change, the snapshot at attachment and after
575
- overflow, and absence of raw provider frames.
576
-
577
- ## 18. Ownership and subagents
578
-
579
- Complete the remaining owned-conversation and abort semantics: durable foreground
580
- subtree cascades, signal/join/fresh-abort across ownership edges, background
581
- boundaries, ordinary and full traversal, conversation abort, and exact idle
582
- waits. Finish invocation-bound owned APIs used by tools and the foreground and
583
- background subagent provisioning patterns, including atomic task/conversation/
584
- registry creation and request-ID-safe submission recovery.
585
-
586
- An abort cascade withdraws queued inputs in every owned conversation it reaches
587
- (spec §5.4). There is no full-teardown primitive: `close()` stops everything
588
- without outcomes, and a host cancels everything by aborting what `inspect()`
589
- lists. No built-in subagent tool or supervisor task: two concise, product-style
590
- examples, `test/examples/22-subagent-foreground.ts` (child owned by the tool task,
591
- reported through `api.details({ conversationId })`, the UI attaching to the
592
- child's events) and `test/examples/23-subagent-background.ts` (reworked in
593
- Package 19: persistent subagents behind one `subagent` tool with spawn, send,
594
- wait, stop, and status actions; a background anchor task owns each child, and a
595
- background reporter task per message delivers it and posts the answer back as a
596
- follow-up input, request-ID-safe across restarts), show the patterns.
597
-
598
- Test deep ownership trees, owner edges after terminal settlement, nested
599
- background boundaries, conversation abort/join with surviving passive writes and
600
- background tasks, cancellation of waiters without cancellation of work, and
601
- atomic cancellation intent. Test default non-inheritance, inheritance from the
602
- current committed tail, empty source conversations, document fork policies,
603
- configuration overrides through `init`, foreground subagent cascade, and
604
- background supervisor recovery before and after submission admission.
605
-
606
- ## 19. Structured concurrency
607
-
608
- Implement spec §5.5 and its consequences, replacing `after` and `pi.post-tools`.
609
- No backward compatibility.
610
-
611
- - Ownership: `TaskOptions.ownership` is required (`{ kind: "conversation" }` or
612
- `{ kind: "task", taskId }`); `TaskRecord.owner?: TaskId`; child tasks live in
613
- their owner's conversation; `background` only for conversation-owned tasks;
614
- new task- or conversation-ownership requires a live owner (not `completing`
615
- or `terminal`). Update every `createTask` call site, including tools'
616
- `api.createTask`, examples, and tests. Storage: persist `owner` and the new
617
- statuses (memory, JSONL, SQLite schema edited in place; no migrations while WIP), conformance cases. No owner
618
- index: the scheduler derives edges from the live tasks it loads.
619
- - States: real `waiting { checkpoint, on, policy }` and `completing { outcome }`
620
- statuses in `TaskState` and every backend and status scan; `NextTaskState`
621
- gains `waiting`; `runtime.outcomes()`; `inspect()` reports `completing`.
622
- Remove `after` (records, options, scheduler dependency handling).
623
- - Scheduler: a waiting task is never reserved until every task in `on` is
624
- terminal (then reserved directly from `waiting` to `running` at its
625
- checkpoint), unless an abort mark lets it reach its abort handler; `on` over
626
- tasks it does not own requires `allSettled`; `on` rejects missing tasks, the
627
- task itself, and its owner chain; failFast marks live owned siblings in `on` on a held or terminal
628
- non-`completed` outcome, in reconcile. Terminal commits (task- and
629
- scheduler-written) with live ordinary owned work become `completing`; a later
630
- scheduler commit finalizes them when that work is gone, re-evaluated on every
631
- commit and at open; scheduler-written holds defer their Harness cleanup
632
- (`settleSchedulerOutcome`) to the final commit, task-written holds land their
633
- other writes at hold; waiters and task-document retirement at the final
634
- commit. Abort invocations start only when ordinary owned work is gone
635
- (bottom-up, judged on committed records). Orphaning happens only where the
636
- abort invocation would start, or directly in `abortTask()` when nothing owned
637
- is live, so orphans never hold. Cascade (spec §5.4): only live owners cascade (abort mark or held
638
- non-`completed` outcome); terminal owners never do; ordinary traversal and
639
- idle follow task→task edges too. `Conversation.abort(context, { background:
640
- true })` snapshot semantics.
641
- - Built-ins: generation owns its tool tasks and waits `allSettled` in a new
642
- `tools` phase that runs the old post-tools body (§8.5); sequential rounds
643
- create one tool at a time from `pending`; the next generation is
644
- conversation-owned; the generation abort handler appends `aborted` results for
645
- unstarted calls. Remove `pi.post-tools`, `PostToolsTask`, `PostToolsHooks`
646
- (`afterTools` moves to `GenerationHooks`). Tool tasks that own conversations
647
- hold `completing` after their result entry. Events: `turn_end` when a
648
- generation's outcome is committed (hold or terminal, whichever first).
649
- - Docs: spec §5.5 and §12 footguns are written; update README (task ownership,
650
- waiting, `completing`, abort order), CHANGELOG, and examples 12/22/23 plus a new
651
- `test/examples/24-child-tasks.ts` (checkout with four payments: failFast,
652
- `abortTask(checkout)`, crash and reopen, each printing outcomes).
653
-
654
- Exhaustive tests (new `test/harness-structured.test.ts` plus updates): ownership
655
- validation (missing ownership, cross-conversation child, background child,
656
- owner completing/terminal); waiting with allSettled and failFast (failed,
657
- aborted, faulted, orphaned children; held failures trigger failFast before the
658
- failing child drains); `on` with already-terminal and non-owned tasks
659
- (`allSettled` only); waiting on subsets in sequence; spawning without waiting;
660
- `completing` for task- and scheduler-written outcomes, including work created
661
- during the hold, finalization at open, `abortTask()` on a completing task, held
662
- failure aborting below, waiters and document retirement only at the final
663
- commit; bottom-up abort order across three levels (child terminal records before
664
- the parent's abort handler starts), including an aborted waiting parent; `on`
665
- validation (missing, self, owner chain, empty `on`); a waiting task whose
666
- definition is missing when it may resume (blocked, then orphaned when aborted);
667
- creating owned work in a finishing commit (task ownership rejects, a task in an
668
- owned conversation holds); cascades through
669
- task→task and task→conversation→task edges; background boundaries and the
670
- `{ background: true }` abort; a terminal owner never cascading (interrogating a
671
- finished or Esc'd subagent runs normally); crash/reopen at every point (before
672
- and after create+wait, after a child fails before siblings are marked, after all
673
- children terminal before the parent resumes, during holds and abort handlers);
674
- generation tool rounds parallel and sequential with Esc before, during, and
675
- after tools, unstarted sequential calls, faulted tool slots under generation
676
- abort, faulted generation keeping run control until its tools drain, `turn_end`
677
- before the successor's `turn_start` when a generation holds, and a tool holding
678
- `completing` while its subagent's extension work runs. Rerun every existing suite; migrate the post-tools,
679
- sequential-round, and dependency tests to the new shapes without weakening them.
680
-
681
- ## 20. Compaction and overflow
682
-
683
- Implement spec §8.7 and its consequences. Compaction never appends to a busy
684
- conversation concurrently (§7.4 preparation does not recheck the transcript): a
685
- blocking compaction appends while its generation holds the run and waits for it;
686
- every other compaction places its summary through a write submission.
687
-
688
- - Task: `pi.compaction` (`CompactionTask`, `CompactionHooks`, `CompactionInput`,
689
- `CompactionCheckpoint`, `CompactionResult`, `CompactionReason`) with phases
690
- `select`, `summarize`, `retry`, the abort handler, and the `beforeCompact`
691
- hook. Register it with the built-ins. `CompactionEntry` token for
692
- `pi.compaction` with `data: { reason }`.
693
- - `ContextView.contributions`: each active entry's contribution after all
694
- in-range edits (including edits on older in-range markers). Range selection is one pure function over those
695
- contributions and `keepRecentTokens`, shared by the generation's checks and
696
- `select`; the summarized messages are the prefix contributions ordered by
697
- §2.1 rules 7–8. The generation's size estimate (§8.3) uses the newest
698
- assistant appended after the head marker, not pi-ai's timestamp heuristic. Port the coding agent's serializer, summarization
699
- system prompt, and structured prompt (one prompt, with a line about carrying an
700
- earlier summary forward) into `src/harness/compaction.ts`; do not import from
701
- coding-agent.
702
- - Placement: direct append for a task-owned (blocking) compaction; for a
703
- conversation-owned one, factor the admission body of `Submissions.submit()` into
704
- a function over a `Tx` so the task's classifying commit admits the write
705
- (request ID `compaction:<taskId>`) with the same idle/queued/stale/final-boundary
706
- behavior. No new stale logic.
707
- - Config: `CompactionPolicy`, `DEFAULT_COMPACTION_POLICY`,
708
- `ConversationConfigState.compaction`, `getCompaction`/`setCompaction`.
709
- - Live: `pi.live.compactions` (`CompactionStatus[]`, task ID order, key removed
710
- when empty), added in the creating commit and removed in the outcome commit
711
- (also at a `completing` hold);
712
- `settleSchedulerOutcome` removes it for faulted/orphaned compactions.
713
- - Generation: `compacted`/`overflow` checkpoint fields; threshold checks in
714
- `prepare` (blocking child + wait; background conversation-owned task in the
715
- commit moving to `request`); overflow classification before the retry branch;
716
- failing an overflow generation whose compaction produced no entry.
717
- - `Conversation.compact()` admits the manual task (conversation-owned, not
718
- background) with its status in one commit and returns its ID.
719
- - Usage: every classified summarization response adds its usage to
720
- `pi.usage.models`.
721
- - Events: `compaction_start`/`compaction_end`, snapshot `compactions`, batch
722
- order per §9.4.
723
- - Docs: README (compaction, policy, events), CHANGELOG, and a new product-style
724
- example `test/examples/25-compaction.ts` (faux model: a long chat that crosses
725
- the background threshold, the summary lands at a boundary, a manual `compact()`
726
- while busy, and an overflow recovered by a blocking compaction, printing the
727
- model context before and after).
728
-
729
- Not in v1 (note only): a cache-friendly summary request that resends the
730
- agent's exact last request plus one summarization user message with pi-ai
731
- `toolChoice: "none"`, so the prefix including tool declarations is a cache hit.
732
- It fits background compaction well below the window; overflow and a context
733
- without room for the prompt and summary keep the serialized request.
734
-
735
- Exhaustive tests (new `test/harness-compaction.test.ts`, faux provider, plus
736
- updates):
737
-
738
- - Range selection (pure): cut at a user entry, at an assistant entry mid-run
739
- (one prompt followed by many tool rounds), never at a tool result or system
740
- entry; a huge last tool result keeps its assistant; budget never reached and
741
- only-the-marker prefix are nothing to compact; an existing marker is
742
- summarized first and older in-range markers drop out; omitted and replaced
743
- entries use their edited contribution; excluded error/aborted assistants are
744
- not candidates; the §8.7 worked example.
745
- - Summarization request: exact messages (system prompt, serialized
746
- conversation, instructions line), tool-result truncation, no tools,
747
- `cacheRetention: "none"`, no `deferred`, `maxTokens` from `reserveTokens` and
748
- the model's max, pinned model/thinking/stream options surviving a config change
749
- mid-summary.
750
- - Summary entry: kind, wrapped text, `head` = first kept, `data.reason`; model
751
- context after placement is `[summary, kept...]` followed by a full system
752
- baseline on the next preparation; raw history unchanged and still scannable via
753
- `entries()`; `pi.usage.models` includes every attempt, including failed,
754
- retried, and stale ones; declines and hook-supplied summaries add none.
755
- - Outcomes: nothing to compact (`{}`), `beforeCompact` decline (`{}`) and
756
- supplied summary (no model call), first decision wins, hook throw reported and
757
- ignored; `no_model`; retryable error then success; retries exhausted;
758
- non-retryable error; `length` stop; tool call in the response; empty text.
759
- - Manual: idle with empty inbox (placed at once, `{ submissionId }` whose submission is `done`);
760
- idle with queued follow-ups after a failed run (final boundary places the
761
- summary first, then starts the follow-up in the compacted context); busy
762
- (queued, placed at the next `postTools` and the run continues in the compacted
763
- context; placed at `final`); stale because a reset landed while summarizing;
764
- stale because a later compaction cut further; an older summary with a later cut
765
- placed after a newer one; two queued summaries in one boundary with the
766
- second cutting before, at, and after the first (only "before" is stale); `Conversation.abort()` aborts a manual compaction and keeps an
767
- already queued summary; `Submission.abort()` withdraws a queued summary;
768
- `waitForIdle` waits for a manual compaction; `compact()` returns the ID
769
- `waitForTask` settles with.
770
- - Background threshold: starts only above the background threshold, only with
771
- `backgroundTokens > 0`, only when no status is listed, not when disabled, not
772
- without a cut, and not after a blocking compaction in the same generation; the
773
- generation does not wait; summary placed at the next boundary; survives
774
- `Conversation.abort()`; `abort({ background: true })` and `abortTask()` stop
775
- it; idle waits ignore it.
776
- - Blocking threshold: generation waits, nothing appended before the wait, then
777
- re-prepares with a baseline and sends; request proceeds after nothing to
778
- compact, decline, failure, fault, and direct `abortTask()` of the child; no
779
- second compaction when still above the threshold; a background compaction in
780
- flight ends stale; Esc during the blocking compaction aborts child then
781
- generation (child terminal before the generation's abort handler) and settles
782
- inputs `aborted`.
783
- - Overflow: error entry appended and excluded from the retry request, attempt
784
- unchanged, compaction then retry succeeds; second overflow fails
785
- `model_error` with its error entry; overflow with compaction disabled, without
786
- a cut, or after a threshold compaction fails; overflow whose compaction
787
- declines/fails/aborts fails `model_error` with the overflow text; an overflow
788
- error matching retryable patterns is still not retried; silent `stop`/`length`
789
- overflow is an ordinary answer.
790
- - Live status and events: status added in the creating commit with `blocking`,
791
- `attempt`, and `retry` during backoff, removed with every outcome
792
- (completed, failed, aborted, faulted, orphaned); several concurrent statuses;
793
- `compaction_start`/`compaction_end` in batch order; snapshot `compactions` for a late joiner during summarize and retry;
794
- view mount after a compaction head (kept entries stay mounted).
795
- - Recovery: crash and reopen in `select` (hook reruns), `summarize` (request
796
- resent once, no duplicate usage), `retry`, after placement, with the
797
- generation waiting on a blocking compaction, with a summary queued, and with a
798
- blocked compaction definition (orphaned on abort, status removed).
799
- - Estimate and cut edge cases: a background summary queued mid-run and placed
800
- at `postTools` makes the successor's estimate ignore the pre-compaction usage,
801
- so no blocking compaction starts (also with a fixed Harness clock); an
802
- assistant finishing while a summary is queued; raw order `assistant(call),
803
- user, toolResult, assistant` never cuts at that user; edits carried by an
804
- older in-range marker and a kept entry replacing a summarized one are reflected
805
- in the summarized messages and the hook's `messages`; kept `pi.system` deltas
806
- get omit edits in the next single baseline; compaction in a fork whose cut
807
- falls on a parent entry (head, stale checks, view mount).
808
- - Interactions: a summary queued before a reset in the same boundary (both
809
- placed, reset wins) and after it (stale); a manual compaction's status blocks
810
- a background start; a queued summary surviving a failed run is placed by the
811
- next submission's final boundary before the new input; the summarization
812
- request itself overflowing fails `model_error` and a waiting overflow
813
- generation fails with the original overflow text; a retryable error after an
814
- overflow compaction has the full retry budget; a background summary landing
815
- during a retry backoff stays queued while a blocking compaction wins; an
816
- application edit placed while summarizing is lost as §12 documents; a hook
817
- that creates owned work and supplies a summary (placement and status removal
818
- at hold, receipt after the child); `compact()` right after open enables
819
- scheduling; a rejected classifying commit leaves no usage, submission,
820
- summary, or outcome; idle admission that drains an older queued summary
821
- together with the current one.
822
- - Rerun every existing suite; generation, inbox, events, and view tests keep
823
- passing unchanged except for the new optional fields.
824
-
825
- ## 21. Reload and final conformance
826
-
827
- Complete any remaining §2.2 surface and lifecycle gates and the §7.5 in-process
828
- reload path through the registry: batch replacement while the Harness keeps
829
- running, pinned in-flight work, and task handover at phase boundaries.
830
- Close/reopen is not required for reload.
831
-
832
- Test that close seals commit and mutation admission, lets already-admitted
833
- storage settlement finish despite caller cancellation, stops future state/watch
834
- delivery, joins task/tool/hook invocations outside the Session line, writes no
835
- abort or terminal outcome, starts no fresh abort invocation, and never runs old
836
- and new Harness generations concurrently. Include cancellation during watch
837
- acquisition and an already-running callback that remains caller-owned across
838
- shutdown. Verify service withdrawal and client detach.
839
-
840
- Run the exhaustive public conformance matrix: stable persisted root identity;
841
- all root/create/lookup/fork/reset/compact/abort/idle paths; every configuration
842
- getter/setter and fork override; typed input/write submissions; task wait/abort;
843
- generic document access; registry changes before open, between open and resume,
844
- and while work runs; structural watches; and blocked tasks surviving open. Compile-test every §2.2 and §3 owner/key/seed overload,
845
- the normative usage sequences, and the Chord guide. The erased registry test must
846
- use a concrete narrowed-input task with multiple checkpoint phases and custom
847
- hooks. Verify that a Chord root-replacement delta remains distinct from a
848
- Session-selected storage checkpoint.
849
-
850
- Run all package-specific tests and the repository check. Finish with a local
851
- coding-agent turn and a reopened interrupted turn through the public Harness,
852
- then stop for final review.
853
-
854
- ## 22. Task graph view
855
-
856
- A live, observable view of the Session's task graph for UIs and debugging, like
857
- `ConversationView` for a conversation: every live task with its owner edge
858
- (conversation or task), status (`running`, `waiting on …`, `completing`),
859
- background flag, and owned conversations, published after each commit as a
860
- replicated state or watch. `inspect()` already provides a one-off snapshot of
861
- the live task records. Specify and build it after the final conformance package.