p2 (Pi Mobile on Chord v2) moves from pi-private into pi as one commit. Root scripts that loop all workspaces use --if-present; npm test, ./test.sh and the release script cover packages/* only (p2: cd apps/p2 && npm test); sync-versions also updates apps' workspace pins.
21 KiB
p3 / Chord v2: the whole target design as a walkthrough
A reading aid for checking completeness, not a new source. Sources: redesign-decisions.md (wins on conflict),
target-design.md, questions.md, plugin-loading.md, p3-functions/, changes.md. Marks: [D] decided,
[B] built in today's Chord, [S] superseded by a later decision, [O] open, [P] proposed, not decided.
Section 17 lists conflicts between the documents found while writing this.
1. Plan
- [D] p3 next to p2, as
apps/p3, own data directory. p2 keeps running unchanged. Plugins are ported one at a time. - [D] No migration of p2 data: p3 starts empty (no
seed, no catalog/shares/subagents import, no legacy parsing). - [D] Chord v2 lives inside p3 until stable, then moves to
packages/chord(pi first, CI) and p2 is deleted. - [D] Durable boundary: Durable keeps importing today's Chord (
delta,context,output); p3 hands Durable only plain data and Durable's own types. Chord v2 takesdelta/outputover unchanged.OutputBuffer/Progressalready moved to@earendil-works/chord/output[B]. - [D] No backward compatibility anywhere unless asked.
- [D] Approach: functionality first (
p3-functions/: 22 domains), then objects per domain (owner, host, pure/native, relationships), then target design v2 written from the decision log, then reviewers judge it.
2. Concepts
| concept | what it is |
|---|---|
| facet | a plugin's code for one host kind; its setup is a pure host-lifetime object [D] |
| object type | contract: name, input (address), variants, version, host [B] |
| object | one instance per (type, input, principal key) |
| principal | who calls; stamped by links, typed by the app [B] |
| reference | what get returns; keeps the object alive; live and hydrated [B] |
| document | named JSON value an object owns, written in transactions |
| derive | value computed from documents/readables |
| native | a value opened with self.open in an object (socket, process, DB handle) [D] |
| routine | standing behaviour, self.routines.* |
| job | owned work started by a method, outlives the call |
| registry | an owner's extension point, versioned immutable table |
| host value | typed native the host provides before facets load |
ctx |
per call: signal, principal, call-scoped get; in jobs jobId, progress |
3. Object types and principals [B]
local(same host, unvalidated) orremote(over links, variants per principal, TypeBox-validated).hostdeclares placement (hostNamed,hostFamily); changing it is breaking. Callers don't passat.- Same version binds if the provider covers the caller; breaking change bumps
version;accepts: { [old]: adapter }keeps old consumers, elsecontract_mismatch. - References are a union discriminated by
variant; strict narrowing withinVariant/expectVariant/RefIn(entry 8). Weakening type checking was rejected. - Principals:
createHost<P>(), every principal has akind. p2:owner(access: local | relay | device),guest,worker,server,host. Stateinit(2b). Origin ids per client (entry 8). Protocol 4. host.serve(Type, factory, { lifetime: "referenced" | "host", linger? });"host"checked by types [B].
4. Factories, natives, the facet [D]
- Factory principle: a factory is synchronous and only declares (references, documents, natives, derives, routines, jobs, registrations). Chord activates. Handles it gets are valid for the object's whole life.
- Natives only in objects:
self.open(async ({ signal }) => value, close). Chord opens during activation (object goes live after all opens), closes in reverse order when the object ends. The value never changes during the object's life.- Gone [S]:
self.resource, resourceversion,replace,deps, reopen, stale handles, "read.valueat use" (target-design §12).
- Gone [S]:
- No natives across plugins: others reach a native only through the owning object's methods and state.
- The facet is a pure object: documents, references, routines, registrations; no
self.open. Served objects may read/write the facet's documents; one transaction may span own docs + facet docs (lock order: own line, then facet). The facet may not reach into served objects' documents. - Between plugins only: object types, registries, host values.
- Rejected: ceremony (
native/dirs,defineNative, lints).
5. Ownership [D]
- Every thing has exactly one owning object, the smallest it belongs to. The owner holds the data, enforces rules in its methods, offers operations. Nobody reaches past it.
- Ownership nests: session owns its conversations and location; a conversation owns its content and rules (read-only,
follows session cwd), set in the creating commit (Durable
createConversationwithinit). - Methods return data; objects are handed out as addresses (
ctx.get(Type, address)). - Registries add behaviour (tools, routes, renderers, commands, send routers, input gates); never guard owned data.
- Core owners always exist; plugin owners may be missing (→ optional references [O]).
- Avoid a core god object.
- Porting guideline: what core enforces on data lives in a format core owns, written by the plugin at creation.
Example: one-shot / cwd-following children marked in a core per-conversation policy document;
ReadOnlyConversationsandSessionCwdFollowersgo away.
6. References
- [B]
self.get(factory, released with the object),await ctx.get(call),host.get/host.hold(outside). - [B] Awaited reference is live and hydrated;
state: falsefor method-only use. - [B] Link drop: values stay (
loading), re-hold automatically, calls wait. - [D] Failed object ends its references with the cause; consumers re-hold with backoff (web gets the TUI's backoff instead of fixed 500 ms).
- [D] Backoff per address (1 s doubling to 60 s): a
getduring backoff returns the stored error; concurrent gets share one attempt. Cut short by: plugin reload, a dependency of the failed attempt changing,ref.retry(),self.retryOn(...readables). - [D] Consumers handle errors as on a disconnect. No call/request-id framework (Chat's
requestIdstays).
7. Documents and transactions [D]
defineState(name, { schema, version, initial, migrate?, keyed?, persist?, checkpointWhen? }).self.state(Def)→ one handle. Singleton: loaded at activation,handle.valuesync, exposable readable. Keyed:await handle.get(key),for await (key of handle.keys())(paged from storage). No live key list; live collections are singleton docs; large ones are paged methods.- Writes only via
await tx.draft(handle, key?, { tags? })inself.tx(ctx, async tx => …);tx.retire. - Durable's rule: inside a tx only
await tx.*; no other objects, models, processes, network, nested tx. Fetch before; decide inside. - Commit: per-object line; storage batch serialized per host; adopt; notify once; one wire batch.
- Shorthands:
handle.change(ctx, key?, fn),handle.retire(ctx, key?). - Versions: pure
migrate, on access; old code on newer doc throws; nomigrate→ access refused, data kept. - Tags: typed (
defineTag<T>), set explicitly on create;store.retireByTag. p2SessionTag. - Lifetime: in-memory docs live with their object identity; persisted until retired / session delete.
persistonly on stable identities (lifetime: "host"orstoreKey(input)).- Rejected: optimistic transactions; Durable's Session as object store; volatile documents.
8. Store and storage [D]
- Chord owns the store: lines, serialized commits, migrate on access,
checkpointWhen, retire, tags, invariants. - Eviction as in Durable for keyed persisted docs (10 min unused and unwatched; LRU, one timer).
- Storage error ends the store; host reports and restarts.
DocumentStorageinterface (commit,find,load,scan,byTag,mintId); memory implementation in Chord; p2/p3 adapts Durable's SQLiteStorage(kind = <definition>.<objectHash>); tag index in the adapter.- Conformance suite in
@earendil-works/chord/testing. - Persistence split: app state in Chord documents; only transcript-bound data in Durable.
- Kinds of state: document, derive, source readable, paged method, plugin database (as a native).
9. Derives [D]
self.derive(name, sources, (values, previous) => …);previouskept across factory re-runs by name.
10. Jobs [D]
defineJob(name, { version, args, progress, result, concurrency?, persist?, accepts? }).- Sandwich: intent
tx.start(job, args, { key? })(busy check and dedupe atomic, run starts after commit) → runrun(args, ctx)(anything; external writes keyed byctx.jobId; keeps old code on reload) → finishfinish(outcome, tx)in current code, ack in the same commit; retried with backoff if it throws. - Outcome
{ ok, result } | { ok: false, error: threw | aborted | lost }. job.start(args, ctx, opts)returns a run withoutcome(ctx); caller's abort ends only its wait.- Abort is a request (
job.abort(id)fires the signal). runningreadable{ id, version, args, progress, phase };concurrencycounts active runs;phase: finishing | blocked.- Versions via
accepts; stored outcomes never rewritten; missing adapter →blocked, not a refused reload. persist: true(stable identities): a live run at crash endslost.- Running jobs count for keep-alive and the relocation fence.
- Not jobs: request/response (method), input into running work (Questions), crash-resumable work (Durable task).
- [O] jobs as objects with an "until done" lifetime.
11. Routines [D]
self.routines.on(readable|event, fn),every(name, interval, fn)(due time survives reloads),loop(name, fn),custom(name, setup).- Start when the object goes live, stop when it ends, restart on reload of its plugin; serialized per routine (new instance waits for the old handler in flight, with a deadline).
- Reconcile from current value; no "in progress" flags; long work → job.
- No bare timers/callbacks outside routines, jobs or calls.
- [O]
overlap: "latest".
12. Registries and host values [D]
defineRegistry<V>(name, { version, accepts, conflict: "error" | "last" }); facet registers; entry carries its build version; tables are immutable values swapped atomically; no late binding; no lifecycle in entries.hostValue<T>(name, { version }), provided before any facet on every host (worker: harness, environments, init, location, liveness, cwd followers; server: session hosts, server info, revocation; web/TUI: navigation, theme, clipboard). Replaceopaquemembers.- Start barrier: open Durable without scheduling → host values → load facets → check cwd/model →
resume(). - Rejected: late-bound entries, dynamic registries.
13. Reload [D]
- Explicit (
npm run reload -- <plugin>), never on save. Behaves like a restart of the changed plugins. - Per host: load (bundle; error rejects) → staging (facet setup and every live object's new factory with a
recording
self; nothing activated; a throw rejects) → commit (atomic) → after. - Rollback all-or-nothing per host: any load/staging error leaves the host entirely on previous code. Status per host suffices.
- Pure objects swapped in place: new methods and derives; documents, references, jobs stay; routines restart serialized; new references go live; gate opens. Running calls finish on old code.
- Native objects of the reloaded plugins restart: running work aborted (
aborted), natives close, factory runs, natives reopen. No per-module digest; split expensive natives into their own plugin. Open fails → that objectfailedwith backoff; rest of host on new code. - Remote consumers see a normal brief object restart:
loading, values readable, calls park, re-hold; calls on the old instance end with an error. - Errors: code errors (load, setup, factories) → rollback; activation errors (opens, new references, migrations) → per object; a method bug → that call.
- No factory trial, no migrate dry run, no consumer contract check during prepare.
- Removing a type: its objects end; in-memory docs go; persisted docs stay.
- Rejected: drain-based handover, version fences,
restart_required, reload votes / cross-host transactions, revoking proxies on.value.
14. Rollout [D]
- Revision = (server incarnation, sequence) over the complete desired code set (every facet + digest).
- Server installs, then each worker, then clients; each reports
installed/installed_failed/rejected. A starting or reconnecting host installs the current revision before anything is routed to it. Installed = code published (objects activate on demand). - Mismatch responses carry the provider's revision and status: provider ahead or installing →
updating(values readable, calls park); provider rejected → fail fast; same revision →contract_mismatchfinal. Deadline on waits. - Server rejects → rollout stops.
- [O] check own host's status before waiting; serialize installs per host; digest of the code set (no reinstall after a server restart).
15. Wire, presence, Questions, ctx
- Wire [D]: documents are state members;
snapon hold,opsper commit, diff ops for derives, throttled job progress,resetfor migration,error. Commits atomic for consumers: onestateframe with abatchper hold. [O] atomic per link, diffed ops for adapted bindings, size limit. - Presence [D]:
host.references(Type)readable (input, principal, clients); origin ids per client; linger bit (lingering keeps the worker alive, not a viewer). Replaces presence/viewer/login-abandon leases. [O] link drop counts as lingering; counts per origin. - Questions [D]: p3 service for input into running work: shared store, per-principal asker/viewer views, typed
questions (text, select, confirm), policies first/quorum/all over a live audience,
keyto reattach, deadline, authorization per question, secrets redacted. Used by login, tool approval, question tool. - ctx [D]:
signal,principal,get; in jobsjobId,progress. No code versions, no root ids.
16. Bytes and session files
- [D] Bytes are a schema type (
Uint8Array): structured clone on IPC (serialization: "advanced"), binary frames on WebSockets. No base64 in JSON. Lives in Chord v2 inside p3 (not added to today's Chord). - [D] Large transfers chunked by the owner:
begin/append({ upload, offset, bytes })/finish,read({ id, offset, length }), ~1 MiB chunks; awaitedappendis backpressure; resume from stored offset after a link drop; unfinished uploads expire. Helper wraps the loop. No stream type in Chord. - [P]
SessionFiles {session}on the server: one owner of uploads (25 MiB, ≤20/message), sent-file snapshots (25 MiB), artifact versions (2 MiB, name + version); index readable without a worker; bytes under<data>/sessions/<id>/files/; copy on clone and fork, delete with the session. Not in it: workspace downloads (streamed), shell/codemode spill (in the env).DownloadStore.save(unused) goes. Pending sub-points: one preview limit (256 vs 512 KiB); fork's artifact list empty or copied; copying attachments into the remote env.
17. Session lifecycle [D]
- Archive retires nothing (stops the worker, ends guest access, closes forwards).
- Session delete (new) removes the whole session: tagged documents, the worker's database and document storage,
files, catalog entry. [O] how:
SessionDeletedregistry vs. per-session objects +SessionTag+ session directory; search index per session vs. global + hook.
18. Versions and adapters
| surface | on change |
|---|---|
| object type | accepts adapter, coverage check [B] |
| registry | accepts adapter |
| host value | accepts; harness refused on mismatch for now |
| document | pure migrate |
| job | accepts on outcomes and running |
| native | object restarts on reload (no version) |
| Durable tasks/docs | Durable migrate [B] |
| link protocol | refused; hosts update together [B] |
| UI components, API | exposed as they are; versioned with the SDK later |
[O] file formats (envs.json, …) |
unversioned today |
19. Plugin loading (plugin-loading.md, revision 4)
- Sources: built-in, global (
<data>/plugins), global packages (npm/git,--ignore-scripts), project (<cwd>/.pi/p2/plugins, worker/web/tui only, never server). - Discovery = directory listing, never parses code; server selects, workers scan nothing; dependencies resolved by contract name at runtime; missing provider fails at activation with the consumer's name.
- Names:
<plugin>.<name>for contracts, registries, Durable extensions, entry kinds; tools<plugin>_<name>. - Loader D: one base bundle per surface + one script per facet, host packages passed in, no module map (GC reclaims old generations); stable filename per (facet, digest), V8 compilation cache off. Vite removed after web D.
- Project trust:
trust.jsonper directory,ask/never, pin-to-content mode; content-addressed snapshots; approved bytes = executed bytes; change → dirty → owner Reload. Project.pi/skills/.pi/promptsgated too. - Scopes: a page/TUI worker holds one scope;
ClientBundlepushes bundle hashes; TUI snapshots partitioned per scope. - Leaving a scope: provenance map, fallback renderers, blocked tasks with Abort.
- Security table: trusting a project grants the owner's OS account and owner rights in the browser.
- Contracts are a versioned package, never host packages.
- [O] base skills on remote sessions; publishing; plugin identity across projects; guest opt-in; project root; browser compile cache. Project plugins as a whole: separate discussion.
20. Functionality map (p3-functions/)
22 domains; cross-cutting needs (one file store, per-session lifecycle, readers without workers, generation-fenced reports, catch-up after commits, work outliving its caller, request-id idempotency, cancellation chains, keep-alive vs presence, execution env following the session, single config writer, attribution, one permission matrix, revocation, chunked bytes, one notice channel, UI state survival, TUI consent); ~25 duplicates; ~18 drop candidates.
21. Open (all)
- Optional references (a plugin owner may be missing; radius dies without ports today).
- Keep-alive computed by the worker itself.
- UI renderer reload vs. leaving (login aborted, TUI overlays close).
- File formats as versioned interfaces.
- Terminal host process as a revision artifact.
- Durable documents shared across plugins.
- Session delete mechanics (section 17).
closewith a reason (reload/end/stop).- Call-scoped natives.
- Async activation guard for authority-carrying addresses.
- Invariants across several owners.
- Published projections with a generation fence.
- Connection pools; config files with a single writer.
- Rollout fixes (section 14); wire; presence;
overlap: "latest"; project plugins; jobs as objects. - Step 2 objects for every domain, starting with
SessionFiles.
22. Tracked, not scheduled (changes.md)
- Leaks C3 (Chord kept listener), C5 (p2 guard listener per reload), C7 (harness keeps envs
close); rerun GC tests. - p2 hazards on interrupted calls (shell timer, dedupe in a closure, withdrawn text only in replies, web send while disconnected, missing request ids, dropped terminal keys, multi-step revocation/relocate/clone).
- Durable nested commits hang by convention only.
- #10959 (remote references accept a foreign ctx), owner fixes via subagent.
23. Rejected (all)
Drain-based handover; version fences without transactions; object restart for state changes; Durable Session as object
store; volatile documents; per-group lines; generic call/request ids; optimistic transactions; outbox for transient
delivery errors; revoking proxies on .value; late-bound registry entries; dynamic registries; restart_required;
detached references counting toward presence; contracts taken from the running p2; ceremony (native/ dirs,
defineNative, lints); weakening type checking; "magic" input-field matching for delete; factory trial; migrate dry
run; consumer contract check during prepare; reload votes; defaultProjectTrust: "always"; loader variants A and B.
24. Conflicts found between the documents
target-design.md§15 andplugin-loading.md7.5: "a facet whose setup throws keeps its previous version while the rest of the host moves on". The decision log replaced this with all-or-nothing rollback per host.questions.mdkeeps its store in a facetself.resource. Facets may not open natives now, andself.resourceis gone: the store must move into an object (e.g. a host-lifetimeQuestionStorewithself.open, or plain documents if in-memory is enough).target-design.md§5 example (self.resourcein the facet,deps) and §12 (resources replaced in place) are superseded byself.openin objects.target-design.md§7 "handles belong to their code generation ...stale" assumed in-place resource replacement; with pure swap and native restart, whether old-generation handles still needstaleis unclear.plugin-loading.md9.2a gap list mentionsopaquemembers and unversioned registries; both are addressed by host values and versioned registries.