@earendil-works/chord 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +69 -14
  3. package/dist/cell.d.ts +30 -9
  4. package/dist/cell.d.ts.map +1 -1
  5. package/dist/cell.js +52 -12
  6. package/dist/cell.js.map +1 -1
  7. package/dist/cluster.d.ts +4 -3
  8. package/dist/cluster.d.ts.map +1 -1
  9. package/dist/cluster.js +58 -42
  10. package/dist/cluster.js.map +1 -1
  11. package/dist/connection.d.ts +171 -58
  12. package/dist/connection.d.ts.map +1 -1
  13. package/dist/connection.js +676 -217
  14. package/dist/connection.js.map +1 -1
  15. package/dist/context/index.d.ts +8 -0
  16. package/dist/context/index.d.ts.map +1 -1
  17. package/dist/context/index.js +15 -0
  18. package/dist/context/index.js.map +1 -1
  19. package/dist/contract.d.ts +120 -45
  20. package/dist/contract.d.ts.map +1 -1
  21. package/dist/contract.js +104 -15
  22. package/dist/contract.js.map +1 -1
  23. package/dist/declare.d.ts +11 -48
  24. package/dist/declare.d.ts.map +1 -1
  25. package/dist/declare.js +1 -21
  26. package/dist/declare.js.map +1 -1
  27. package/dist/delta/index.d.ts +2 -1
  28. package/dist/delta/index.d.ts.map +1 -1
  29. package/dist/delta/index.js +4 -0
  30. package/dist/delta/index.js.map +1 -1
  31. package/dist/errors.d.ts +4 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +15 -0
  34. package/dist/errors.js.map +1 -1
  35. package/dist/host.d.ts +214 -120
  36. package/dist/host.d.ts.map +1 -1
  37. package/dist/host.js +446 -241
  38. package/dist/host.js.map +1 -1
  39. package/dist/index.d.ts +8 -8
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +4 -4
  42. package/dist/index.js.map +1 -1
  43. package/dist/link.d.ts +47 -12
  44. package/dist/link.d.ts.map +1 -1
  45. package/dist/link.js +200 -14
  46. package/dist/link.js.map +1 -1
  47. package/dist/loader.d.ts.map +1 -1
  48. package/dist/loader.js +7 -2
  49. package/dist/loader.js.map +1 -1
  50. package/dist/manager.d.ts.map +1 -1
  51. package/dist/manager.js +35 -9
  52. package/dist/manager.js.map +1 -1
  53. package/dist/object.d.ts +438 -0
  54. package/dist/object.d.ts.map +1 -0
  55. package/dist/{record.js → object.js} +614 -335
  56. package/dist/object.js.map +1 -0
  57. package/dist/output.d.ts +91 -0
  58. package/dist/output.d.ts.map +1 -0
  59. package/dist/output.js +360 -0
  60. package/dist/output.js.map +1 -0
  61. package/dist/registry.d.ts +3 -3
  62. package/dist/registry.d.ts.map +1 -1
  63. package/dist/registry.js +1 -1
  64. package/dist/registry.js.map +1 -1
  65. package/dist/reload.d.ts +35 -27
  66. package/dist/reload.d.ts.map +1 -1
  67. package/dist/reload.js +256 -192
  68. package/dist/reload.js.map +1 -1
  69. package/dist/remote.d.ts +130 -35
  70. package/dist/remote.d.ts.map +1 -1
  71. package/dist/remote.js +493 -189
  72. package/dist/remote.js.map +1 -1
  73. package/dist/sqlite/cloudflare.d.ts +52 -0
  74. package/dist/sqlite/cloudflare.d.ts.map +1 -0
  75. package/dist/sqlite/cloudflare.js +134 -0
  76. package/dist/sqlite/cloudflare.js.map +1 -0
  77. package/dist/sqlite/database.d.ts +37 -0
  78. package/dist/sqlite/database.d.ts.map +1 -0
  79. package/dist/sqlite/database.js +2 -0
  80. package/dist/sqlite/database.js.map +1 -0
  81. package/dist/sqlite/index.d.ts +4 -0
  82. package/dist/sqlite/index.d.ts.map +1 -0
  83. package/dist/sqlite/index.js +3 -0
  84. package/dist/sqlite/index.js.map +1 -0
  85. package/dist/sqlite/migrations.d.ts +10 -0
  86. package/dist/sqlite/migrations.d.ts.map +1 -0
  87. package/dist/sqlite/migrations.js +33 -0
  88. package/dist/sqlite/migrations.js.map +1 -0
  89. package/dist/sqlite/node.d.ts +38 -0
  90. package/dist/sqlite/node.d.ts.map +1 -0
  91. package/dist/sqlite/node.js +174 -0
  92. package/dist/sqlite/node.js.map +1 -0
  93. package/dist/sqlite/schema.d.ts +3 -0
  94. package/dist/sqlite/schema.d.ts.map +1 -0
  95. package/dist/sqlite/schema.js +10 -0
  96. package/dist/sqlite/schema.js.map +1 -0
  97. package/dist/telemetry-sqlite/index.d.ts +24 -0
  98. package/dist/telemetry-sqlite/index.d.ts.map +1 -0
  99. package/dist/telemetry-sqlite/index.js +273 -0
  100. package/dist/telemetry-sqlite/index.js.map +1 -0
  101. package/dist/telemetry-sqlite/store.d.ts +9 -0
  102. package/dist/telemetry-sqlite/store.d.ts.map +1 -0
  103. package/dist/telemetry-sqlite/store.js +110 -0
  104. package/dist/telemetry-sqlite/store.js.map +1 -0
  105. package/dist/types.d.ts +39 -28
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js.map +1 -1
  108. package/dist/websocket.d.ts +4 -5
  109. package/dist/websocket.d.ts.map +1 -1
  110. package/dist/websocket.js +3 -2
  111. package/dist/websocket.js.map +1 -1
  112. package/dist/wire.d.ts +210 -129
  113. package/dist/wire.d.ts.map +1 -1
  114. package/dist/wire.js +108 -55
  115. package/dist/wire.js.map +1 -1
  116. package/docs/guide.md +267 -203
  117. package/docs/spec.md +141 -114
  118. package/docs/tutorial.md +257 -235
  119. package/package.json +27 -1
  120. package/dist/record.d.ts +0 -320
  121. package/dist/record.d.ts.map +0 -1
  122. package/dist/record.js.map +0 -1
