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.
- package/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +328 -2
- package/ai_docs/durable_pi/announcement.md +860 -0
- package/ai_docs/durable_pi/changelog.md +51 -0
- package/ai_docs/durable_pi/chord-delta-findings.md +362 -0
- package/ai_docs/durable_pi/coding-agent-integration.md +70 -0
- package/ai_docs/durable_pi/pico-v5-chord-usage.md +391 -0
- package/ai_docs/durable_pi/pico-v5-handoff.md +921 -0
- package/ai_docs/durable_pi/readme.md +617 -0
- package/ai_docs/durable_pi/sources.md +22 -0
- package/ai_docs/durable_pi/spec.md +4696 -0
- package/ai_docs/durable_pi/vacation-planner.md +57 -0
- package/ai_docs/pi/chord.md +268 -0
- package/ai_docs/pi/cli-integration.md +110 -0
- package/ai_docs/pi/cli.md +325 -0
- package/ai_docs/pi/codemode.md +211 -0
- package/ai_docs/pi/compaction.md +467 -0
- package/ai_docs/pi/configuration.md +51 -0
- package/ai_docs/pi/containerization.md +187 -0
- package/ai_docs/pi/custom-provider.md +177 -0
- package/ai_docs/pi/environment-variables.md +102 -0
- package/ai_docs/pi/extensions.md +278 -0
- package/ai_docs/pi/how-pi-works.md +53 -0
- package/ai_docs/pi/index.md +43 -0
- package/ai_docs/pi/json.md +230 -0
- package/ai_docs/pi/keybindings.md +198 -0
- package/ai_docs/pi/llama-cpp.md +118 -0
- package/ai_docs/pi/mcp.md +274 -0
- package/ai_docs/pi/message-types.md +265 -0
- package/ai_docs/pi/models.md +209 -0
- package/ai_docs/pi/overview.md +166 -0
- package/ai_docs/pi/packages.md +133 -0
- package/ai_docs/pi/pi-agent-core.md +568 -0
- package/ai_docs/pi/pi-ai.md +1942 -0
- package/ai_docs/pi/prompt-templates.md +63 -0
- package/ai_docs/pi/providers.md +204 -0
- package/ai_docs/pi/quickstart.md +140 -0
- package/ai_docs/pi/readme.md +99 -0
- package/ai_docs/pi/rpc-commands.md +864 -0
- package/ai_docs/pi/rpc-extension-ui.md +204 -0
- package/ai_docs/pi/rpc.md +197 -0
- package/ai_docs/pi/sdk.md +151 -0
- package/ai_docs/pi/security.md +103 -0
- package/ai_docs/pi/session-format.md +297 -0
- package/ai_docs/pi/sessions.md +70 -0
- package/ai_docs/pi/settings.md +173 -0
- package/ai_docs/pi/shell-aliases.md +97 -0
- package/ai_docs/pi/skills.md +97 -0
- package/ai_docs/pi/slash-commands.md +64 -0
- package/ai_docs/pi/sources.md +57 -0
- package/ai_docs/pi/terminal-setup.md +223 -0
- package/ai_docs/pi/termux.md +121 -0
- package/ai_docs/pi/themes.md +140 -0
- package/ai_docs/pi/tmux.md +59 -0
- package/ai_docs/pi/toolkit.md +168 -0
- package/ai_docs/pi/tui.md +129 -0
- package/ai_docs/pi/usage.md +98 -0
- package/ai_docs/pi/virtual-models.md +118 -0
- package/ai_docs/pi/windows.md +71 -0
- package/apps/dpi-backend/.nvmrc +1 -0
- package/apps/dpi-backend/API.md +99 -0
- package/apps/dpi-backend/README.md +119 -0
- package/apps/dpi-backend/deploy/backend.env.example +9 -0
- package/apps/dpi-backend/deploy/dpi-backend.plist +26 -0
- package/apps/dpi-backend/deploy/dpi-backend.service +22 -0
- package/apps/dpi-backend/package.json +33 -0
- package/apps/dpi-backend/prompts/one_sentence.md +3 -0
- package/apps/dpi-backend/prompts/smart_ass_opus_fix.md +169 -0
- package/apps/dpi-backend/scripts/check-contract.mjs +151 -0
- package/apps/dpi-backend/scripts/migrate-playground.ts +66 -0
- package/apps/dpi-backend/scripts/record-nested-fixture.mts +154 -0
- package/apps/dpi-backend/scripts/token.ts +57 -0
- package/apps/dpi-backend/src/addons.ts +139 -0
- package/apps/dpi-backend/src/auth.ts +20 -0
- package/apps/dpi-backend/src/config.ts +54 -0
- package/apps/dpi-backend/src/contract-check.ts +118 -0
- package/apps/dpi-backend/src/errors.ts +50 -0
- package/apps/dpi-backend/src/extensions.ts +510 -0
- package/apps/dpi-backend/src/faux.ts +80 -0
- package/apps/dpi-backend/src/history.ts +165 -0
- package/apps/dpi-backend/src/idempotency.ts +20 -0
- package/apps/dpi-backend/src/jev.ts +43 -0
- package/apps/dpi-backend/src/jobs.ts +790 -0
- package/apps/dpi-backend/src/main.ts +76 -0
- package/apps/dpi-backend/src/model-catalog.ts +180 -0
- package/apps/dpi-backend/src/pairing-files.ts +142 -0
- package/apps/dpi-backend/src/pairing.ts +459 -0
- package/apps/dpi-backend/src/routes.ts +217 -0
- package/apps/dpi-backend/src/shared-state.ts +85 -0
- package/apps/dpi-backend/src/store.ts +1240 -0
- package/apps/dpi-backend/src/streams.ts +256 -0
- package/apps/dpi-backend/src/tunnel.ts +955 -0
- package/apps/dpi-backend/tsconfig.json +13 -0
- package/apps/dpi-playground/README.md +83 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-CFGc7lHT.js +77 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/assets/index-DFdKpwm_.css +1 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/dist/index.html +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/index.html +15 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/anthropic.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/deepseek.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/durable-pi.svg +14 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/gemini.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/grok.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/kimi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/lab.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/meta.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/minimax.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/openai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/qwen.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/xiaomi.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/public/icons/zai.png +0 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/App.vue +180 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/commands.ts +119 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/composables.ts +129 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/hub.ts +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/icon-path.ts +7 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/ids.ts +2 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/jobs.ts +80 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/link-policy.ts +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown-policy.ts +115 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/markdown.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-close.ts +23 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair-handoff.ts +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/pair.ts +166 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/qrcode.ts +323 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/signature.ts +146 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/store.ts +373 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/client/stream.ts +183 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/AgentView.vue +342 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Composer.vue +198 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ContextBar.vue +254 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceCredentialsDialog.vue +40 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DeviceIcon.vue +56 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/DirBrowser.vue +95 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ExtensionsPanel.vue +96 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/HelpDialog.vue +69 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Inspector.vue +425 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/JobCard.vue +221 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/MobileNav.vue +39 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/ModelPicker.vue +71 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NetworkSidebar.vue +236 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/NewSessionDialog.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/OfflinePanel.vue +128 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PairDeviceWizard.vue +433 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/PromptDialog.vue +89 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/StateEditor.vue +179 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TasksPanel.vue +280 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/Toast.vue +11 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TokenManager.vue +175 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TopBar.vue +114 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TranscriptItem.vue +152 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TreeRow.vue +195 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/components/TunnelStatusBar.vue +107 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/extensions.css +105 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/jobs.css +655 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/main.ts +22 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/network.css +675 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/src/style.css +3203 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/tsconfig.json +16 -0
- package/apps/dpi-playground/apps/dpi-playground-fe/vite.config.ts +14 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/package.json +5 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/audit.ts +406 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/client-ip.ts +72 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/credentials.ts +265 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/guard.ts +43 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/http-errors.ts +156 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/identity.ts +60 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/limiter.ts +165 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/main.ts +460 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/monitor.ts +320 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/mux.ts +277 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/pairing.ts +816 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/proxy.ts +128 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/rotation.ts +583 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/seed.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/transport.ts +108 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-token.ts +11 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel-transport.ts +560 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/src/tunnel.ts +873 -0
- package/apps/dpi-playground/apps/dpi-playground-hub/tsconfig.json +13 -0
- package/apps/dpi-playground/package.json +37 -0
- package/apps/dpi-sdk/README.md +74 -0
- package/apps/dpi-sdk/package.json +20 -0
- package/apps/dpi-sdk/scripts/net.ts +73 -0
- package/apps/dpi-sdk/src/api.ts +631 -0
- package/apps/dpi-sdk/src/app-state.ts +111 -0
- package/apps/dpi-sdk/src/client.ts +330 -0
- package/apps/dpi-sdk/src/errors.ts +16 -0
- package/apps/dpi-sdk/src/jobs.ts +99 -0
- package/apps/dpi-sdk/src/models.ts +472 -0
- package/apps/dpi-sdk/src/node/chunk.ts +488 -0
- package/apps/dpi-sdk/src/node/config.ts +175 -0
- package/apps/dpi-sdk/src/node/drivers.ts +402 -0
- package/apps/dpi-sdk/src/node/ops.ts +114 -0
- package/apps/dpi-sdk/src/node/private-dir.ts +48 -0
- package/apps/dpi-sdk/src/node/registry-migration.ts +549 -0
- package/apps/dpi-sdk/src/node/registry.ts +390 -0
- package/apps/dpi-sdk/src/node/sse-writer.ts +151 -0
- package/apps/dpi-sdk/src/protocol/backoff.ts +31 -0
- package/apps/dpi-sdk/src/protocol/limits.ts +199 -0
- package/apps/dpi-sdk/src/protocol/pairing.ts +311 -0
- package/apps/dpi-sdk/src/protocol/rest-routes.ts +105 -0
- package/apps/dpi-sdk/src/protocol/tunnel.ts +331 -0
- package/apps/dpi-sdk/src/sse.ts +117 -0
- package/apps/dpi-sdk/src/transcript.ts +291 -0
- package/apps/dpi-sdk/tsconfig.json +13 -0
- package/apps/dpi-sdk/w +20 -0
- package/apps/dpi-tui/README.md +54 -0
- package/apps/dpi-tui/bin/dpi-tui.mjs +5 -0
- package/apps/dpi-tui/package.json +20 -0
- package/apps/dpi-tui/src/app.ts +433 -0
- package/apps/dpi-tui/src/commands.ts +151 -0
- package/apps/dpi-tui/src/main.ts +122 -0
- package/apps/dpi-tui/src/pairing.ts +287 -0
- package/apps/dpi-tui/src/pickers.ts +238 -0
- package/apps/dpi-tui/src/render.ts +164 -0
- package/apps/dpi-tui/src/theme.ts +57 -0
- package/apps/dpi-tui/tsconfig.json +14 -0
- package/bin/device.mjs +5 -0
- package/bin/hub.mjs +5 -0
- package/bin/tui.mjs +5 -0
- package/deploy/Caddyfile +120 -0
- package/docs/verification.md +102 -0
- package/dpi-network.example.yaml +62 -0
- package/images/01_durable_agent_network.svg +185 -0
- package/images/02_pi_made_durable.svg +204 -0
- package/images/03_restart_mid_tool.svg +168 -0
- package/images/07_one_api_every_client.svg +123 -0
- package/images/08_device_session_agent.svg +123 -0
- package/images/09_compact_keep_history.svg +143 -0
- package/images/10_shared_app_state.svg +177 -0
- package/images/11_durable_jobs_wait.svg +141 -0
- package/images/12_tools_change_mid_run.svg +231 -0
- package/images/13_hook_blocks_tool.svg +122 -0
- package/images/14_fork_at_any_entry.svg +119 -0
- package/images/15_web_client.png +0 -0
- package/images/16_terminal_client.svg +43 -0
- package/images/17_devices_are_data.svg +182 -0
- package/images/18_should_every_agent_be_durable.svg +118 -0
- package/images/19_sqlite_on_every_device.svg +155 -0
- package/images/20_admission_and_effects.svg +134 -0
- package/justfile +33 -0
- package/package.json +41 -4
- 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).
|