my-durable 0.0.0-stage → 0.0.2

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 (259) hide show
  1. package/.env.example +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +328 -2
  4. package/ai_docs/durable_pi/announcement.md +860 -0
  5. package/ai_docs/durable_pi/changelog.md +51 -0
  6. package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
  7. package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
  8. package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
  9. package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
  10. package/ai_docs/durable_pi/readme.md +617 -0
  11. package/ai_docs/durable_pi/sources.md +22 -0
  12. package/ai_docs/durable_pi/spec.md +4696 -0
  13. package/ai_docs/durable_pi/vacation-planner.md +57 -0
  14. package/ai_docs/pi/chord.md +268 -0
  15. package/ai_docs/pi/cli-integration.md +110 -0
  16. package/ai_docs/pi/cli.md +325 -0
  17. package/ai_docs/pi/codemode.md +211 -0
  18. package/ai_docs/pi/compaction.md +467 -0
  19. package/ai_docs/pi/configuration.md +51 -0
  20. package/ai_docs/pi/containerization.md +187 -0
  21. package/ai_docs/pi/custom-provider.md +177 -0
  22. package/ai_docs/pi/environment-variables.md +102 -0
  23. package/ai_docs/pi/extensions.md +278 -0
  24. package/ai_docs/pi/how-pi-works.md +53 -0
  25. package/ai_docs/pi/index.md +43 -0
  26. package/ai_docs/pi/json.md +230 -0
  27. package/ai_docs/pi/keybindings.md +198 -0
  28. package/ai_docs/pi/llama-cpp.md +118 -0
  29. package/ai_docs/pi/mcp.md +274 -0
  30. package/ai_docs/pi/message-types.md +265 -0
  31. package/ai_docs/pi/models.md +209 -0
  32. package/ai_docs/pi/overview.md +166 -0
  33. package/ai_docs/pi/packages.md +133 -0
  34. package/ai_docs/pi/pi-agent-core.md +568 -0
  35. package/ai_docs/pi/pi-ai.md +1942 -0
  36. package/ai_docs/pi/prompt-templates.md +63 -0
  37. package/ai_docs/pi/providers.md +204 -0
  38. package/ai_docs/pi/quickstart.md +140 -0
  39. package/ai_docs/pi/readme.md +99 -0
  40. package/ai_docs/pi/rpc-commands.md +864 -0
  41. package/ai_docs/pi/rpc-extension-ui.md +204 -0
  42. package/ai_docs/pi/rpc.md +197 -0
  43. package/ai_docs/pi/sdk.md +151 -0
  44. package/ai_docs/pi/security.md +103 -0
  45. package/ai_docs/pi/session-format.md +297 -0
  46. package/ai_docs/pi/sessions.md +70 -0
  47. package/ai_docs/pi/settings.md +173 -0
  48. package/ai_docs/pi/shell-aliases.md +97 -0
  49. package/ai_docs/pi/skills.md +97 -0
  50. package/ai_docs/pi/slash-commands.md +64 -0
  51. package/ai_docs/pi/sources.md +57 -0
  52. package/ai_docs/pi/terminal-setup.md +223 -0
  53. package/ai_docs/pi/termux.md +121 -0
  54. package/ai_docs/pi/themes.md +140 -0
  55. package/ai_docs/pi/tmux.md +59 -0
  56. package/ai_docs/pi/toolkit.md +168 -0
  57. package/ai_docs/pi/tui.md +129 -0
  58. package/ai_docs/pi/usage.md +98 -0
  59. package/ai_docs/pi/virtual-models.md +118 -0
  60. package/ai_docs/pi/windows.md +71 -0
  61. package/apps/dpi-backend/.nvmrc +1 -0
  62. package/apps/dpi-backend/API.md +99 -0
  63. package/apps/dpi-backend/README.md +119 -0
  64. package/apps/dpi-backend/deploy/backend.env.example +9 -0
  65. package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
  66. package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
  67. package/apps/dpi-backend/package.json +33 -0
  68. package/apps/dpi-backend/prompts/one_sentence.md +3 -0
  69. package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
  70. package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
  71. package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
  72. package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
  73. package/apps/dpi-backend/scripts/token.ts +57 -0
  74. package/apps/dpi-backend/src/addons.ts +139 -0
  75. package/apps/dpi-backend/src/auth.ts +20 -0
  76. package/apps/dpi-backend/src/config.ts +54 -0
  77. package/apps/dpi-backend/src/contract-check.ts +118 -0
  78. package/apps/dpi-backend/src/errors.ts +50 -0
  79. package/apps/dpi-backend/src/extensions.ts +510 -0
  80. package/apps/dpi-backend/src/faux.ts +80 -0
  81. package/apps/dpi-backend/src/history.ts +165 -0
  82. package/apps/dpi-backend/src/idempotency.ts +20 -0
  83. package/apps/dpi-backend/src/jev.ts +43 -0
  84. package/apps/dpi-backend/src/jobs.ts +790 -0
  85. package/apps/dpi-backend/src/main.ts +76 -0
  86. package/apps/dpi-backend/src/model-catalog.ts +180 -0
  87. package/apps/dpi-backend/src/pairing-files.ts +142 -0
  88. package/apps/dpi-backend/src/pairing.ts +459 -0
  89. package/apps/dpi-backend/src/routes.ts +217 -0
  90. package/apps/dpi-backend/src/shared-state.ts +85 -0
  91. package/apps/dpi-backend/src/store.ts +1240 -0
  92. package/apps/dpi-backend/src/streams.ts +256 -0
  93. package/apps/dpi-backend/src/tunnel.ts +955 -0
  94. package/apps/dpi-backend/tsconfig.json +13 -0
  95. package/apps/dpi-playground/README.md +83 -0
  96. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
  97. package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
  98. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
  99. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
  100. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
  101. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
  102. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
  103. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
  104. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
  105. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
  106. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
  107. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
  108. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
  109. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
  110. package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
  111. package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
  112. package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
  113. package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
  114. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
  115. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
  116. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
  117. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
  118. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
  119. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
  120. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
  121. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
  122. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
  123. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
  124. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
  125. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
  126. package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
  127. package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
  128. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
  129. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
  130. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
  131. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
  132. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
  133. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
  134. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
  135. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
  136. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
  137. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
  138. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
  139. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
  140. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
  141. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
  142. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
  143. package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
  144. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
  145. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
  146. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
  147. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
  148. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
  149. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
  150. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
  151. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
  152. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
  153. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
  154. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
  155. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
  156. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
  157. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
  158. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
  159. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
  160. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
  161. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
  162. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
  163. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
  164. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
  165. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
  166. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
  167. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
  168. package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
  169. package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
  170. package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
  171. package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
  172. package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
  173. package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
  174. package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
  175. package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
  176. package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
  177. package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
  178. package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
  179. package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
  180. package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
  181. package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
  182. package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
  183. package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
  184. package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
  185. package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
  186. package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
  187. package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
  188. package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
  189. package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
  190. package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
  191. package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
  192. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
  193. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
  194. package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
  195. package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
  196. package/apps/dpi-playground/package.json +37 -0
  197. package/apps/dpi-sdk/README.md +74 -0
  198. package/apps/dpi-sdk/package.json +20 -0
  199. package/apps/dpi-sdk/scripts/net.ts +73 -0
  200. package/apps/dpi-sdk/src/api.ts +631 -0
  201. package/apps/dpi-sdk/src/app-state.ts +111 -0
  202. package/apps/dpi-sdk/src/client.ts +330 -0
  203. package/apps/dpi-sdk/src/errors.ts +16 -0
  204. package/apps/dpi-sdk/src/jobs.ts +99 -0
  205. package/apps/dpi-sdk/src/models.ts +472 -0
  206. package/apps/dpi-sdk/src/node/chunk.ts +488 -0
  207. package/apps/dpi-sdk/src/node/config.ts +175 -0
  208. package/apps/dpi-sdk/src/node/drivers.ts +402 -0
  209. package/apps/dpi-sdk/src/node/ops.ts +114 -0
  210. package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
  211. package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
  212. package/apps/dpi-sdk/src/node/registry.ts +390 -0
  213. package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
  214. package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
  215. package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
  216. package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
  217. package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
  218. package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
  219. package/apps/dpi-sdk/src/sse.ts +117 -0
  220. package/apps/dpi-sdk/src/transcript.ts +291 -0
  221. package/apps/dpi-sdk/tsconfig.json +13 -0
  222. package/apps/dpi-sdk/w +20 -0
  223. package/apps/dpi-tui/README.md +54 -0
  224. package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
  225. package/apps/dpi-tui/package.json +20 -0
  226. package/apps/dpi-tui/src/app.ts +433 -0
  227. package/apps/dpi-tui/src/commands.ts +151 -0
  228. package/apps/dpi-tui/src/main.ts +122 -0
  229. package/apps/dpi-tui/src/pairing.ts +287 -0
  230. package/apps/dpi-tui/src/pickers.ts +238 -0
  231. package/apps/dpi-tui/src/render.ts +164 -0
  232. package/apps/dpi-tui/src/theme.ts +57 -0
  233. package/apps/dpi-tui/tsconfig.json +14 -0
  234. package/bin/device.mjs +5 -0
  235. package/bin/hub.mjs +5 -0
  236. package/bin/tui.mjs +5 -0
  237. package/deploy/Caddyfile +120 -0
  238. package/docs/verification.md +102 -0
  239. package/dpi-network.example.yaml +62 -0
  240. package/images/01_durable_agent_network.svg +185 -0
  241. package/images/02_pi_made_durable.svg +204 -0
  242. package/images/03_restart_mid_tool.svg +168 -0
  243. package/images/07_one_api_every_client.svg +123 -0
  244. package/images/08_device_session_agent.svg +123 -0
  245. package/images/09_compact_keep_history.svg +143 -0
  246. package/images/10_shared_app_state.svg +177 -0
  247. package/images/11_durable_jobs_wait.svg +141 -0
  248. package/images/12_tools_change_mid_run.svg +231 -0
  249. package/images/13_hook_blocks_tool.svg +122 -0
  250. package/images/14_fork_at_any_entry.svg +119 -0
  251. package/images/15_web_client.png +0 -0
  252. package/images/16_terminal_client.svg +43 -0
  253. package/images/17_devices_are_data.svg +182 -0
  254. package/images/18_should_every_agent_be_durable.svg +118 -0
  255. package/images/19_sqlite_on_every_device.svg +155 -0
  256. package/images/20_admission_and_effects.svg +134 -0
  257. package/justfile +33 -0
  258. package/package.json +41 -4
  259. package/scripts/verify.mjs +210 -0
