- References are Ref<C> (was Handle<C>), objects ObjectImpl (record.ts is object.ts), ImplementationOf<C> (was
BehaviorOf<C>), ResourceDeclaration (was ResourceSpec); epochs are code versions (codeVersion in Ctx, RootOptions,
Shape, Host, frames and reload reports). Docs say object and reference.
- A remote object's variant is named variant (was kind): factories return { variant }, references expose ref.variant.
- Links announce a protocol version (PROTOCOL_VERSION = 2) in hello; peers of different versions close the link with
reason protocol-version <n> and report it through LinkOptions.onProtocolMismatch.
- ServeOptions.lifetime ("referenced" | "host") replaces eager: a host-lifetime object is local, has an empty input,
exists once per host and lives until the host closes; awaitEager is awaitHostObjects. A reload that makes a type
referenced releases the host's own reference.
33 KiB
Chord specification
This document specifies the runtime in src. Where it and the code disagree, the disagreement is a bug. Known limitations and decisions lists what the runtime does not do yet and the choices the sections below leave open.
Hard constraints:
- Chord's runtime depends only on its own
deltamodule (tracker, applyImmutable, encoder/decoder, diff) and Node/browser built-ins plustypeboxfor schemas. No other Pi packages. - Built-in plugins are trusted, loaded from source in-process. The plugin loader uses dynamic
import()with a cache-busting query (owner-approved exception,TODO(loader): replace with an isolating, evicting loader; old module copies leak; TUI plugins need isolation). - Hot reload is required. Host restart only for Chord/runtime code changes or host-level crash recovery.
1. Concepts
| Term | Meaning |
|---|---|
| facet | A plugin's code for one host (surface): it serves object types (defineFacet). |
| object type | Name + contract. remote (addressable from other hosts; schemas for input, variant state members, method args/results) or local (host-only; members may be anything, including functions and native objects, but must be listed in the contract: method, opaque or a state schema). |
| factory | (input, principal, self) => implementation. Synchronous. Registered with host.serve(type, factory, options) from a plugin facet. |
| object | One instance of an object type. Stable identity per (type, canonicalInput, principalKey). Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |
| implementation | The object's code: what a factory run returns (methods + state members; variant for remote types). Immutable, per code version. |
| reference | What get returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |
| principal | App-defined. Stamped by links. In-host code acts as the host principal. |
| root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, host.root). Carries { rootId, codeVersion }. |
| code version | One committed set of implementations/registrations per host (server-issued id). |
| code holder | Long-lived execution that is NOT a root: Durable phases/tool runs, browsers, self.task loops. Every Chord call they make starts a fresh short root. |
2. Contracts
Contracts are runtime values for every type (shipped bundles have no TypeScript; third-party plugins are built separately). TS types are inferred from them.
export const Chat = remote("pi.chat", {
version: 1, // contract version: a plain number, bumped only on breaking changes
input: Type.Object({ session: Type.String(), conversation: Type.Number() }),
variants: {
viewer: { state: { view: ViewSchema } },
participant: {
state: { view: ViewSchema },
methods: { send: method(SendSchema, Type.Object({ submissionId: Type.Number() })) },
},
},
});
export const Index = local("pi.index", {
version: 1,
input: Type.Object({ session: Type.String() }),
members: {
search: method(Type.Object({ query: Type.String() }), Type.Array(Hit)),
onChange: method(Type.Function([Change], Type.Void()), Unsubscribe), // non-JSON allowed for locals
harness: opaque("pi.durable-harness@1"), // native object, by name+version
},
});
- Input identity: normalize (apply defaults, strip unknown), validate, then canonical JSON with fields equal to their schema default omitted. Changing defaults or normalization requires a new type name.
- Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.
- Local contracts list every member (
members: { search: method(...), harness: opaque(...), status: Schema }). A reference that is stillloadingmust know whetherh.xis a method (a caller that waits until live), a state member (a readable) or an opaque value, before any implementation exists. Members missing from the contract are found only once the reference is live. - Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.
Compatibility rule
A consumer built against version M binds to a provider at version N iff N = M and the provider's description covers the consumer's; otherwise only through accepts[M] adapters declared by the provider (serve(type, factory, { accepts: { 1: (current) => v1View } })); else contract_mismatch.
Covers: every consumer member exists with the same kind; methods: provider accepts every argument the consumer can send (new fields optional, nothing newly required or retyped) and the provider's result fits the consumer's expected result (extra fields fine); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.
Additive changes need no bump. Breaking changes (remove/rename/retype, newly required, or a change of meaning with the same shape) bump the number. Same version + non-covering description = forgotten bump, refused. Meaning changes without a bump are undetectable (author's responsibility, as with Durable document versions).
Checked at: get binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote references (consumer token in the hold frame).
The exact host-owned reference to a lifetime: "host" object is a lifecycle pin, not a surviving old-contract consumer. When its
provider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before
releasing the old one so the same object, state and unchanged resources survive. It needs no accepts adapter
for its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens
and the normal coverage/adapter checks; state-version and migration checks are unchanged.
3. Factory and declarations
Factories are synchronous; returning a promise throws. Declarations are identified across factory reruns by property names in object literals (not call order, not separate string keys). Names are unique per object per declaration kind; a declaration method may be called more than once if names don't repeat. References are identified by their address.
host.serve(StatusStore, ({ session }, _host, self) => {
const state = self.state({
status: cell(StatusSchema, { running: false, tasks: 0 }), // schema required, initial value
errors: cell(Type.Array(Type.String()), []),
});
const catalog = self.get(Catalog, {}); // reference; identity = canonical address (+ at)
const res = self.resources({
watch: resource({
rev: 1, deps: [catalog], exclusive: false,
open: async ({ signal }) => openWatch(session, signal),
close: async (w) => w.close(),
}),
});
self.listen({
changes: listener(res.watch, (w) => w.events, (event, ctx) => state.status.change(ctx, (d) => { d.tasks = event.tasks; })),
});
self.tasks({ poll: async (ctx) => { /* loop until ctx.signal aborts */ } });
const derived = self.derive({ visible: derivation([state.status], (s) => project(s)) }); // synchronous
self.register(Routes, { "GET /status": handler }); // registry keys are the registry's own keys; `lifetime: "host"` objects only
return { status: derived.visible, report: (args, ctx) => { state.status.replace(ctx, args); return null; } };
}, {
state: { version: 2, migrate: { status: (old, from) => upgrade(old, from) } }, // migrate gets the old value and its version
linger: 30_000, // with lifetime "referenced" (the default) only
});
| Declaration | Survives reload | Notes |
|---|---|---|
state({ name: cell(schema, initial) }) |
same cell and value; migrated on state-version bump | Schema required (no TypeScript at runtime). change(ctx, draft => void), replace(ctx, value), value, subscribe. Writes through change touch only mutated paths. |
get(type, input, { at? }) |
kept if declared again (same address) | returns a reference |
resources({ name: resource({ rev, deps?, exclusive?, open, close }) }) |
kept unless rev/deps change |
open runs at prepare (non-exclusive) or at the gate (exclusive); reference exposes .value |
listen({ name: listener(source, select?, fn) }) |
same subscription while source (resource) and select (identity or source text) are unchanged; fn swapped |
each event starts a fresh root in the current code version; select is not re-run for a kept subscription |
tasks({ name: fn }) |
restarted with new code at commit (old one aborted via ctx.signal) | a code holder; calls inside start fresh roots |
derive({ name: derivation(sources, fn) }) |
kept; fn swapped | synchronous fn |
register(registry, { key: value }) |
value swapped at commit | lifetime: "host" objects only |
expose(resource) |
follows the resource | a local member (usually opaque) whose reference value is the resource's value once live |
State members of an implementation may be cells, derivations, or state members of references the object holds (they are defined once the object is live). cell.replace(ctx, value, { diff? }) publishes a structural diff when the new value shares containers with the old one (or diff: true), else r. Facet setup receives { serve, name, root, registry } of its host.
migrate per cell is (old, fromVersion) => new. An object may be several versions behind (author bumped more than once between reloads).
Removed vs guide: self.own, holdWhile, async factories, inferred dependencies.
Synchronous factory, asynchronous activation
self.get and self.resource only declare and return placeholders. Activation of an object:
- run the factory synchronously (declarations);
- await declared resources'
open()(e.g. connect a DB); - await declared references becoming live (dependencies activate first, coalesced);
- object is live: references
live,readyresolves.
Inside the factory, placeholders must not be used (resource.value throws not_ready; reference calls throw). Methods/listeners/tasks run only on live objects, so their dependencies are live. External consumers (UI, roots) holding a loading reference: calls wait until live (ctx cancellation, deadline); state members read undefined until hydrated. Activation failures propagate to dependents and all waiting gets; the next get retries.
Lifetime
serve(type, factory, { lifetime }):
lifetime |
Created | Ends | linger |
|---|---|---|---|
"referenced" (default) |
at the first get of its address |
after the last reference is released, plus linger |
allowed (default 0) |
"host" |
when its facet loads (or at serve); loading waits until it is live |
when the host closes, or a reload removes its facet | refused |
"host"is refused atservefor remote types, for an input that does not accept{}, and together withlinger. The type has exactly one object: the empty input (defaults applied), created for the host principal. Everygetreaches it, whatever its principal (the factory sees the host principal); agetwith another input isinvalid/input. Other references do not change its lifetime: the host keeps its own until it closes.- Only
"host"objects may register (invalid/not_host_lifetimeotherwise). - A
"host"object whose activation fails is started again by the next reload that changes its type. A reload that makes a type"host"creates its object; one that makes it"referenced"releases the host's reference, so the object lives on only while others reference it.
Host objects readiness
host.install(facet, { awaitHostObjects? })installs declarations synchronously and returns a promise for this facet's host object activations.host.load(facets, { awaitHostObjects? })installs the entire batch before awaiting its host object activations.awaitHostObjectsdefaults to true. Unreferenced objects are not activated.- Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the
activation error (ChordError codes preserved; ordinary errors wrapped as
unavailable). Host object failures also reachonError. Installation is not a rollback transaction; setup/registration errors frominstallthrow synchronously. Abandoned startup should close the host. { awaitHostObjects: false }starts the same host objects but resolves after installation, without waiting for their activation.host.ready()awaits a snapshot of the host's references to its host objects at call time (including directserveand deferred loads), rejects their activation failures, and does not acquire additional references. It is not an ongoing health check and does not await task completion or remote state hydration.- Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with
{ awaitHostObjects: false }, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then announces control readiness so its parent can accept the link. Only after admission does it awaithost.ready(). Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.
4. Principals, variants, references
createHost({ principalKey })maps principal to a key that includes connection provenance and grant revision. Objects are per principal key.hostis a built-in principal for in-host calls.- Remote factories return
{ variant, ...members };variantmust be a declared variant; members must match it. Local factories return any object.variantis reserved as a member name. - Reference:
status(loading | live | released | gone),readypromise,variant,shape({ variant, members, contract, codeVersion }),onStatus(listener)(every status change, with the error forgone), late-bound members,release(). A remote reference's state member holds a value only while subscribed (undefinedafter its last subscriber leaves). Shape changes (reload, variant) are pushed; members missing from the current variant rejectnot_in_variant; released references rejectreleased; forced disposal makes referencesgone. - Methods are
(args, ctx) => Promise<result>. Factories still check arguments; variants are not complete authorization. - Revocation:
host.invalidatePrincipal(match)force-disposes matching objects on this host and propagates over links; links re-stamp or close.
5. Lifetime
- Reference owners: object (declared
get), call (ctx.get, released at settle), connection (remote references; relay maps per origin connection), host (lifetime: "host"), UI. - Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.
- Disposal after the last reference is released +
linger: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), the object's references released (cascade). - Bulk forced disposal (
host.close,invalidatePrincipal) goes in waves, referrers before what they reference, so a resource'sclosecan still use its object's references;host.closecloses links after the objects. - Forced disposal: references
goneimmediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup. - Reference graph acyclic in-host (checked at get); across hosts by convention.
- Links have ping/timeout; a dead link releases everything held through it.
6. Roots and code versions
Ctx = { signal, rootId, codeVersion, get(...) , principal? }. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).- A link creates a root for a request that carries none; nested remote calls carry
{rootId, codeVersion}. - Dispatch: a call with code version E runs the object's implementation for E. A host retains at most
currentandpreviouscode versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejectedsuperseded(retryable by starting a new root). Never substitute another code version. - Browsers never carry code versions into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server
Manifestobject; incompatible contract fingerprint ->upgrade_required.
7. Reload
Explicit trigger (reload_plugins tool, button, API). Serialized; no nesting.
- Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.
- Load: on every affected host, import new facet modules (cache-busted).
- Prepare: for each live object of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; references recorded; non-exclusive resources whose key is new or
rev/depschanged are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New objects activated during prepare use the current code version (old code). Any failure: abort, discard candidates. Vote. - Gate (only objects whose state version was bumped or which replace an exclusive resource, plus objects depending on those resources):
- Admission cut: from now, an old-code-version root that has not touched the object gets
upgrade_conflict(retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted. - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).
- Exclusive resources: close old, open new; on failure reopen old and abort.
- Run
migrateinto private copies; validate.
- Admission cut: from now, an old-code-version root that has not touched the object gets
- Commit (server commits itself, then sends commit to children; the decision is not persisted, see section 12; FIFO links guarantee no host receives a new-code version root before committing): each host in one synchronous step installs migrated cell values and state versions, installs implementations as code version E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.
The coordinator's result carries
children: each participating child's prepared report (its vote). - Retire: when no roots of
previousremain (or on the next reload, which first waits for retirement with a deadline), drop its implementations and release declarations only it declared.
Compatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.
State compatibility rule: reload prepare compares each cell's old and new schema. Unchanged: compatible (no gate). Changed with the state version bumped: gate + migrate, output validated against the new schema. Changed without a bump: reload refused. A change of meaning with an identical schema needs a bump too (undetectable; author's responsibility). Initial values and migrate outputs are validated; per-write validation in dev mode.
8. Registries
export const Routes = defineRegistry<RouteHandler>("http.routes", { conflict: "error" });
self.register(Routes, { "GET /download": handler }); // in a `lifetime: "host"` object; keys are the registry's keys
host.registry(Routes).get("GET /download", ctx); // table of ctx.codeVersion (or current without ctx)
host.registry(Routes).subscribe((table, codeVersion) => ...); // bridges for imperative consumers (Durable)
Per-code-version immutable tables; order = first registration order, kept on replace. An application can implement Durable's RegistryReader from the table (snapshot per code version) so phases pin a complete snapshot; Chord does not ship that bridge.
9. Hosts and links
createHost({ name, principalKey, upstream? });host.accept(link)for children;host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link> }).- Links:
memoryLinkPair()(tests), Node IPC (child_process), WebSocket (serverws, browserWebSocket). - Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; payload principal fields ignored.
- A child that connects behind its parent's code version is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's code version. Calls its roots of the old code version make upstream are admitted in the parent's current code version (the parent never ran that code version), not rejected
superseded. - Relay: server forwards frames for
athosts to the resolved child link, mapping references per origin connection; authorizes before resolving/spawning and re-checks after. - Wire frames (JSON):
hello/bye,hold/release(a reference over a link:holdreturnsshapeorgone; there is no separateget),shape,call/result/cancel,sub/unsubandstate(snap/ops/resetwith a per-subscription delta encoder),gone,root_end,retire,invalidate,ping/pong,ack, reload control (prepare/vote/commit/abort). - Protocol version:
hellocarriesprotocol(PROTOCOL_VERSION, now 2; absent means 1). A side that sees another version processes nothing more, callsLinkOptions.onProtocolMismatch({ local, peer })and closes the link with reasonprotocol-version <its version>; the other side's connection reads that reason and reports the mismatch too. Peers do not negotiate: the outdated side must load the current code (a browser reloads its page). Frame fields follow the version: theshapeframe names the variantvariant, code versions travel ascodeVersion. - Flow control: both ends announce
acksin their hello and acknowledge what they processed (ack, a cumulative count of every frame butack). While the frames not yet acknowledged exceed the window (window, default 256 KiB of JSON text), state frames wait at the sender, one slot per subscription: ops are appended, a snapshot or reset replaces the slot, an unsubscription drops it, and a slot that outgrows four windows becomes a reset of the current value (a relay subscribes downstream anew instead). Every other frame is sent at once. Links that report no frame sizes (IPC) are never windowed; a peer withoutacksis neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (maxCalls); calls beyond it are answered at once withunavailable/too_many_calls. Nothing outside the connection may add frames to a link: acknowledgements count them.
10. Host manager (library)
createHostManager({ driver, keepAlive }): fork driver (entry, init, lock, env), serialized start/stop/replace, exit-before-replace, process-tree kill after grace, control channel (ready/fatal/shutdown), liveness callback (booting, activeCalls, viewers with heartbeats, durable tasks, idleFor).
11. Test coverage
The tests cover at least: lifetime/references/linger/forced disposal/gone; activation coalescing and abandonment; principals and variants; remote calls over memory links incl. relay and principal forgery attempts; state snap/ops/reset over a link with reconnect; registries per code version; reload: compatible (no wait, pinned roots), prepare isolation (writes throw, failing factory leaves everything unchanged), state bump with migrate and gate, upgrade_conflict for late old roots, deadline abort, exclusive resource replacement, cross-host commit ordering, two-version retirement and superseded.
12. Known limitations and decisions
Still open
- Retirement across hosts. A child retires a code version only after its parent retired it (
retireframe) and its own roots of that code version ended; the server decides from its own roots. A root started on a worker that calls up to a server which has already retired the code version both had still getssuperseded(retryable). - The commit decision is not persisted (section 7, step 5). A server crash between its own commit and a child's commit leaves that child on the old code version until it reconnects and is reconciled.
- One level of reload participants. Only the coordinator's direct child hosts take part; a child does not forward
prepareto its own children. - Reconciliation needs the child's module list. It is remembered from cluster reloads (
host.remoteModules), so a server restart forgets it, and a child behind a restarted server then reloads no modules: it gets the code version id with its own code. The reconciliation vote deadline is fixed at 30 s; a failed reconciliation closes the link, so the host manager restarts the worker. - Links do not resume. The provider releases everything held through a closed link at once, so re-held objects are new ones. Keeping references for a reconnect window would need session ids on links. Calls in flight when a link drops reject
unavailable; whether they ran is unknown. - Refused clients cannot tell. The WebSocket adapter closes unauthenticated sockets; the client sees a dropped upstream and its calls wait for a reconnect until their ctx is aborted.
- The publication queue is process-wide and synchronous. A slow observer delays every later notification in the process, including those of other hosts in the same process.
- No automatic restart. A terminal host failure is picked up by the host manager's
keepAliveorfatalpath, not inside Chord. - The Node loader leaks. Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (
TODO(loader)). - Stale socket locks. On Unix systems other than Linux, two processes that probe the same stale socket file at the same moment could both take the lock. Managers serialize starts per lock name.
- Listener
selectidentity. A listener's subscription is kept across factory runs when its origin andselectare unchanged;selectcounts as unchanged when identical or with the same source text, so aselectwhose implementation depends on captured values that change between runs is not detected. - Shutdown and dead links.
host.close()disposes objects in waves (referrers before what they reference) and closes links last. Aclosethat awaits a remote call over a link that is already down waits for a reconnect; bound such calls.
Decisions
- Errors. The guide's codes; a
reasonfield carries the detail, for exampleinvalid/args,invalid/cycle,invalid/duplicate,invalid/read_only,invalid/not_host_lifetime,invalid/registry_conflict, orunavailable/unknown_type. AChordErrorthrown by a factory or a dependency keeps its code through every dependent; other errors becomeunavailable, with the original ascause. - Reload errors. There is no
reload_abortedcode. A failed reload throws the cause's code and reason with the messagereload aborted: …(the cause is attached). Reasons of its own areaborted/gate_deadline(with the blocking roots indetails),aborted/deadline,aborted/vote_deadlineandunavailable/reload_running. Across hosts, the message names the host that failed. A child cannot reload on its own (unavailable/not_coordinator). - New objects during prepare. A reload pre-allocates the next code version ("pending"). Types the same reload adds are served from it; their objects install implementation only for the pending code version and are force-disposed if the reload aborts.
- Gate admission. From the gate until commit or abort, creating a new object of a changed type fails with
upgrade_conflict(retryable, reasongate); nothing is paused, so nothing can deadlock. Gated objects getminCodeVersion = E': calls and writes carrying an older code version are rejectedupgrade_conflictpermanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose state version or exclusive resources change are gated. - Tasks and listeners during the gate. Tasks of gated objects are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated objects are queued and replayed through the new listener at commit, or the old one at abort.
- Grandfathered roots are the roots that called into the object and are still running at the cut. Every host remembers on which links a root's calls went out and sends
root_endon them when the root ends, so leases outlive calls across hosts. - Removed types and missed retirement. Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced, so those roots get
supersededon their next call. - Two-version rule. An object of a changed type created after a commit has only new code; a previous-code-version root calling it gets
superseded. - Validation. Remote calls are validated at reference dispatch, in-host too: a direct binding against the provider's schema, a binding through an adapter against the consumer's. A provider validates each snapshot and reset of a remote state member once per (re)subscription, against the consumer's contract through an
acceptsadapter and the provider's otherwise; ops are validated only in dev mode. At reload, the new provider's description must cover the old provider's when the version is unchanged, in addition to the check against live consumers and facets'uses. - Contracts. Local contracts list their members, so a loading reference knows whether a member is a method, state or opaque. Canonical identity omits input fields equal to their default; with TypeBox, use
Type.Optional(Type.X({ default }))to let callers omit them. Provider variants must cover consumer variants; members the consumer's contract does not list are hidden. Schema change detection compares canonical JSON, so reorderinganyOfbranches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted. - Principals. A root ctx at ingress acts as its principal (
host.root(fn, { principal })); contexts inside objects act ashost. The acceptor decides a link's kind and name (accept(link, { principal, kind, name })); only configured host links are addressable, carry roots and may vote, and control frames are accepted only from the parent. Clients never carry roots or code versions. Relaying needs a matchingroute;authorize(principal, hostName)runs beforeresolveand again after, and revoking or re-stamping a principal closes its relays at once.invalidatePrincipal(match)re-stamps or closes matching links and propagates pattern matches to child hosts. - Cells and readables. A value returned from
changereplaces the value and is always validated;replacetransfers ownership without copying; by default ({ diff }overrides) it publishes a diff when the new value reuses a container of the old one at the same key, elser.subscribe(fn)delivers values, starting with the current one;events(fn)deliverssnap, thenops, orreset. Reference state members are stable readables,undefinedwhile loading or outside the variant, and reset when their source moves. Remote state members hydrate only while subscribed and forget their value when the last listener leaves. - References and calls. A reference's identity is its address; a second
self.getof the same address in one run returns the same reference. A reference call without ctx, or with a task ctx, starts a fresh root in the current code version; a ctx whose call or root already settled is refused (aborted/expired), one from another hostinvalid/foreign_ctx. Calling a reference inside a factory throwsnot_ready. - Resources and disposal. New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same object that depend on them. A new object for an address that is still being disposed waits for that disposal. A resource open that completes after its signal aborted closes its value at once.
- Links and the host manager. Every received frame is validated against its schema; malformed frames, duplicate ids and control frames from the wrong side close the link. Each connection pings every
pingIntervaland closes afterpingTimeoutwithout a frame. The child holds a kernel lock (an abstract socket on Linux and Android, a named pipe on Windows, a probed socket file elsewhere), acquired bychildControl().lock()before storage opens. The manager kills the child's process group on every exit path and derivesbootingandidleForfrom the child's liveness reports. WebSocket support is structural, so Chord does not depend onws. - Flow control holds back state only. A relay (Cloudflare to a browser) can hide a slow reader, so transport backpressure cannot be relied on; acknowledgements are end to end. Results must not wait behind state, so links are FIFO except that waiting state frames can be overtaken: a result may arrive before state changes made before it. Frames of one subscription stay in order. Waiting state is coalesced rather than queued, so a stalled peer costs at most a bounded slot per subscription, and the next frame it gets is current.
- The state API stays. Objects keep named cells and derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.