package/docs/spec.md CHANGED
@@ -11,15 +11,15 @@ Hard constraints:
11
11
 
12
12
  | Term | Meaning |
13
13
  | --- | --- |
14
- | 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). |
15
- | factory | `(input, principal, self) => behavior`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |
16
- | record | Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, holds, listeners, tasks, registrations. Survives reloads. |
17
- | behavior | What a factory run returns (methods + state members; `kind` for remote variants). Immutable, per epoch. |
18
- | handle | What `get` returns. Late-bound. Never the record or behavior. |
19
- | hold | One handle's claim. A record lives while held. |
14
+ | facet | A plugin's code for one host (surface): it serves object types (`defineFacet`). |
15
+ | 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). |
16
+ | factory | `(input, principal, self) => implementation`. Synchronous. Registered with `host.serve(type, factory, options)` from a plugin facet. |
17
+ | object | One instance of an object type. Stable identity per `(type, canonicalInput, principalKey)`. Owns cells, resources, references, listeners, tasks, registrations. Survives reloads. |
18
+ | implementation | The object's code: what a factory run returns (methods + state members; `variant` for remote types). Immutable, per code version. |
19
+ | reference | What `get` returns: a late-bound claim on an object, never the object or its implementation. An object lives while referenced. |
20
20
  | principal | App-defined. Stamped by links. In-host code acts as the `host` principal. |
21
- | root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, epoch }`. |
22
- | epoch | One committed set of behaviors/registrations per host (server-issued id). |
21
+ | root | One short Chord call tree, admitted at trusted ingress (link request without root, UI event, timer, `host.root`). Carries `{ rootId, codeVersion }`. |
22
+ | code version | One committed set of implementations/registrations per host (server-issued id). |
23
23
  | 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. |
24
24
 
25
25
  ## 2. Contracts
@@ -30,6 +30,7 @@ Contracts are runtime values for every type (shipped bundles have no TypeScript;
30
30
  export const Chat = remote("pi.chat", {
31
31
  version: 1, // contract version: a plain number, bumped only on breaking changes
32
32
  input: Type.Object({ session: Type.String(), conversation: Type.Number() }),
33
+ host: hostFamily("session:", "session"), // where its objects live: `session:` + input.session
33
34
  variants: {
34
35
  viewer: { state: { view: ViewSchema } },
35
36
  participant: {
@@ -52,129 +53,150 @@ export const Index = local("pi.index", {
52
53
 
53
54
  - 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.
54
55
  - Remote calls validate args/results and state values at the provider boundary. Local calls are not validated per call.
55
- - Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A handle that is still `loading` must know whether `h.x` is a method (a caller that waits until live), a state member (a readable) or an opaque value, before any behavior exists. Members missing from the contract are found only once the handle is live.
56
+ - Local contracts list every member (`members: { search: method(...), harness: opaque(...), status: Schema }`). A reference that is still `loading` must know whether `h.x` is 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.
56
57
  - Every bundle embeds the contract tokens it was built against (its own copy). Fingerprint = hash of the canonical description.
58
+ - Placement (remote types, required): `host: hostNamed(name)` (one host) or `hostFamily(prefix, key)` (the host `prefix` + the input's required string field `key`). Data only, in the description (and so the fingerprint and the `hold` token); there is no `at` option. A `get` normalizes the input with the caller's contract, resolves the host and acquires locally if that is this host, else remotely. `serve` (and a reload that loads a facet) refuses a type whose placement excludes the host's name. A host refuses a hold whose input its own contract places elsewhere (`invalid/placement`), and a relay refuses a hold whose target disagrees with its token's placement and input. Coverage requires equal placements, and an `accepts` adapter never bridges two placements: a moved type is a breaking change. A remote reference's address is the type, the normalized input and the principal: no host name.
57
59
 
58
60
  ### Compatibility rule
59
61
 
60
62
  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`.
61
63
 
62
- 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.
64
+ Covers: every consumer member exists with the same kind; methods: both take arguments or neither does, the 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; a void consumer accepts any); state: provider value fits consumer schema (extra fields fine); opaque: same name@version.
63
65
 
64
66
  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).
65
67
 
66
- Checked at: `get` binding, reload prepare (new provider token vs every surviving consumer holding the type and every loaded plugin importing it), remote holds (consumer token in the hold frame).
68
+ 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).
67
69
 
68
- The exact host-owned eager bootstrap hold is a lifecycle pin, not a surviving old-contract consumer. When its
69
- provider is replaced, commit replaces that hold with one bound to the new provider contract, acquiring it before
70
- releasing the old hold so the same record, state and unchanged resources survive. It needs no `accepts` adapter
71
- for its own previous contract. Prepare/abort do not change that hold. All other consumers retain their own tokens
70
+ The exact host-owned reference to a `lifetime: "host"` object is a lifecycle pin, not a surviving old-contract consumer. When its
71
+ provider is replaced, commit replaces that reference with one bound to the new provider contract, acquiring it before
72
+ releasing the old one so the same object, state and unchanged resources survive. It needs no `accepts` adapter
73
+ for its own previous contract. Prepare/abort do not change that reference. All other consumers retain their own tokens
72
74
  and the normal coverage/adapter checks; state-version and migration checks are unchanged.
73
75
 
74
76
  ## 3. Factory and declarations
75
77
 
76
- 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 record per declaration kind; a declaration method may be called more than once if names don't repeat. Holds are identified by their address.
78
+ Factories are synchronous; returning a promise throws. A declaration has a name only if something of it survives a reload and must be found again: state (its value) and resources (the open value), plus listeners (a kept subscription). Names are strings, unique per object per declaration kind (a repeated name is `invalid/duplicate`); kinds have separate names. Derivations and tasks have no names: a reload makes new ones. References are identified by their address.
77
79
 