@@ -0,0 +1,391 @@
1
+ > Source: [packages/durable/docs/pico-v5-chord-usage.md](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/durable/docs/pico-v5-chord-usage.md)
2
+ > Retrieved with Firecrawl: 2026-10-05. Repository snapshot: `b78e6a9085343ec0f308c3d377da72528b4cf7ee`.
3
+ > Relative documentation links point to local copies where available, otherwise to the source snapshot.
4
+
5
+ # Pico5 documents through Chord
6
+
7
+ This guide uses the contracts in the [Pico5 specification](spec.md). Its code
8
+ compiles and runs as `test/chord-guide.test.ts`; keep the two in sync.
9
+
10
+ - **Session:** a durable container for conversations, entries, tasks, and documents;
11
+ it serializes mutations on one commit line.
12
+ - **Facet:** a feature's setup unit in a Chord host. Setup declares services and
13
+ dependencies synchronously; `onActivate` runs after dependencies are ready.
14
+ - **Context:** explicitly passed cancellation and invocation values. Forward the caller's context.
15
+ - **Service:** a typed token and implementation acquired with `env.use`. Remote
16
+ services contain replicated state and async methods with JSON arguments plus a trailing `Context`.
17
+ - **Durable document:** JSON persisted as a complete base, Chord operations, and
18
+ checkpoints. Its definition token declares schema, version, scope, and initialization;
19
+ conversation scope also declares history and fork behavior.
20
+ - **Transaction draft:** the revocable copy-on-write object from `await tx.doc`,
21
+ valid only inside that commit callback, including all nested objects.
22
+ - **DocumentState:** a disposable read-only Chord state bound to one document incarnation.
23
+ - **ReplicatedState:** Chord's immutable complete values through `value` and
24
+ `subscribe(listener)`. `value` is `undefined` before hydration or while disconnected.
25
+ The listener receives `(value, context, delivery)`; delivery contains `kind`
26
+ (`hydrate` or `update`) and a stream `sequence`, not a Session commit number.
27
+
28
+ Documents are scoped directly to a Session, conversation, or task. A task is
29
+ durable work attached to a conversation; its documents retire when it becomes
30
+ terminal and never participate in conversation forks.
31
+
32
+ ## Imports and the adapter boundary
33
+
34
+ Examples build on one another. `ConversationRecord`, `EntryRecord`, and
35
+ `TaskRecord` are persisted records, while `Conversation` is the public
36
+ conversation object and `Entry`/`Task` are typed definitions. The imports:
37
+
38
+ ```ts
39
+ import {
40
+ createFacetHost, createRemoteServiceBinding, defineFacet, defineService,
41
+ type Context, type Facet, type FacetHost, type RemoteServiceTransport, type ReplicatedState,
42
+ } from "@earendil-works/chord";
43
+ import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
44
+ import {
45
+ type ConversationId, defineDoc, defineDocFamily, type DocumentObserver,
46
+ type Harness, type Session, type TaskId, type TaskRuntime,
47
+ } from "@earendil-works/pi-durable";
48
+ ```
49
+
50
+ A `Harness` is a `Session`, so every function below also takes an open Harness.
51
+
52
+ `documentState()` performs Chord adoption internally: it atomically captures a
53
+ committed snapshot, registers for every later exact frame, and returns an already
54
+ hydrated read-only state. Operations covered by the snapshot are never
55
+ redelivered. Disposing the state releases observation, not the document. Pico
56
+ remains the sole mutator.
57
+
58
+ ## 1. A Session-wide canvas
59
+
60
+ All conversations in this Session share one canvas. Session documents are
61
+ current-only and have no history or fork settings.
62
+
63
+ ```ts
64
+ type Stroke = { color: string; points: { x: number; y: number }[] };
65
+ type CanvasState = { strokes: Stroke[] };
66
+
67
+ const CanvasDoc = defineDoc<CanvasState>({
68
+ kind: "app.canvas", version: 1, scope: "session",
69
+ initial: () => ({ strokes: [] }),
70
+ // Store a complete base after at most 99 replayed deltas.
71
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 99,
72
+ });
73
+
74
+ interface CanvasService {
75
+ readonly state: ReplicatedState<CanvasState | null>;
76
+ addStroke(stroke: Stroke, context: Context): Promise<void>;
77
+ }
78
+ const Canvas = defineService<CanvasService>("app.canvas");
79
+
80
+ async function createCanvasFacet(session: Session, context: Context): Promise<Facet> {
81
+ // Creation is explicit; observation never writes.
82
+ await session.commit(async tx => {
83
+ await tx.doc(CanvasDoc);
84
+ }, context);
85
+ const state = await session.documentState(CanvasDoc, context);
86
+ if (state === undefined) throw new Error("canvas was retired during setup");
87
+ return defineFacet({
88
+ id: "app.canvas/session",
89
+ setup(env) {
90
+ env.own(() => state.dispose());
91
+ env.provide(Canvas, {
92
+ state,
93
+ async addStroke(stroke, context) {
94
+ await session.commit(async tx => {
95
+ const draft = await tx.doc(CanvasDoc);
96
+ draft.strokes.push(stroke); // Chord copies the assigned stroke by value.
97
+ }, context);
98
+ },
99
+ });
100
+ },
101
+ });
102
+ }
103
+
104
+ const CanvasConsumer = defineFacet({
105
+ id: "app.canvas/consumer",
106
+ setup(env) {
107
+ const canvas = env.use(Canvas); // Declare now; access only after activation.
108
+ env.onActivate(() => {
109
+ env.own(canvas.state.subscribe((value, _context, delivery) => {
110
+ console.log(delivery.kind, delivery.sequence, value?.strokes.length ?? "retired");
111
+ })); // subscribe delivers the current hydrated value, then updates.
112
+ });
113
+ },
114
+ });
115
+
116
+ async function runCanvasExample(session: Session): Promise<void> {
117
+ const context = BACKGROUND_CONTEXT;
118
+ const provider = await createCanvasFacet(session, context);
119
+ const host = await createFacetHost({ facets: [provider, CanvasConsumer] });
120
+ try {
121
+ await host.services.use(Canvas).addStroke({
122
+ color: "black", points: [{ x: 10, y: 20 }, { x: 30, y: 40 }],
123
+ }, context);
124
+ } finally {
125
+ await host.dispose(); // Unsubscribes; does not delete the canvas or close Session.
126
+ }
127
+ }
128
+ ```
129
+
130
+ The application owns the open Session. Acquire the document state asynchronously
131
+ before synchronous `setup`, then transfer its disposal to the facet;
132
+ `env.provide` cannot run in `onActivate`. Install one provider per
133
+ Session host. Chord may reload presentation facets independently. Session-side
134
+ tools, hooks, tasks, and prompt sections reload through the Harness
135
+ registry: install the replacement extension under the same name (spec section 7.5).
136
+ Work already running keeps the old code, so the facet manages its old resources'
137
+ lifetime itself.
138
+
139
+ Remote clients supply a `RemoteServiceTransport` connected to the host's `services`
140
+ provider; Chord prescribes no socket protocol. `CanvasConsumer` also works unchanged
141
+ in a UI host configured with a remote service source.
142
+
143
+ ```ts
144
+ async function connectCanvas(transport: RemoteServiceTransport, context: Context) {
145
+ const services = createRemoteServiceBinding({ services: [Canvas], transport });
146
+ const canvas = services.use(Canvas);
147
+ const stop = canvas.state.subscribe((value, _context, delivery) => {
148
+ console.log(delivery.kind, delivery.sequence, value?.strokes ?? "retired");
149
+ });
150
+ try {
151
+ await services.ready(context); // Initial snapshot installed; not all future updates.
152
+ } catch (error) {
153
+ stop();
154
+ await services.dispose(BACKGROUND_CONTEXT);
155
+ throw error;
156
+ }
157
+ return async () => {
158
+ stop();
159
+ await services.dispose(BACKGROUND_CONTEXT);
160
+ };
161
+ }
162
+ ```
163
+
164
+ ```text
165
+ explicit first tx.doc -> commit base { strokes: [] }
166
+ addStroke(A) -> commit A -> local and remote subscribers see A
167
+ worker restarts -> open same durable storage; install canvas facet again
168
+ acquire source -> load base + committed deltas, without rerunning initial()
169
+ late client -> hydrate complete canvas including A, then ordered updates
170
+ conversation fork -> still uses this same Session canvas
171
+ ```
172
+
173
+ Use persistent storage for restart survival. The memory backend is not persistent.
174
+ A method's successful commit does not promise every remote callback has run yet.
175
+
176
+ Shut down in reverse: detach clients and dispose the facet host, which withdraws
177
+ its services and disposes the document states it owns, then close the Harness.
178
+ Closing the Harness first would end the states under still-connected clients:
179
+ they keep the last value and never update again.
180
+
181
+ ```ts
182
+ async function shutdown(host: FacetHost, detachClients: () => Promise<void>, harness: Harness, context: Context) {
183
+ await detachClients();
184
+ await host.dispose();
185
+ await harness.close(context);
186
+ }
187
+ ```
188
+
189
+ ## 2. Conversation-scoped diff reviews
190
+
191
+ A **document family** uses one definition for many instances. The logical key is
192
+ `(kind, conversationId, key)`; the persisted numeric document ID identifies an
193
+ incarnation and is never reused. The creation seed is not part of the key.
194
+
195
+ ```ts
196
+ type ReviewInput = { path: string; patch: string };
197
+ type ReviewComment = { id: string; line: number; text: string };
198
+ type ReviewState = ReviewInput & { comments: ReviewComment[] };
199
+ const ReviewDoc = defineDocFamily<ReviewState, ReviewInput>({
200
+ kind: "app.diff-review", version: 1, family: true, scope: "conversation",
201
+ history: "latest", fork: "current",
202
+ initial: seed => ({ path: seed.path, patch: seed.patch, comments: [] }),
203
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 49,
204
+ });
205
+ interface DiffReviewService {
206
+ readonly state: ReplicatedState<ReviewState | null>;
207
+ identity(context: Context): Promise<{ conversationId: ConversationId; key: string }>;
208
+ addComment(comment: ReviewComment, context: Context): Promise<void>;
209
+ }
210
+ const DiffReviews = defineService<DiffReviewService>("app.diff-reviews");
211
+
212
+ function reviewFacet(
213
+ session: Session, conversationId: ConversationId,
214
+ reviews: readonly { key: string; seed: ReviewInput }[], context: Context,
215
+ ): Facet {
216
+ return defineFacet({
217
+ id: "app.diff-reviews/session",
218
+ setup(env) {
219
+ const instances = env.provideMany(DiffReviews);
220
+ env.onActivate(async () => {
221
+ for (const review of reviews) {
222
+ await session.commit(async tx => {
223
+ await tx.doc(ReviewDoc, conversationId, review.key, review.seed);
224
+ }, context);
225
+ const state = await session.documentState(ReviewDoc, conversationId, review.key, context);
226
+ if (state === undefined) throw new Error("review was retired during setup");
227
+ env.own(() => state.dispose());
228
+ // Chord instance keys route services; they are not numeric document incarnation IDs.
229
+ instances.spawn(JSON.stringify([conversationId, review.key]), {
230
+ state,
231
+ async identity() { return { conversationId, key: review.key }; },
232
+ async addComment(comment, context) {
233
+ await session.commit(async tx => {
234
+ const draft = await tx.doc(ReviewDoc, conversationId, review.key, review.seed);
235
+ draft.comments.push(comment); // Chord copies the assigned comment by value.
236
+ }, context);
237
+ },
238
+ }); // The facet owns spawned service lifetimes automatically.
239
+ }
240
+ });
241
+ },
242
+ });
243
+ }
244
+ ```
245
+
246
+ Example input: `[{ key: "review-7", seed: { path: "a.ts", patch: "-old\n+new" } }]`.
247
+ Pass distinct family keys and a real conversation ID; install the facet as above.
248
+ Keyed consumers use `env.observe(DiffReviews, handler)`, not `env.use`. The handler
249
+ receives `(review, context)`; call `review.identity(context)` to identify it and
250
+ `review.state.subscribe` to observe comments.
251
+
252
+ These comments are conversation-scoped: ending the generating task or unloading
253
+ the facet does not retire them. `current` copies every logically
254
+ present review at fork-commit time into independent child instances with new
255
+ numeric IDs, retaining family keys. Parent and child then diverge.
256
+
257
+ ```text
258
+ parent review-7: comment A -> transcript entry E -> comment B
259
+ fork at E with current: child review-7 contains A and B
260
+ child adds C: parent still contains only A and B
261
+ ```
262
+
263
+ With `fork: "initial"`, no instance copies; the first child `tx.doc()` uses its supplied seed.
264
+ `latest` cannot read history or fork `asOf`; use `history: "rewindable"` with `asOf`
265
+ to reflect E's commit. Reaccess ignores later seeds; updating the patch requires
266
+ an explicit mutation or a distinct review key.
267
+
268
+ ## 3. Task-scoped output and a tool/task watch
269
+
270
+ This is a bounded progress window, not the permanent result. Scope itself gives
271
+ it task lifetime; no history, fork, owner, or conversation setting is needed.
272
+
273
+ ```ts
274
+ type JobInput = { command: string };
275
+ type JobOutput = { stdout: string; chunks: number };
276
+ const JobOutputDoc = defineDoc<JobOutput>({
277
+ kind: "app.job-output", version: 1, scope: "task",
278
+ initial: () => ({ stdout: "", chunks: 0 }),
279
+ checkpointWhen: (_value, _ops, info) => info.deltasSinceBase >= 99,
280
+ });
281
+ async function appendJobOutput(
282
+ runtime: TaskRuntime<JobInput, { phase: "running" }, null, object>,
283
+ chunk: string, context: Context,
284
+ ): Promise<void> {
285
+ // Read process output outside this callback. The runtime gates the live task.
286
+ await runtime.commit(async tx => {
287
+ const draft = await tx.doc(JobOutputDoc, runtime.taskId);
288
+ draft.stdout = (draft.stdout + chunk).slice(-50_000);
289
+ draft.chunks += 1;
290
+ }, context);
291
+ }
292
+ async function observeJob(
293
+ api: DocumentObserver, producerTaskId: TaskId,
294
+ finished: Promise<void>, context: Context,
295
+ ): Promise<void> {
296
+ const watch = await api.watchDoc(JobOutputDoc, producerTaskId, context);
297
+ if (watch === undefined) return;
298
+ console.log(watch.value === null ? "retired" : watch.value.stdout);
299
+ try {
300
+ watch.start(async (value, ops, _context) => {
301
+ await sendCommittedFrame(value, ops);
302
+ await render(value === null ? "retired" : value.stdout);
303
+ }); // Serialized callbacks never overlap.
304
+ await finished; // Caller-supplied observation lifetime; outside any commit.
305
+ } finally {
306
+ await watch.stop(); // Idempotent; prevents another callback from starting.
307
+ }
308
+ }
309
+ ```
310
+
311
+ The running phase calls `appendJobOutput` and must also commit its next checkpoint
312
+ or terminal outcome. `api` is the invocation's `DocumentObserver`; `TaskRuntime`
313
+ includes that interface. `tx.doc()` creation validates that the target task is live and derives its
314
+ conversation from the task record. A later non-creating watch lookup returns
315
+ `undefined` after the task is terminal and its document has retired.
316
+
317
+ ```text
318
+ watchDoc: capture immutable V0 + register for later exact committed frames
319
+ producer commits D1 and D2 before start: buffer (V1, D1), (V2, D2)
320
+ start: caller has initialized from V0; deliver D1, then D2 serially
321
+ producer commits D3 while listener awaits: buffer D3; never overlap callbacks
322
+ 101 pending frames: replace the pending suffix with [["r", newestValue]]
323
+ producer becomes terminal: deliver [["r", null]], then close as retired
324
+ ```
325
+
326
+ Watches automatically stop when their invocation ends. A Session-acquired watch
327
+ is caller-owned and stops on Session close. Previously delivered snapshots never
328
+ mutate. Slow or unstarted delivery retains up to 100 exact frames, then replaces
329
+ the undelivered suffix with a full-value root replacement. A watch is convergent
330
+ state observation rather than a transition journal. Retirement ends the
331
+ incarnation's stream; a service exposing that state must withdraw or remain
332
+ terminal at `null`. Recreation requires a new watch/state and numeric document ID. A terminal task cannot
333
+ create more output. Task-scoped documents
334
+ never copy into forks. Preserve required output in result entries or Session- or
335
+ conversation-scoped documents in the terminal commit before retirement. Use a
336
+ task-scoped family only when one task needs several independently keyed documents.
337
+
338
+ ## 4. Built-in conversation views and commit delivery
339
+
340
+ `ConversationView` contains `conversation`, raw transcript `entries`, and
341
+ `docs: Readonly<Record<string, JsonObject>>`. Selected built-in singletons mount
342
+ under `docs` by stable document kind, not numeric incarnation ID. The spec's
343
+ illustrative path mapping is:
344
+
345
+ ```text
346
+ document ["s", ["generation", "message"], value]
347
+ -> view ["s", ["docs", "pi.live", "generation", "message"], value]
348
+ ```
349
+
350
+ `pi.live` is specified in `spec.md` section 8.2.
351
+ The mount publishes one batch per complete Session commit: entry/head changes and
352
+ changed mounted documents together, without a tracker or semantic projection.
353
+ Third-party documents are **not automatically mounted**; use their own Chord
354
+ services or trusted invocation watches. A retired source publishes `null`; a
355
+ dynamic service should then withdraw its instance rather than expose stale data.
356
+
357
+ ```text
358
+ addStroke -> hold Session mutation line -> await tx.doc -> mutate tracker change draft
359
+ callback succeeds -> tracker prepare: immutable candidate + immutable ops
360
+ Session checkpoint predicate selects a base or delta exactly once
361
+ atomic storage commit: persist selected document and record writes
362
+ storage succeeds -> adopt candidate + enqueue candidate/ops, still on line
363
+ release line -> deliver committed source ops -> adapter -> local/remote Chord consumers
364
+ late subscriber -> atomically capture committed value + adapter sequence + subscription
365
+ ```
366
+
367
+ No visible-undurable path exists. Callback, tracker preparation, and checkpoint
368
+ failure abort normally before Storage admission. An uncertain Storage failure
369
+ publishes nothing and poisons the open Session; close and reopen it instead of
370
+ continuing.
371
+
372
+ ## Footguns
373
+
374
+ - Never retain drafts, nested proxies, or bound array methods across commits;
375
+ they are revoked. Assigned containers are copied by value.
376
+ - Async commit holds the line through storage settlement and baseline adoption.
377
+ Await document access there, not models, processes, network calls, or humans.
378
+ - Only `tx.doc()` creates. `snapshot`, `documentState`, and `watchDoc` return
379
+ `undefined` when absent and never write. Family seeds are used only when the
380
+ first `tx.doc()` creates an incarnation; later seeds are ignored.
381
+ - Initialize from the fixed `watch.value` before `start()`. Slow or unstarted
382
+ delivery retains up to 100 exact frames, then replaces the pending suffix with
383
+ one full-value root replacement. No serialized-byte accounting is performed.
384
+ Do not use a watch as an audit log.
385
+ - `stop()` prevents future callbacks but does not abort or join one already running.
386
+ - Definitions own checkpoints, not storage heuristics. Keep document kinds, versions, fork policies, and
387
+ public paths stable; schema changes require migration, not a source-only rename.
388
+
389
+ Chord sources: [types](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/src/types.ts), [facet examples](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/test/facets.test.ts),
390
+ [state](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/src/services/state.ts), [provider](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/src/services/provider.ts),
391
+ [Context](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/src/context/index.ts), [Delta](https://github.com/earendil-works/pi/blob/b78e6a9085343ec0f308c3d377da72528b4cf7ee/packages/chord/src/delta/README.md).