78
80
  ```ts
79
81
  host.serve(StatusStore, ({ session }, _host, self) => {
80
- const state = self.state({
81
- status: cell(StatusSchema, { running: false, tasks: 0 }), // schema required, initial value
82
- errors: cell(Type.Array(Type.String()), []),
82
+ const status = self.state("status", StatusSchema, { running: false, tasks: 0 }, // schema required, initial value
83
+ { version: 2, migrate: (old, from) => upgrade(old, from) }); // per cell; default version 1
84
+ const errors = self.state("errors", Type.Array(Type.String()), []);
85
+ const catalog = self.get(Catalog, {}); // reference; identity = canonical address (+ at)
86
+ const watch = self.resource("watch", {
87
+ version: 1, deps: [catalog], exclusive: false,
88
+ open: async ({ signal }) => openWatch(session, signal),
89
+ close: async (w) => w.close(),
83
90
  });
84
- const catalog = self.get(Catalog, {}); // hold; identity = canonical address (+ at)
85
- const res = self.resources({
86
- watch: resource({
87
- rev: 1, deps: [catalog], exclusive: false,
88
- open: async ({ signal }) => openWatch(session, signal),
89
- close: async (w) => w.close(),
90
- }),
91
- });
92
- self.listen({
93
- changes: listener(res.watch, (w) => w.events, (event, ctx) => state.status.change(ctx, (d) => { d.tasks = event.tasks; })),
94
- });
95
- self.tasks({ poll: async (ctx) => { /* loop until ctx.signal aborts */ } });
96
- const derived = self.derive({ visible: derivation([state.status], (s) => project(s)) }); // synchronous
97
- self.register(Routes, { "GET /status": handler }); // registry keys are the registry's own keys; eager records only
98
- return { status: derived.visible, report: (args, ctx) => { state.status.replace(ctx, args); return null; } };
91
+ self.on("changes", watch, (w) => w.events, (event, ctx) => status.change(ctx, (d) => { d.tasks = event.tasks; }));
92
+ self.task(async (ctx) => { /* loop until ctx.signal aborts */ });
93
+ const visible = self.derive([status], (s) => project(s)); // synchronous
94
+ self.register(Routes, { "GET /status": handler }); // registry keys are the registry's own keys; `lifetime: "host"` objects only
95
+ return { status: visible, errors, report: (args, ctx) => { status.replace(ctx, args); } }; // exposed state = what is returned
99
96
  }, {
100
- state: { version: 2, migrate: { status: (old, from) => upgrade(old, from) } }, // migrate gets the old value and its version
101
- linger: 30_000,
102
- eager: false,
97
+ linger: 30_000, // with lifetime "referenced" (the default) only
103
98
  });
104
99
  ```
105
100
 
106
101
  | Declaration | Survives reload | Notes |
107
102
  | --- | --- | --- |
108
- | `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. |
109
- | `get(type, input, { at? })` | kept if declared again (same address) | returns a handle |
110
- | `resources({ name: resource({ rev, deps?, exclusive?, open, close }) })` | kept unless `rev`/`deps` change | `open` runs at prepare (non-exclusive) or at the gate (exclusive); handle exposes `.value` |
111
- | `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 epoch; `select` is not re-run for a kept subscription |
112
- | `tasks({ name: fn })` | restarted with new code at commit (old one aborted via ctx.signal) | a code holder; calls inside start fresh roots |
113
- | `derive({ name: derivation(sources, fn) })` | kept; fn swapped | synchronous fn |
114
- | `register(registry, { key: value })` | value swapped at commit | eager records only (v1) |
115
- | `expose(resource)` | follows the resource | a local member (usually `opaque`) whose handle value is the resource's value once live |
103
+ | `state(name, schema, initial \| { init }, { version?, migrate? })` | same cell and value (`initial` ignored, `init` not run); migrated when its version rises; a cell the reload adds runs `init` at prepare (gated objects: at the gate) | Schema required (no TypeScript at runtime). `change(ctx, draft => void)`, `replace(ctx, value)`, `value`, `subscribe`. Writes through `change` touch only mutated paths. Private state is a cell the implementation does not return. `init(ctx)` (may be async) gives the first value, see activation |
104
+ | `get(type, input, { at? })` | kept if declared again (same address) | returns a reference |
105
+ | `resource(name, { version, deps?, exclusive?, open, close? })` | kept unless `version`, `deps` or `exclusive` change; a kept resource adopts the new declaration (its `close`) at commit | `open` runs at prepare (non-exclusive) or at the gate (exclusive); returns a resource whose `.value` is the open value; a replaced resource closes with its own declaration's `close` |
106
+ | `on(name, source, [select,] handler)` | same subscription while source and `select` (identity or source text) are unchanged; `handler` swapped | each event starts a fresh root in the current code version; `select` is not re-run for a kept subscription. State sources deliver their current value at start and after every reload that replaced them |
107
+ | `task(fn)` | restarted with new code at commit: the old run is aborted via ctx.signal, and the new one starts once it finished (at most `taskStopDeadline`, default 5 s, then anyway, reported) | a code holder; calls inside start fresh roots |
108
+ | `derive(sources, fn)` | a new derivation, computed at commit | synchronous fn |
109
+ | `register(registry, { key: value })` | value swapped at commit | `lifetime: "host"` objects only |
110
+ | `expose(resource)` | follows the resource | a local member (usually `opaque`) whose reference value is the resource's value once live |
111
+
112
+ 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, signal }` of its host; `signal` aborts once this run of the facet is no longer installed (a committed reload replaced or removed it, the reload that loaded it aborted, or the host closed).
113
+
114
+ `migrate` per cell is `(old, fromVersion) => new`. A cell may be several versions behind (author bumped more than once between reloads). Without `migrate`, a raised version keeps the value, which must fit the new schema.
116
115
 
117
- State members of a behavior may be cells, derivations, or state members of handles the record holds (they are defined once the record 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.
116
+ **Listeners on state reconcile.** A listener whose source is state (a cell, a derivation, a reference's state member) is subscribed when it starts and gets the current value, then every new value. A reload that replaces its source (every derivation is new; a reference to another address is another source) subscribes it again, so it gets the current value again: its handler must reconcile (be idempotent), not count events. An event source (a resource's events, any other `subscribe`) delivers no current value; a kept subscription to it is not touched by a reload.
118
117
 
119
- `migrate` per cell is `(old, fromVersion) => new`. A record may be several versions behind (author bumped more than once between reloads).
118
+ **Methods.** `method(args?, result?)`: without `args` the method takes none (callers `ref.name(ctx?)`, implementation `(ctx) => …`; `method(undefined, result)` for one that returns something); without `result` it is void (callers get `undefined`, whatever the implementation returned). The description records both (absent fields), and the `call` frame carries no `args` and the `result` frame no `value` for them. A consumer and provider must agree on whether a method takes arguments; a consumer without a result accepts a provider's.
120
119
 
121
120
  Removed vs guide: `self.own`, `holdWhile`, async factories, inferred dependencies.
122
121
 
123
122
  ### Synchronous factory, asynchronous activation
124
123
 
125
- `self.get` and `self.resource` only declare and return placeholders. Activation of a record:
124
+ `self.get` and `self.resource` only declare and return placeholders. Activation of an object:
126
125
 
127
126
  1. run the factory synchronously (declarations);
128
127
  2. await declared resources' `open()` (e.g. connect a DB);
129
- 3. await declared holds becoming live (dependencies activate first, coalesced);
130
- 4. record is live: handles `live`, `ready` resolves.
128
+ 3. await declared references becoming live (dependencies activate first, coalesced);
129
+ 4. run the `init` of every cell declared with one, in parallel, each in a root of its own acting as the host principal; validate and install the values;
130
+ 5. attach derivations, start listeners and tasks;
131
+ 6. object is live: references `live`, `ready` resolves.
132
+
133
+ Inside the factory, placeholders must not be used (`resource.value` and an `init` cell's `value` throw `not_ready`; reference calls throw). An `init` that throws, or returns a value its schema refuses, fails the activation like a failed `open`. An `init` must not wait for its own object or one that holds it (it is not live yet), and must not read another `init` cell of its object (they run in parallel). `init` gives a first value from a source the object reads anyway; it does not persist anything, and a cell is not saved anywhere by having one. Methods/listeners/tasks run only on live objects, so their dependencies are live. An object goes live only once its references are ready (live and, for remote ones, hydrated) and the state sources it exposes or derives from have their values, also where they forward a remote reference held some other way; waiting for references is bounded by the host's `readyTimeout` (default 60 s, `unavailable/ready_timeout`, also for a reference cycle across hosts). External consumers holding a `loading` reference (`host.hold`): calls wait until live (ctx cancellation, deadline); state members throw `not_ready` until the first hydration. Activation failures propagate to dependents and all waiting gets; the next get retries.
134
+
135
+ ### Lifetime
136
+
137
+ `serve(type, factory, { lifetime })`:
138
+
139
+ | `lifetime` | Created | Ends | `linger` |
140
+ | --- | --- | --- | --- |
141
+ | `"referenced"` (default) | at the first `get` of its address | after the last reference is released, plus `linger` | allowed (default 0) |
142
+ | `"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 |
131
143
 
132
- Inside the factory, placeholders must not be used (`resource.value` throws `not_ready`; handle calls throw). Methods/listeners/tasks run only on live records, so their dependencies are live. External consumers (UI, roots) holding a `loading` handle: 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.
144
+ - `"host"` is refused at `serve` for remote types, for an input that does not accept `{}`, and together with `linger`;
145
+ the types (`ServeOptionsFor`) refuse the same at compile time, where such an input is one with a required field.
146
+ The type has exactly one object: the empty input (defaults applied), created for the host principal. Every `get`
147
+ reaches it, whatever its principal (the factory sees the host principal); a `get` with another input is
148
+ `invalid/input`. Other references do not change its lifetime: the host keeps its own until it closes.
149
+ - Only `"host"` objects may register (`invalid/not_host_lifetime` otherwise).
150
+ - A `"host"` object whose activation fails is started again by the next reload that changes its type. A reload that
151
+ makes a type `"host"` creates its object; one that makes it `"referenced"` releases the host's reference, so the
152
+ object lives on only while others reference it.
133
153
 
134
- ### Host eager readiness
154
+ ### Host objects readiness
135
155
 
136
- - `host.install(facet, { awaitEager? })` installs declarations synchronously and returns a promise for this facet's
137
- eager activations. `host.load(facets, { awaitEager? })` installs the entire batch before awaiting its eager
138
- activations. `awaitEager` defaults to true. Unheld lazy objects are not activated.
156
+ - `host.install(facet, { awaitHostObjects? })` installs declarations synchronously and returns a promise for this
157
+ facet's host object activations. `host.load(facets, { awaitHostObjects? })` installs the entire batch before awaiting
158
+ its host object activations. `awaitHostObjects` defaults to true. Unreferenced objects are not activated.
139
159
  - Factory, resource-open, dependency and activation-finalization failures reject the returned promise with the
140
- activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Eager failures also
160
+ activation error (ChordError codes preserved; ordinary errors wrapped as `unavailable`). Host object failures also
141
161
  reach `onError`. Installation is not a rollback transaction; setup/registration errors from `install` throw
142
162
  synchronously. Abandoned startup should close the host.
143
- - `{ awaitEager: false }` starts the same eager objects but resolves after installation, without waiting for their
144
- activation. `host.ready()` awaits a snapshot of the host's eager holds at call time (including direct eager
145
- `serve` and deferred loads), rejects their activation failures, and does not acquire additional holds. It is not
146
- an ongoing health check and does not await task completion or remote state hydration.
163
+ - `{ awaitHostObjects: false }` starts the same host objects but resolves after installation, without waiting for
164
+ their activation. `host.ready()` awaits a snapshot of the host's references to its host objects at call time
165
+ (including direct `serve` and deferred loads), rejects their activation failures, and does not acquire additional
166
+ references. It is not an ongoing health check and does not await task completion or remote state hydration.
147
167
  - Local bootstrap readiness and upstream admission are separate. A worker connects its upstream link, loads with
148
- `{ awaitEager: false }`, awaits its local bootstrap handle (e.g. Harness, with no upstream dependency), then
168
+ `{ awaitHostObjects: false }`, awaits its local bootstrap reference (e.g. Harness, with no upstream dependency), then
149
169
  announces control readiness so its parent can accept the link. Only after admission does it await `host.ready()`.
150
- Otherwise an eager reporter depending on server SessionStatus deadlocks with the parent waiting for control
170
+ Otherwise a host object reporting to the server's SessionStatus deadlocks with the parent waiting for control
151
171
  readiness. Deferred loads skip the whole barrier, not selected dependencies; callers explicitly choose local
152
- bootstrap handles, keep bootstrap busy until full readiness, and propagate later startup errors.
172
+ bootstrap references, keep bootstrap busy until full readiness, and propagate later startup errors.
153
173
 
154
- ## 4. Principals, variants, handles
174
+ ## 4. Principals, variants, references
155
175
 
156
- - `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Records are per principal key. `host` is a built-in principal for in-host calls.
157
- - Remote factories return `{ kind, ...members }`; `kind` must be a declared variant; members must match it. Local factories return any object.
158
- - Handle: `status` (`loading | live | released | gone`), `ready` promise, `kind`, `shape` (`{ kind, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. A remote handle's state member holds a value only while subscribed (`undefined` after its last subscriber leaves). Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released handles reject `released`; forced disposal makes handles `gone`.
176
+ - Principals are the app's type `P` (`createHost<P>`, `Host<P>`, `defineFacet<P>`, `Factory<C, P>`, `Ctx<P>`; default `unknown`). Chord invents none: `createHost({ self })` is the principal of the host's own code (roots, code inside objects, `lifetime: "host"` objects), `connect(link, { principal })` what the parent's own requests act as, `accept(link, { principal })` what a link acts as, `Route.principal(hostName)` what a routed host acts as when it calls back. All are required.
177
+ - `createHost({ principalKey })` maps principal to a key that includes connection provenance and grant revision. Objects are per principal key. The host's own principal is recognized by key (`principalKey(p) === principalKey(self)`), not by object identity, so an equal principal never creates a second object.
178
+ - Remote factories return `{ variant, ...members }`; `variant` must be a declared variant; members must match it. Local factories return any object. `variant` is reserved as a member name.
179
+ - Reference: `status` (`loading | live | released | gone`), `ready` promise, `variant`, `shape` (`{ variant, members, contract }`), `onStatus(listener)` (every status change, with the error for `gone`), late-bound members, `release()`. `self.get` is synchronous (the object goes live after its references are ready); `ctx.get` and `host.get` resolve once the reference is ready: live, and every state member of its variant hydrated (`state: false`: live only). They reject with the activation error, `aborted` (ctx aborted or settled, `signal`), or `unavailable/ready_timeout`, releasing only that reference. `host.hold` is the synchronous form for UI runtimes. A held remote reference stays subscribed to its variant's state until released; after a link loss its last values stay readable while it re-holds. State members are typed `T`; reading before the first hydration throws `not_ready`, outside the variant `not_in_variant`, after the end the end's error. A remote reference type is a union discriminated by `variant`. Shape changes (reload, variant) are pushed; members missing from the current variant reject `not_in_variant`; released references reject `released`; forced disposal makes references `gone`.
180
+ - Shared bindings: remote references on a host with the same key (type, normalized input identity, principal key, consumer contract fingerprint) are views onto one binding: one hold, one stream per state member, calls over that hold. Views have their own owner (object, call, external), status, observers and `release`; the binding ends (gone) or re-holds for all of them. Without views a binding closes, or lingers for `linger.types[typeName]` ms (`createHost({ linger: { types, cap } })`, at most `cap`, default 4, least recently released first) while staying held and hydrated.
159
181
  - Methods are `(args, ctx) => Promise<result>`. Factories still check arguments; variants are not complete authorization.
160
- - Revocation: `host.invalidatePrincipal(match)` force-disposes matching records on this host and propagates over links; links re-stamp or close.
182
+ - Revocation: `host.invalidatePrincipal(match)` force-disposes matching objects on this host and propagates over links; links re-stamp or close.
161
183
 
162
184
  ## 5. Lifetime
163
185
 
164
- - Hold owners: record (declared `get`), call (`ctx.get`, released at settle), connection (remote holds; relay maps per origin connection), host (eager), UI.
186
+ - Reference owners: object (declared `get`), call (`ctx.get`, released at settle), connection (remote references; relay maps per origin connection), host (`lifetime: "host"`), UI.
165
187
  - Activation coalesced per address; if all requesters leave before it completes, the result is disposed. Factory throw: unwind declarations, reject gets, next get retries.
166
- - Disposal after last hold + `linger`: tasks aborted, listeners removed, resources closed (reverse order; all run; errors aggregated), holds released (cascade).
167
- - Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, holders before what they hold, so a resource's `close` can still use its record's holds; `host.close` closes links after the records.
168
- - Forced disposal: handles `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.
169
- - Hold graph acyclic in-host (checked at get); across hosts by convention.
188
+ - 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).
189
+ - Bulk forced disposal (`host.close`, `invalidatePrincipal`) goes in waves, referrers before what they reference, so a resource's `close` can still use its object's references; `host.close` closes links after the objects.
190
+ - Forced disposal: references `gone` immediately, new calls rejected, in-flight call contexts aborted, then cooperative cleanup.
191
+ - Reference graph acyclic in-host (checked at get); across hosts by convention.
170
192
  - Links have ping/timeout; a dead link releases everything held through it.
171
193
 
172
- ## 6. Roots and epochs
194
+ ## 6. Roots and code versions
173
195
 
174
- - `Ctx = { signal, rootId, epoch, get(...) , principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).
175
- - A link creates a root for a request that carries none; nested remote calls carry `{rootId, epoch}`.
176
- - Dispatch: a call with epoch E runs the record's behavior for E. A host retains at most `current` and `previous` epochs (two-version rule). A call for an epoch the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another epoch.
177
- - Browsers never carry epochs into the server: browser calls start roots at the server. Browsers learn of changes via shape/reset pushes and a server `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.
196
+ - `Ctx = { signal, rootId, codeVersion, get(...): Promise<Ref>, principal? }`. Explicit; no AsyncLocalStorage dependency (may be used in dev to warn).
197
+ - A link creates a root for a request that carries none; nested remote calls carry `{rootId, codeVersion}`.
198
+ - Dispatch: a call with code version E runs the object's implementation for E. A host retains at most `current` and `previous` code versions (two-version rule). A call for a code version the host doesn't have (older than previous, or host started later) is rejected `superseded` (retryable by starting a new root). Never substitute another code version.
199
+ - 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 `Manifest` object; incompatible contract fingerprint -> `upgrade_required`.
178
200
 
179
201
  ## 7. Reload
180
202
 
@@ -182,39 +204,42 @@ Explicit trigger (`reload_plugins` tool, button, API). Serialized; no nesting.
182
204
 
183
205
  1. Build/type-check gate (application tooling, outside Chord): dependency-closed set; refuse breaking remote contract changes without a contract version bump.
184
206
  2. Load: on every affected host, import new facet modules (cache-busted).
185
- 3. Prepare: for each live record of each affected type, run the new factory in recording mode: existing cells readable, writes throw; listeners/tasks/registrations recorded, not started; holds recorded; non-exclusive resources whose key is new or `rev`/`deps` changed are opened as candidate-owned (closed on abort). Validate keys, variants, registry collisions. New records activated during prepare use the current epoch (old code). Any failure: abort, discard candidates. Vote.
186
- 4. Gate (only records whose state version was bumped or which replace an exclusive resource, plus records depending on those resources):
187
- - Admission cut: from now, an old-epoch root that has not touched the record gets `upgrade_conflict` (retryable), not queued. Roots that already hold an activation lease on it are grandfathered; their re-entry is admitted.
207
+ 3. 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 name is new or whose `version`/`deps` changed 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.
208
+ 4. Gate (only objects with a cell whose version was raised or which replace an exclusive resource, plus objects depending on those resources):
209
+ - 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.
188
210
  - Wait for grandfathered activations to finish, deadline (default 5 s). Missed deadline: abort the reload, reopen, nothing changed (report blocking roots).
189
211
  - Exclusive resources: close old, open new; on failure reopen old and abort.
190
212
  - Run `migrate` into private copies; validate.
191
- 5. 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-epoch root before committing): each host in one synchronous step installs migrated cell values and state versions, installs behaviors as epoch E', swaps registry tables, restarts tasks with new code, rebinds listeners, pushes shape/reset to subscribers, opens gates. E' current, E previous.
213
+ 5. 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 cell 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.
192
214
  The coordinator's result carries `children`: each participating child's prepared report (its vote).
193
- 6. Retire: when no roots of `previous` remain (or on the next reload, which first waits for retirement with a deadline), drop its behaviors and release declarations only it declared.
215
+ 6. Retire: when no roots of `previous` remain (and, on a child, the parent retired it), drop its implementations and release declarations only it declared. The next reload first waits for that, at most `retireDeadline`; then it aborts the roots still running on `previous` (their ctx signals), gives them up to 1 s to stop, and retires anyway. A coordinator retires before it sends `prepare`, so its `retire` frames reach the children first.
194
216
 
195
217
  Compatible changes (no state bump, no exclusive resource change) skip the gate: no waiting.
196
218
 
197
- 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.
219
+ State compatibility rule: reload prepare compares each cell's old and new schema and version. Unchanged: compatible (no gate). Version raised: gate + `migrate` (or the kept value), validated against the new schema. Schema changed without a raised version: reload refused. A lowered version: 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.
198
220
 
199
221
  ## 8. Registries
200
222
 
201
223
  ```ts
202
224
  export const Routes = defineRegistry<RouteHandler>("http.routes", { conflict: "error" });
203
- self.register(Routes, { "GET /download": handler }); // in an eager record; keys are the registry's keys
204
- host.registry(Routes).get("GET /download", ctx); // table of ctx.epoch (or current without ctx)
205
- host.registry(Routes).subscribe((table, epoch) => ...); // bridges for imperative consumers (Durable)
225
+ self.register(Routes, { "GET /download": handler }); // in a `lifetime: "host"` object; keys are the registry's keys
226
+ host.registry(Routes).get("GET /download", ctx); // table of ctx.codeVersion (or current without ctx)
227
+ host.registry(Routes).subscribe((table, codeVersion) => ...); // bridges for imperative consumers (Durable)
206
228
  ```
207
229
 
208
- Per-epoch immutable tables; order = first registration order, kept on replace. An application can implement Durable's `RegistryReader` from the table (snapshot per epoch) so phases pin a complete snapshot; Chord does not ship that bridge.
230
+ 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.
209
231
 
210
232
  ## 9. Hosts and links
211
233
 
212
- - `createHost({ name, principalKey, upstream? })`; `host.accept(link)` for children; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link> })`.
234
+ - `createHost({ name, self, principalKey, upstream?: { link, principal } })`; `host.accept(link, { principal })` for children; `host.connect(link, { principal })` for the parent; `host.route(prefix, { authorize(principal, hostName), resolve(hostName) => Promise<Link>, principal(hostName) })`.
213
235
  - Links: `memoryLinkPair()` (tests), Node IPC (child_process), WebSocket (server `ws`, browser `WebSocket`).
214
- - 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.
215
- - A child that connects behind its parent's epoch is fenced (nothing routed to it) and reconciled by a reload of its modules to the parent's epoch. Calls its roots of the old epoch make upstream are admitted in the parent's current epoch (the parent never ran that epoch), not rejected `superseded`.
216
- - Relay: server forwards frames for `at` hosts to the resolved child link, mapping holds per origin connection; authorizes before resolving/spawning and re-checks after.
217
- - Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a hold returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub` and `state` (`snap`/`ops`/`reset` with a per-subscription delta encoder), `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, reload control (`prepare`/`vote`/`commit`/`abort`).
236
+ - Principal stamping: the accepting side stamps the principal of what connects (from its authenticator); forwarded principals accepted only from the parent link; the parent's own requests (no forwarded principal) act as the principal given to `connect`; payload principal fields ignored.
237
+ - 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`.
238
+ - Relay: server forwards a hold to the child link of its target host (`at`, resolved by the consumer from the contract's placement and the normalized input, and checked against both), mapping references per origin connection; authorizes before resolving/spawning and re-checks after.
239
+ - Wire frames (JSON): `hello`/`bye`, `hold`/`release` (a reference over a link: `hold` returns `shape` or `gone`; there is no separate `get`), `shape`, `call`/`result`/`cancel`, `sub`/`unsub`/`resync` and `state`, `gone`, `root_end`, `retire`, `invalidate`, `ping`/`pong`, `ack`, reload control (`prepare`/`vote`/`commit`/`abort`).
240
+ - State: a `hold` implies the state of its variant (`state: false`: none until `sub`; `unsub` stops it). The provider answers with units: a `shape` whose `state` lists the members the consumer lacks, followed by one `state` frame `as: "snap"` per listed member; the consumer applies the shape once those snapshots are here. Later `state` frames of a member (addressed by hold id and member `m`) are `ops` (per-stream delta encoder), `reset`, or `error`; a `snap`/`reset` without `value` carries `undefined`. A failed snapshot before the first hydration ends the reference with its error. A reload or variant change sends a unit with the members that are new; members that left stop. `again` marks a re-hold after a link loss (the consumer has values). `origin` names the client connection a hold comes from (stamped by the host that accepted the client, forwarded by relays, honored only from the parent link).
241
+ - Protocol version: `hello` carries `protocol` (`PROTOCOL_VERSION`, now 4; absent means 1). A side that sees another version processes nothing more, calls `LinkOptions.onProtocolMismatch({ local, peer })` and closes the link with reason `protocol-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: the `shape` frame names the variant `variant`, code versions travel as `codeVersion` (2); remote contract tokens carry their placement (`host`), and a `hold` carries the input normalized by the consumer (3); a hold implies its state, units, `origin`, and `shape` without a code version (4).
242
+ - Flow control: both ends announce `acks` in their hello and acknowledge what they processed (`ack`, a cumulative count of every frame but `ack`). While the frames not yet acknowledged exceed the window (`window`, default 256 KiB of JSON text), state waits at the sender in slots flushed in creation order: one per member stream (ops are appended, a snapshot or reset replaces it, a release or `unsub` drops it) and one per hold for a unit (lazy: built from the current values when sent; a newer unit merges into it). Waiting ops are capped per link (`pendingCap`, default four windows): beyond, the oldest streams become a reset of their current value built at flush (a relay sends `resync` downstream and drops that member's ops until the reset arrives). The first unit of a hold that is not `again` and every frame but state are sent at once. Links that report no frame sizes (IPC) are never windowed; a peer without `acks` is neither windowed nor acknowledged. An acceptor can limit the calls a peer has in progress (`maxCalls`); calls beyond it are answered at once with `unavailable/too_many_calls`. Nothing outside the connection may add frames to a link: acknowledgements count them.
218
243
 
219
244
  ## 10. Host manager (library)
220
245
 
@@ -222,40 +247,42 @@ Per-epoch immutable tables; order = first registration order, kept on replace. A
222
247
 
223
248
  ## 11. Test coverage
224
249
 
225
- The tests cover at least: lifetime/holds/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 epoch; 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`.
250
+ 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`.
226
251
 
227
252
  ## 12. Known limitations and decisions
228
253
 
229
254
  ### Still open
230
255
 
231
- - **Retirement across hosts.** A child retires an epoch only after its parent retired it (`retire` frame) and its own roots of that epoch 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 epoch both had still gets `superseded` (retryable).
232
- - **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 epoch until it reconnects and is reconciled.
256
+ - **Retirement across hosts.** A child retires a code version only after its parent retired it (`retire` frame) and its own roots of that code version ended; the server decides from its own roots. A child that comes in step (hello or reconciliation) is told which code versions the parent retired before. A root started on a worker that calls up to a server which has already retired the code version both had still gets `superseded` (retryable).
257
+ - **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.
233
258
  - **One level of reload participants.** Only the coordinator's direct child hosts take part; a child does not forward `prepare` to its own children.
234
- - **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 epoch 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.
235
- - **Links do not resume.** The provider releases everything held through a closed link at once, so re-held records are new ones. Keeping holds for a reconnect window would need session ids on links. Calls in flight when a link drops reject `unavailable`; whether they ran is unknown.
259
+ - **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.
260
+ - **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.
236
261
  - **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.
237
262
  - **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.
238
263
  - **No automatic restart.** A terminal host failure is picked up by the host manager's `keepAlive` or `fatal` path, not inside Chord.
239
264
  - **The Node loader leaks.** Every reload imports fresh module copies that are never evicted, and it does not isolate plugins (`TODO(loader)`).
240
265
  - **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.
241
- - **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose behavior depends on captured values that change between runs is not detected.
242
- - **Shutdown and dead links.** `host.close()` disposes records in waves (holders before what they hold) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.
266
+ - **Listener `select` identity.** A listener's subscription is kept across factory runs when its origin and `select` are unchanged; `select` counts as unchanged when identical or with the same source text, so a `select` whose implementation depends on captured values that change between runs is not detected.
267
+ - **Shutdown and dead links.** `host.close()` disposes objects in waves (referrers before what they reference) and closes links last. A `close` that awaits a remote call over a link that is already down waits for a reconnect; bound such calls.
243
268
 
244
269
  ### Decisions
245
270
 
246
- - **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_eager`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.
271
+ - **Errors.** The guide's codes; a `reason` field carries the detail, for example `invalid/args`, `invalid/cycle`, `invalid/duplicate`, `invalid/read_only`, `invalid/not_host_lifetime`, `invalid/registry_conflict`, or `unavailable/unknown_type`. A `ChordError` thrown by a factory or a dependency keeps its code through every dependent; other errors become `unavailable`, with the original as `cause`.
247
272
  - **Reload errors.** There is no `reload_aborted` code. A failed reload throws the cause's code and reason with the message `reload aborted: …` (the cause is attached). Reasons of its own are `aborted/gate_deadline` (with the blocking roots in `details`), `aborted/deadline`, `aborted/vote_deadline` and `unavailable/reload_running`. Across hosts, the message names the host that failed. A child cannot reload on its own (`unavailable/not_coordinator`).
248
- - **New records during prepare.** A reload pre-allocates the next epoch ("pending"). Types the same reload adds are served from it; their records install behavior only for the pending epoch and are force-disposed if the reload aborts.
249
- - **Gate admission.** From the gate until commit or abort, creating a new record of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated records get `minEpoch = E'`: calls and writes carrying an older epoch are rejected `upgrade_conflict` permanently, and a gated record drops its previous-epoch behavior at commit. Only records whose state version or exclusive resources change are gated.
250
- - **Tasks and listeners during the gate.** Tasks of gated records are aborted at the admission cut and restart with new code at commit, or with the old code on abort. Listener events for gated records are queued and replayed through the new listener at commit, or the old one at abort.
251
- - **Grandfathered roots** are the roots that called into the record and are still running at the cut. Every host remembers on which links a root's calls went out and sends `root_end` on them when the root ends, so leases outlive calls across hosts.
252
- - **Removed types and missed retirement.** Records of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced, so those roots get `superseded` on their next call.
253
- - **Two-version rule.** A record of a changed type created after a commit has only new code; a previous-epoch root calling it gets `superseded`.
254
- - **Validation.** Remote calls are validated at handle 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 `accepts` adapter 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`.
255
- - **Contracts.** Local contracts list their members, so a loading handle 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 reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.
256
- - **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. 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 epochs. Relaying needs a matching `route`; `authorize(principal, hostName)` runs before `resolve` and 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.
257
- - **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers 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, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Handle state members are stable readables, `undefined` while 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.
258
- - **Holds and calls.** A hold's identity is its address; a second `self.get` of the same address in one run returns the same handle. A handle call without ctx, or with a task ctx, starts a fresh root in the current epoch; a ctx whose call or root already settled is refused (`aborted/expired`), one from another host `invalid/foreign_ctx`. Calling a handle inside a factory throws `not_ready`.
259
- - **Resources and disposal.** New and replaced exclusive resources open at the gate, and so do non-exclusive resources of the same record that depend on them. A new record 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.
273
+ - **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.
274
+ - **Gate admission.** From the gate until commit or abort, creating a new object of a changed type fails with `upgrade_conflict` (retryable, reason `gate`); nothing is paused, so nothing can deadlock. Gated objects get `minCodeVersion = E'`: calls and writes carrying an older code version are rejected `upgrade_conflict` permanently, and a gated object drops its previous-code-version implementation at commit. Only objects whose cell versions or exclusive resources change are gated.
275
+ - **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.
276
+ - **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_end` on them when the root ends, so leases outlive calls across hosts.
277
+ - **Removed types and missed retirement.** Objects of removed types are force-disposed at commit. When a retirement deadline is missed, retirement is forced: those roots are aborted first (their ctx signal, reason `superseded`; a relayed call's root cancels its downstream call), then what only that code version declared is closed, and their next call is `superseded`.
278
+ - **Joining during a reload.** Participants are the child hosts in step when a reload starts. A child whose hello arrives while it runs is decided when it ends: in step, or reconciled to the committed code version; a child already behind is fenced at once, so the calls its roots make meanwhile are admitted in the parent's code version. A reconciliation that is running when a reload starts syncs its child only after that reload ended, against the code version it committed. A reconciliation announces the parent's retired code versions before its `prepare`.
279
+ - **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`.
280
+ - **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 `accepts` adapter 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`.
281
+ - **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 reordering `anyOf` branches counts as a change. Fingerprints use a non-cryptographic 64-bit hash: synchronous and browser-safe; contracts are trusted.
282
+ - **Principals.** A root ctx at ingress acts as its principal (`host.root(fn, { principal })`); contexts inside objects act as `host`. 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 matching `route`; `authorize(principal, hostName)` runs before `resolve` and 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.
283
+ - **Cells and readables.** A value returned from `change` replaces the value and is always validated; `replace` transfers 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, else `r`. `subscribe(fn)` delivers values, starting with the current one; `events(fn)` delivers `snap`, then `ops`, or `reset`. Reference state members are stable readables that throw `not_ready` while loading and `not_in_variant` outside the variant, and reset when their source moves; a listener added before they are readable gets its snapshot then. Remote state members stay hydrated while the reference is held.
284
+ - **References and calls.** A reference's identity is its address; a second `self.get` of 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 host `invalid/foreign_ctx`. Calling a reference inside a factory throws `not_ready`.
285
+ - **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.
260
286
  - **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 `pingInterval` and closes after `pingTimeout` without 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 by `childControl().lock()` before storage opens. The manager kills the child's process group on every exit path and derives `booting` and `idleFor` from the child's liveness reports. WebSocket support is structural, so Chord does not depend on `ws`.
261
- - **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.
287
+ - **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 and capped per link, so a stalled peer costs bounded memory, and the next frame it gets is current. The first unit of a new reference is not held back, so a `get` never waits behind other streams.
288
+ - **The state API stays.** Objects keep named cells and unnamed derivations; giving each object exactly one state value was rejected. Do not reopen this without a new, concrete problem.