@yanlinglabs/winter-runtime-sdk 0.0.1

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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/dist/directory/directory.d.ts +55 -0
  4. package/dist/directory/entries.d.ts +82 -0
  5. package/dist/directory/recovery.d.ts +49 -0
  6. package/dist/door.d.ts +247 -0
  7. package/dist/errors.d.ts +104 -0
  8. package/dist/index.d.ts +25 -0
  9. package/dist/index.js +6679 -0
  10. package/dist/messaging/attribution.d.ts +47 -0
  11. package/dist/messaging/dispatch.d.ts +78 -0
  12. package/dist/messaging/handlers.d.ts +56 -0
  13. package/dist/messaging/inbound.d.ts +110 -0
  14. package/dist/messaging/index.d.ts +39 -0
  15. package/dist/messaging/official-adapter.d.ts +36 -0
  16. package/dist/messaging/router.d.ts +101 -0
  17. package/dist/messaging/sessions.d.ts +49 -0
  18. package/dist/messaging/winter-adapter.d.ts +47 -0
  19. package/dist/native-args.d.ts +76 -0
  20. package/dist/official/adapter.d.ts +88 -0
  21. package/dist/official/aliases.d.ts +48 -0
  22. package/dist/official/auth.d.ts +117 -0
  23. package/dist/official/branding.d.ts +31 -0
  24. package/dist/official/callbacks.d.ts +143 -0
  25. package/dist/official/containment.d.ts +130 -0
  26. package/dist/official/env-allowlist.d.ts +237 -0
  27. package/dist/official/env-registry-rule.d.ts +12 -0
  28. package/dist/official/env-registry.d.ts +3 -0
  29. package/dist/official/errors.d.ts +250 -0
  30. package/dist/official/index.d.ts +31 -0
  31. package/dist/official/mcp-descriptors.d.ts +130 -0
  32. package/dist/official/options-template.d.ts +110 -0
  33. package/dist/official/spawn-proxy.d.ts +144 -0
  34. package/dist/official/spool.d.ts +80 -0
  35. package/dist/official/supervision.d.ts +49 -0
  36. package/dist/official/sweep.d.ts +65 -0
  37. package/dist/sdk.d.ts +214 -0
  38. package/dist/seams/context.d.ts +26 -0
  39. package/dist/seams/directory-store.d.ts +274 -0
  40. package/dist/seams/directory.d.ts +46 -0
  41. package/dist/seams/global-messaging.d.ts +30 -0
  42. package/dist/seams/handoff.d.ts +77 -0
  43. package/dist/seams/index.d.ts +11 -0
  44. package/dist/seams/keychain.d.ts +11 -0
  45. package/dist/seams/materialized-resume.d.ts +46 -0
  46. package/dist/seams/messaging-contract.d.ts +29 -0
  47. package/dist/seams/official-adapter.d.ts +125 -0
  48. package/dist/seams/official-sdk-shapes.d.ts +126 -0
  49. package/dist/seams/stubs.d.ts +34 -0
  50. package/dist/selection/child-runtime.d.ts +81 -0
  51. package/dist/selection/runtime-selection.d.ts +217 -0
  52. package/dist/selection/select-runtime.d.ts +213 -0
  53. package/dist/store/handoff-barrier.d.ts +238 -0
  54. package/dist/store/index.d.ts +11 -0
  55. package/dist/store/materialized-resume.d.ts +100 -0
  56. package/dist/store/pinned-probes.d.ts +17 -0
  57. package/dist/store/reconcile.d.ts +157 -0
  58. package/dist/store/temp-continuity.d.ts +92 -0
  59. package/dist/store/wiring.d.ts +250 -0
  60. package/dist/vendor-paths.d.ts +21 -0
  61. package/dist/version-matrix.d.ts +84 -0
  62. package/docs/conformance-rows.md +195 -0
  63. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yanlingLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # winter-runtime-sdk
2
+
3
+ One door over two agent runtimes. A host that wants both the Winter Agent SDK and the official
4
+ Claude Agent SDK talks to this package as a single SDK: the same `query()`, the same `Options`, the
5
+ same closed `SDKMessage` union — plus runtime-selection inputs.
6
+
7
+ Decision record: WS-00 D19 (2026-09-05). Boundaries that do not move:
8
+
9
+ - `@yanlinglabs/winter-agent-sdk` stands alone for Winter-only hosts and never learns this package
10
+ or the official runtime exists.
11
+ - This package is a selector and an adapter, never a translation layer: Options and the message
12
+ stream pass through verbatim. It owns runtime selection (the D13 rule), the official-SDK adapter
13
+ (Options template, spool env, supervised spawn proxy, mirror errors, tool aliases and deny floor,
14
+ builtin-path containment, Winter MCP plugin registration), shared session-store wiring, the
15
+ cross-runtime handoff barrier with the materialized-resume decoration doors, and the runtime
16
+ directory plus cross-runtime messaging router.
17
+ - The host vendors all three packages directly (`winter-runtime-sdk`, `winter-agent-sdk`,
18
+ `claude-agent-sdk`); this package declares the two SDKs as peer dependencies and receives their
19
+ module instances by injection, so a host that never creates a Claude session never loads the
20
+ official runtime and no SDK is ever instantiated twice.
21
+ - A `brand` profile flows through unchanged (Winter defaults); Claude Code's own literals stay fixed.
22
+
23
+ Status: Phase 7b, all four lanes landed and the door routed. The spine (the package scaffold, the
24
+ contract re-export, the `createRuntimeSdk` constructor with its version matrix, the seams, the test
25
+ harness and CI) and the four lanes behind those seams — the official-SDK adapter, the runtime
26
+ directory and messaging router, the store wiring and handoff barrier, and runtime selection — are on
27
+ `main`, with WS-17's eighteen router-owned rows proven and cited in `docs/conformance-rows.md`. See
28
+ `docs/architecture.md` for the ownership map, the pinned interfaces and how this package consumes the
29
+ Winter SDK before its first publish.
30
+
31
+ ---
32
+
33
+ ## The door: `query()` over both runtimes
34
+
35
+ `RuntimeSdk.query()` routes by the session's DECIDED `RuntimeSelection`, and each leg returns its own
36
+ runtime's handle untouched.
37
+
38
+ ```ts
39
+ // The Winter leg: exactly what it always was. No runtime input, so nothing is decided and nothing
40
+ // is stripped — the caller's own `options` object is forwarded by reference.
41
+ for await (const message of sdk.query({ prompt: "hello" })) { /* SdkMessage */ }
42
+
43
+ // The official leg: a `claude-agent` selection, plus what only a host can answer.
44
+ const query = sdk.query({
45
+ prompt: turns, // string, or an AsyncIterable<string>
46
+ options: {
47
+ cwd: "/work/repo",
48
+ provider: { providerId: "anthropic", authRef: { kind: "keychain", account: "anthropic:default" } },
49
+ runtime: {
50
+ selection, // the session's PERSISTED choice (D13)
51
+ official: {
52
+ sessionId: "s-42", // its directory row is `session:s-42`
53
+ base: minimalOsEnvironmentFrom(process.env),
54
+ mcpServers: officialMcpServers({ /* … */ }),
55
+ },
56
+ },
57
+ },
58
+ });
59
+ ```
60
+
61
+ **What the host passes, and why the Winter leg needs none of it.** A `winter-agent` session is served
62
+ in-process by an SDK that already reads everything it needs from `Options`. A `claude-agent` session is
63
+ a supervised CHILD PROCESS with a durable row of its own, so the door needs three things `Options` has
64
+ no field for: the **session id** its directory row is addressed by (WS-14 §6 rule 2's record is written
65
+ onto that address, and the messaging registry attaches under it), the **minimal OS environment**
66
+ (WS-14 §3's child env is a REPLACEMENT built from an allowlist — nothing inherits, so nothing is read
67
+ from `process.env` by this package, and a `base` without `HOME` is **refused**, because the runtime
68
+ resolves `os.homedir()` through the OS user database when it is missing and `CLAUDE_CONFIG_DIR` cannot
69
+ scope that), and the **vendored runtime path** (§5.1: never the user's
70
+ installed binary; give it once as `createRuntimeSdk({ vendoredOfficialRuntime })` or per query as
71
+ `options.pathToClaudeCodeExecutable`). Everything else has a default that is either derived from the
72
+ brand or read from the pinned contract you already fill in: credentials come from
73
+ `options.provider.authRef` through your own `KeychainSeam`, the spool from the resolved Winter home,
74
+ the session store from the one shared instance both branches use.
75
+
76
+ **What `SessionKey` a door-opened session has.** `sdk.handoff(session, to)` and every store-facing API
77
+ take a `SessionKey`, and both halves of it are chosen by the door rather than by you: `projectKey` is
78
+ the transcript project key the door set on the child (see "the transcript key" below) and `sessionId`
79
+ is the **backend uuid the vendor allocated**, not `runtime.official.sessionId`. The door records that
80
+ uuid on the session's directory row as `backendSessionId` the moment the runtime reports it at
81
+ `system/init`, so
82
+ `sdk.handoff({ projectKey, sessionId: (await sdk.directory.get("session:s-42"))!.backendSessionId! }, "winter-agent")`
83
+ is the route. WS-15 §6.2's cold resume of an exited official session reads the same field.
84
+
85
+ **The transcript key** (R-7b-13). The official child is given
86
+ `CLAUDE_CODE_PROJECT_DIR_NAME`, and it defaults to the Winter SDK's own
87
+ `transcriptProjectKey(options.cwd)` — read off the peer you injected, never re-derived — so both
88
+ branches write under one project directory for one working directory, and the auto-memory directory
89
+ (WS-14 §2's ONE shared directory) derives from the same key. It is also the `projectKey` half of the
90
+ `SessionKey` above. **The pinned runtime validates that variable against `^[A-Za-z0-9_-]{1,64}$` and
91
+ silently substitutes its own cwd-derived name when it does not match**, so the door refuses a key it
92
+ would reject — including its own default — rather than letting the row, the environment and the memory
93
+ directory name a transcript that is somewhere else. Two consequences for a host: a deep working
94
+ directory (a sanitized path over 64 characters) needs an explicit short `runtime.official.projectKey`,
95
+ and the vendor's own fallback key is built from the **realpath** of `cwd` (`/private/var/…` on macOS)
96
+ while `transcriptProjectKey` takes the path as given — so pass the key explicitly rather than relying
97
+ on either default when the two could differ.
98
+
99
+ **What the persisted selection means.** `runtime.selection` is "what this session's record says", so a
100
+ selection that DISAGREES with the record is a request to change runtime — and D13 answers that with
101
+ the certified handoff (`sdk.handoff(session, to)`) or a visible fork, never by serving the new runtime
102
+ on the old transcript. The door refuses with `RuntimeHandoffRequiredError`: in-process on both legs
103
+ (pass `runtime.sessionId` so it can hold you to it) and, on the official leg, against the DURABLE
104
+ directory row before a credential is read or a child spawns. On a session with no record yet, the
105
+ decided selection is PERSISTED at creation, by the door.
106
+
107
+ **What each leg returns.** The Winter peer's `Query` on one side and the official SDK's own `Query` on
108
+ the other — verbatim, both of them. They are different types (the Winter handle carries `messaging`
109
+ and `listModelFamilies`; the official one carries a dozen members this package deliberately never
110
+ names on its published surface), so `query()` is overloaded: a call with **no** `options.runtime` can
111
+ only reach the Winter leg and is typed `Query`, and a call that passes one is typed
112
+ `Query | OfficialQuery`. `isOfficialQuery(handle)` narrows it. On the official leg the launch happens
113
+ at the FIRST PULL — the same lazy spawn the vendor's own `query()` performs — because WS-14 §12's
114
+ "credentials are fetched at spawn" is asynchronous and `query()` returns a handle rather than a
115
+ promise for one; `close()` before the first pull starts nothing at all.
116
+
117
+ **A live session's input stream is how messages reach it.** R-7b-4: delivery into a live session of
118
+ either runtime is a push into that session's input stream. Pass an `AsyncIterable<string>` prompt and
119
+ the door owns that stream — your turns and the router's deliveries interleave in order, with your own
120
+ backpressure preserved — and the session is attached to `sdk.messaging` as a live receiver. Pass a
121
+ **string** prompt and the vendor runs one turn and exits: the session is still recorded in the
122
+ directory, but there is nothing to push into, so it is not attached and delivery to it answers
123
+ `unavailable` rather than pretending.
124
+
125
+ **A session's END is recorded too**, and it changes what a delivery gets. When the message stream
126
+ completes (or `close()` runs) the door detaches the handle, closes the stream and records the row
127
+ `exited` — `unavailable` on a stream that ended in a fault. So a streaming session whose input has
128
+ ended answers **`unavailable` (non-retryable)** exactly like a string-prompted one, never
129
+ `delivery_uncertain`: "the write may have landed" is not an honest answer for a session where nothing
130
+ can land. `sdk.messaging.listReachable` stops listing a session at the same moment (WS-10 §10.2: a
131
+ listing does not enumerate exited transcripts), and a launch that refuses synchronously leaves no row
132
+ at all.
133
+
134
+ **The official branch disables the runtime's remote feature configuration by default** (R-7b-11).
135
+ Every official child gets `TRAFFIC_OPT_OUT_VARIABLES` — the four names are exported, so read them
136
+ rather than trusting this sentence. Measured on the pin, same binary and same options: 25 advertised
137
+ tools with the fetch, 21 without; `DesignSync`, `Monitor`, `PushNotification` and
138
+ `advisor_20260301:advisor` appear only when a CDN answers. A tool surface that moves with no version
139
+ moving is not a pinned artifact, so this is on unless you say otherwise: `remoteConfig: "allow"` (per
140
+ query on `runtime.official`, or deployment-wide on `createRuntimeSdk({ official: { env: { … } } })`)
141
+ opts back in, and the choice is recorded on the session's directory row as
142
+ `RuntimeDirectoryEntry.remoteConfig`.
143
+
144
+ **The materialized-resume PREFERRED door is open for the pinned runtime, by measurement** (R-7b-12).
145
+ WS-17 §8's four probes pass against 0.3.250 on darwin-arm64 and linux-x64, so a handle over that peer
146
+ decorates the materialized copy and leaves the canonical file byte-pure; any other version — or no
147
+ official peer — gets the always-available FALLBACK door (one labelled entry appended after the
148
+ destination confirms). The verdict is data (`materializedResumeReportForPin`), keyed by version and
149
+ re-derived in CI against the real artifact, so a pin bump is a reviewed event rather than an inherited
150
+ answer. A host that measured its own pin passes `handoff: { decorationReport }` and wins.
151
+
152
+ ---
153
+
154
+ ## What a host needs to know before wiring this
155
+
156
+ Each of these is behaviour you cannot discover from the type signatures, and each has a test behind
157
+ it. They are stated here because a host that learns them from an incident learns them expensively.
158
+
159
+ **Inbound messaging to an official session is FAIL-CLOSED until you wire a permission class.** WS-10
160
+ §13 decides delivery from the sender's and receiver's permission classes, and the official runtime
161
+ exposes no way to read its own. So `official.permissionClass` — passed as
162
+ `createRuntimeSdk({ messaging: { messaging: { official: { permissionClass } } } })` — is the ONLY way
163
+ that class is ever known, and without it **every message to every official session is HELD**, never
164
+ delivered. The hold is *default-kind*: it is released the moment the class becomes knowable, and it
165
+ expires under §13's five-minute dialog window. Two things follow that you should design for: **the
166
+ expiry sweep is LAZY** — a held message's receipt is rewritten to `refused` when something next
167
+ addresses that receiver, not on a timer of its own, so between the fifth minute and the next send the
168
+ receipt still reads `held`; and **a hook that throws is an answer, not a crash** — it falls through to
169
+ `unknown`, which holds.
170
+
171
+ **Session-name leases are global by construction.** A display name that has been used is remembered
172
+ after the object is gone, so addressing it earns "that referred to something that has gone" rather
173
+ than "no such agent" (WS-10 §11 rule 5). Rule 5 governs children within their owning conversation;
174
+ sessions are global. Nothing forgets by itself: `RuntimeDirectoryOptions.retention` is where you set
175
+ how long a released lease and a receipted delivery record survive, and **absent means forever**,
176
+ because forgetting a lease changes what a model is told about a name it can no longer reach.
177
+
178
+ **The containment floor is a scan, not a sandbox.** The pre-hoc permission floor refuses any call
179
+ whose ARGUMENTS name a forbidden target (`CLAUDE.md`, `.claude/`, `~/.claude/plans`), case-folded and
180
+ NFKC-normalized for path fields and quote-stripped for command text. **Shell-escape and constructed
181
+ name spellings are caught POST-HOC**, by a sweep registered on `PostToolUse`, `PostToolUseFailure`
182
+ and `PostToolBatch` that snapshots the forbidden names under the session's cwd and the child's HOME,
183
+ removes what APPEARED under its roots during the call, records a typed breach and ends the turn — its
184
+ diff is TIME-BASED rather than causal, so under the child's HOME a vendor home created by something
185
+ else during a long call is removed and attributed to that call (narrow: an existing one is in every
186
+ baseline and is never touched). The sweep walks both
187
+ roots to a bounded depth (6 by default) around every filesystem-touching call, so **it costs a walk
188
+ per call**: on a large tree that is the dominant cost of the floor, and an incremental/fs-events
189
+ design is the follow-up. It sees the synchronously-visible effects of the call it brackets; a
190
+ background write that lands later is caught opportunistically by the next swept call.
191
+
192
+ **A host `PreToolUse` hook that answers `allow` makes 0.3.250 skip `canUseTool` for that call.** The
193
+ floor runs first and any deny wins, so containment is unaffected — but your broker will not see that
194
+ call, which matters if you were counting on it for audit.
195
+
196
+ **The extras door is a positive allowlist with two closed escape hatches.** `configuredExtras` admits
197
+ only names the pinned artifact's own environment registry declares AND that an independent rule
198
+ classifies as non-credential; anything auth-shaped is refused with a sentence naming why, and a
199
+ deployment that has REVIEWED a specific credential-shaped variable names it in
200
+ `reviewedCredentialShapedExtras` — one name at a time, never a wildcard.
201
+
202
+ A second class is refused **by name**, and the set is exported so you can read it rather than trust a
203
+ description: `EXECUTION_INDIRECTION_ENV_NAMES` and `EXECUTION_INDIRECTION_ENV_PREFIXES`
204
+ (`src/official/`). It is the **pinned artifact's own scrub list** — the environment the runtime strips
205
+ before running its policy helper, so the definition of "changes how the child executes code" is the
206
+ vendor's rather than ours — plus that runtime's own doors: `CLAUDE_CODE_SHELL` (the Bash tool's
207
+ shell), `CLAUDE_ENV_FILE` (sourced into every Bash call), the settings paths and plugin directories
208
+ (settings carry `hooks`, `apiKeyHelper` and `env`; plugins are code), the package-manager config files,
209
+ and the binary paths it executes. Whole prefixes are refused where a closed list cannot work:
210
+ `LD_*`, `DYLD_*`, `BASH_FUNC_*`, `PYTHON*`, `PERL5*`, `RUBY*`, `LUA_*`, `DOTNET_*`, `COR*`,
211
+ `APPDOMAIN_MANAGER_*` and **all of `GIT_*`** (git reads `GIT_CONFIG_*` for a `credential.helper` and
212
+ runs `GIT_SSH_COMMAND`, `GIT_EXTERNAL_DIFF` and `GIT_ASKPASS`).
213
+
214
+ Neither of the other two rules can see this class: these names are not credential-SHAPED, and the
215
+ pinned registry legitimately declares many of them, because the runtime really does read them — which
216
+ is why "the registry declares it" cannot be the whole test. Two of them were measured on the pin doing
217
+ exactly what the class describes before they were refused (a planted `CLAUDE_CODE_SHELL` ran as the
218
+ Bash tool's shell 114 times in one session; a planted `CLAUDE_ENV_FILE` was sourced into every Bash
219
+ call — `BASH_ENV` by another door). The reviewed door for this class is `reviewedExecutionExtras`,
220
+ again one name at a time, and a drift gate fails the suite when a pin bump adds a registry name of
221
+ this shape that nothing has classified.
222
+
223
+ **The approval bridge is fail-closed when no broker is configured.** A host MUST supply a broker: with
224
+ none, every call that reaches the bridge is denied. That is deliberate — the alternative is a session
225
+ that approves its own tool calls — and it means "I did not wire a broker yet" behaves like "deny", not
226
+ like "allow".
227
+
228
+ **`interrupt()` stops the turn.** On a streaming turn it ends the generation by throwing, which is
229
+ what a projector (Phase 8) must expect: an interrupted session's stream terminates with an error
230
+ rather than a result.
231
+
232
+ **A handoff will not check that the destination can serve the session unless you give it a catalog.**
233
+ `HandoffPlan.selection` reports `unreviewed` by default, because only the host holds the model catalog
234
+ and the credential map. Passing `createRuntimeSdk({ handoff: { selectionInputFor } })` turns it on:
235
+ `plan()` then asks the selector whether the RECORDED row is still servable and whether the destination
236
+ branch can serve it, and a plan that cannot be served carries a typed refusal instead — `execute()`
237
+ offers the lossy fork before the lease is taken, rather than after the drain and the staged copy.
238
+
239
+ **A handoff can deliberately leak one staging directory.** If the destination confirms init and the
240
+ producer record then fails to write, or if the destination throws while starting against the copy it
241
+ was handed, the `claude-resume-<uuid>` staging root SURVIVES — the destination may be reading it, and
242
+ deleting a live child's `CLAUDE_CONFIG_DIR` is worse than leaving a directory behind. It is locatable
243
+ at `outcome.target.stagingRoot` and belongs to your retention pass.
244
+
245
+ **"Exactly one runtime owns a session" is a convention here, not a mechanism.** The barrier moves
246
+ ownership only after the destination confirms, and the transcript's producer record is authoritative —
247
+ but the store's writer lease is re-entrant per pid and this router hosts both branches in one process,
248
+ so a determined host can still write from the source after a handoff. A real guard needs a
249
+ writer-lease generation the SDK store does not have; it is on the SDK's list.
@@ -0,0 +1,55 @@
1
+ import type { ChildLike } from "@yanlinglabs/winter-agent-sdk/messaging";
2
+ import type { SeamContext } from "../seams/context.js";
3
+ import type { DirectoryResolution, DirectoryResolutionContext, RuntimeDirectory } from "../seams/directory.js";
4
+ import type { RuntimeDirectoryEntry } from "../seams/directory-store.js";
5
+ import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, SerializedRuntimeAddress } from "../seams/messaging-contract.js";
6
+ import { type RuntimeDirectoryRecoveryHooks, type RuntimeDirectoryRetention } from "./recovery.js";
7
+ /** What a caller may configure. Every field has an answer that is correct when it is absent. */
8
+ export interface RuntimeDirectoryOptions extends RuntimeDirectoryRecoveryHooks {
9
+ /** Injected so a test never races a real clock. */
10
+ now?: () => number;
11
+ /**
12
+ * How a `ChildLike` built from a directory row delivers, for the two doors that interface requires.
13
+ *
14
+ * The router core never uses them (it calls `adapter.steerChild`/`resumeChild` and reads `ChildLike`
15
+ * only for `status()` and its record) — see `entryToChildLike`. Absent means those two doors answer
16
+ * a typed non-retryable `unavailable` rather than throwing, so a caller that used the interface it
17
+ * was handed gets an outcome instead of a crash.
18
+ */
19
+ deliverToChild?: (entry: RuntimeDirectoryEntry, message: GlobalAgentMessage) => Promise<DeliveryOutcome>;
20
+ /**
21
+ * WS-10 §13's caps on the two durable sinks, applied by `recover()` at step 6 (Lane B fix r1, n3).
22
+ *
23
+ * ABSENT MEANS BOTH SINKS KEEP EVERYTHING — see `RuntimeDirectoryRetention`: forgetting a released
24
+ * name lease changes what a model is told about a name it can no longer reach, so the router will
25
+ * not choose a horizon for a host that did not state one.
26
+ */
27
+ retention?: RuntimeDirectoryRetention;
28
+ }
29
+ /**
30
+ * The directory, plus the two views the messaging router needs and the seam does not name.
31
+ *
32
+ * `RuntimeDirectory` is what `RuntimeSdk.directory` exposes; a host needs nothing more. The router
33
+ * lives in the same package and needs the SAME snapshot resolution used, or its listing and its
34
+ * resolution could disagree about a row that changed between two `load()` calls.
35
+ */
36
+ export interface RuntimeDirectoryHandle extends RuntimeDirectory {
37
+ /** One consistent read of the store, with the caller-scoped views built from it. */
38
+ snapshot(caller: {
39
+ owningSessionId: string;
40
+ }): Promise<DirectorySnapshot>;
41
+ /** Resolve against an ALREADY-TAKEN snapshot — the router resolves and delivers over one read. */
42
+ resolveIn(snapshot: DirectorySnapshot, to: string, context: DirectoryResolutionContext): Promise<DirectoryResolution>;
43
+ }
44
+ export interface DirectorySnapshot {
45
+ /** Every entry the store held at the moment of the read. */
46
+ readonly entries: readonly RuntimeDirectoryEntry[];
47
+ readonly byAddress: ReadonlyMap<SerializedRuntimeAddress, RuntimeDirectoryEntry>;
48
+ /** WS-10 §11's resolution input: this caller's own children, as the shared `ChildLike` boundary. */
49
+ readonly children: readonly ChildLike[];
50
+ /** Every object this caller may ADDRESS (includes exited sessions — WS-15 §6.2 has a row for them). */
51
+ readonly resolvable: readonly ListedRuntimeObject[];
52
+ /** Every object a listing may SHOW (WS-10 §10.2 — never an exited session). */
53
+ readonly listable: readonly ListedRuntimeObject[];
54
+ }
55
+ export declare function createRuntimeDirectory(context: SeamContext, options?: RuntimeDirectoryOptions): RuntimeDirectoryHandle;
@@ -0,0 +1,82 @@
1
+ import type { ChildLike } from "@yanlinglabs/winter-agent-sdk/messaging";
2
+ import type { RuntimeDirectoryEntry } from "../seams/directory-store.js";
3
+ import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, RuntimeAddress, SerializedRuntimeAddress } from "../seams/messaging-contract.js";
4
+ /**
5
+ * The session that OWNS an address (WS-10 §10.3, §11 rule 2).
6
+ *
7
+ * For a session that is the session itself; for an agent it is the parent, never the child — "a child
8
+ * is only addressable within its owning parent", and every fence in this package is that one
9
+ * sentence applied to a concrete pair of ids.
10
+ */
11
+ export declare function owningSessionIdOf(address: RuntimeAddress): string;
12
+ /** WS-10 §11's listing row for one entry. The row IS the directory record — nothing is inferred. */
13
+ export declare function entryToListedRuntimeObject(entry: RuntimeDirectoryEntry): ListedRuntimeObject;
14
+ /** `starting`/`running` are live for resolution's purposes; everything else is terminal or worse. */
15
+ export declare function isLiveStatus(status: RuntimeDirectoryEntry["status"]): boolean;
16
+ /**
17
+ * WS-10 §10.2's LISTING eligibility — deliberately narrower than resolution's.
18
+ *
19
+ * "It lists what `SendMessage` can currently reach — children of this parent, teammates, eligible
20
+ * LIVE peer sessions — and does NOT enumerate exited transcripts on disk." A terminal CHILD is still
21
+ * listed (it is resumable through its owner, and the subpath's own child row says so with
22
+ * `capabilities.resume`); an exited SESSION is not, because listing one is the enumeration that
23
+ * sentence forbids. Resolution still reaches an exited session by canonical address — WS-15 §6.2's
24
+ * routing table has a row for delivering to one — which is exactly why these are two functions and
25
+ * not one.
26
+ */
27
+ export declare function isListableFrom(entry: RuntimeDirectoryEntry, callerOwningSessionId: string): boolean;
28
+ /**
29
+ * What RESOLUTION may see: every non-archived object this caller could legitimately address.
30
+ *
31
+ * An archived session "refuses until a deliberate user/product resume unarchives it" (WS-15 §6.2), so
32
+ * it is not resolvable at all — the refusal belongs at the delivery door only for objects a caller can
33
+ * name, and an archived row is not one of them.
34
+ */
35
+ export declare function isResolvableFrom(entry: RuntimeDirectoryEntry, callerOwningSessionId: string): boolean;
36
+ /**
37
+ * A `ChildLike` view of a child entry, so WS-10 §11's SHARED resolution runs over directory rows.
38
+ *
39
+ * `steer`/`resume` are the two doors `ChildLike` requires. The router never calls them — the core's
40
+ * own `deliverEnvelope` calls `adapter.steerChild`/`adapter.resumeChild` and uses `ChildLike` only
41
+ * for `status()` and the three `record` fields — so they are wired to the caller's own delivery
42
+ * function rather than left as throws: a `ChildLike` that threw would be a trap for any future caller
43
+ * that reasonably used the interface it was handed.
44
+ *
45
+ * `permission.effectiveMode` is the one member with no source on a directory row, and it is
46
+ * UNOBSERVABLE by construction: the only place the subpath reads it is
47
+ * `childToListedRuntimeObject`'s `mode`, and this package never renders a child through that function
48
+ * — `resolve()` maps every candidate back to `entryToListedRuntimeObject`, which reads the entry's own
49
+ * `mode`. It is stated here rather than left to be discovered, because a future caller that DID
50
+ * render a `ChildLike` directly would otherwise show every child the same made-up mode.
51
+ */
52
+ export declare function entryToChildLike(entry: RuntimeDirectoryEntry, deliver: (entry: RuntimeDirectoryEntry, msg: GlobalAgentMessage) => Promise<DeliveryOutcome>): ChildLike;
53
+ /**
54
+ * THE ONE FIELD-LEVEL MERGE RULE IN THIS PACKAGE, and it exists because two lanes write one row.
55
+ *
56
+ * Lane A's supervised spawn proxy records WS-14 §6 rule 2's observed `CLAUDE_CONFIG_DIR` and §9's
57
+ * pid-plus-start-identity onto the LAUNCHED SESSION's own directory entry, through the store's
58
+ * `upsert` — which is a full REPLACE. A host that later records a status change through
59
+ * `RuntimeDirectory.record()` builds its entry from what IT knows, which is never those two fields:
60
+ * a plain `upsert` would drop them, and WS-15 §6.4 step 2 (revalidate process identity) plus §6 rule
61
+ * 5 (clear the recorded root only after verified cleanup) would then be reading a row that lost its
62
+ * evidence — silently, with nothing failing at the time.
63
+ *
64
+ * SO THE RULE IS NARROW AND STATED: exactly the two ADAPTER-OWNED fields are carried forward when the
65
+ * incoming entry omits them. Everything else is the caller's, including a caller's `undefined` for a
66
+ * field it owns, because a merge that preserved every absent field would make a display name or a
67
+ * parent impossible to clear. Lane A's own `clear()` writes through the STORE (not through this
68
+ * door), so rule 5's clearing still works exactly as it did.
69
+ */
70
+ export declare function mergeAdapterOwnedFields(incoming: RuntimeDirectoryEntry, existing: RuntimeDirectoryEntry | undefined): RuntimeDirectoryEntry;
71
+ /** Many rows, one mapping — so no caller ever writes the `.map(entryToListedRuntimeObject)` itself. */
72
+ export declare function entryToListedRuntimeObjectList(entries: readonly RuntimeDirectoryEntry[]): ListedRuntimeObject[];
73
+ /**
74
+ * The parent's canonical address for a child entry.
75
+ *
76
+ * `parentAddress` is optional on the record, so the fallback derives it from the child's own parsed
77
+ * address — through the SUBPATH's serializer, never a template literal, so this package has exactly
78
+ * one spelling of WS-10 §11's serialization.
79
+ */
80
+ export declare function parentAddressOf(entry: RuntimeDirectoryEntry): SerializedRuntimeAddress | undefined;
81
+ /** The canonical address of a session id — the same one spelling. */
82
+ export declare function sessionAddressOf(winterSessionId: string): SerializedRuntimeAddress;
@@ -0,0 +1,49 @@
1
+ import type { RuntimeDirectoryRecovery } from "../seams/directory.js";
2
+ import type { RuntimeDirectoryEntry, RuntimeDirectoryStore } from "../seams/directory-store.js";
3
+ export interface RuntimeDirectoryRecoveryHooks {
4
+ /**
5
+ * WS-15 §6.4 step 2 / WS-14 §9: is the recorded `{pid, startedAt}` still that same process?
6
+ *
7
+ * A BARE PID IS NEVER ENOUGH — an OS recycles them — so an implementation must compare the start
8
+ * identity too, which is why the field is a pair. Absent: nothing revalidates.
9
+ */
10
+ revalidateProcessIdentity?: (entry: RuntimeDirectoryEntry) => Promise<boolean> | boolean;
11
+ /**
12
+ * WS-15 §6.4 step 3: "where policy permits" — the host's call, never the router's.
13
+ *
14
+ * THIS HOOK IS THE READER OF §6 RULE 2's DURABLE ROOT (whole-branch review, F-5). `entry.configDir`
15
+ * is the `CLAUDE_CONFIG_DIR` the child was OBSERVED to get — for a store-backed resume, a
16
+ * `claude-resume-<uuid>` staging root that "the default spawner exposes no post-cleanup lookup
17
+ * for". The field exists precisely for the case where the process that knew it is gone, which is
18
+ * this one, and nothing inside this package can act on it: reconciling or cleaning a vendor root
19
+ * after a crash is a policy decision (WS-05 §12's barrier does it for a live handoff; a restart is
20
+ * the host's). So it travels on the entry, the step's own report counts the rows that carry one,
21
+ * and a host that reattaches is expected to read it.
22
+ */
23
+ reattachSupervised?: (entry: RuntimeDirectoryEntry) => Promise<"reattached" | "skipped"> | "reattached" | "skipped";
24
+ }
25
+ /**
26
+ * How long the two durable sinks REMEMBER, in milliseconds (Lane B fix r1, n3).
27
+ *
28
+ * BOTH DEFAULT TO "FOREVER" (`undefined`), and that is deliberate rather than lazy: forgetting is a
29
+ * product decision with a visible model-facing consequence, so the router will not make it for a host
30
+ * that did not ask. `nameLeases` in particular is exactly how long a stale name keeps answering "that
31
+ * referred to something that has gone" instead of "no such agent" (WS-10 §11 rule 5).
32
+ *
33
+ * Neither number can delete evidence: a claimed-but-unreceipted delivery (WS-15 §6.4 step 5) and a
34
+ * HELD lease are excluded by the stores' own doors, at every retention.
35
+ */
36
+ export interface RuntimeDirectoryRetention {
37
+ /** Receipted delivery records older than this are dropped at recovery. Absent = kept forever. */
38
+ deliveries?: number;
39
+ /** Released name leases released longer ago than this are dropped. Absent = kept forever. */
40
+ nameLeases?: number;
41
+ }
42
+ export interface RecoverDirectoryInput {
43
+ store: RuntimeDirectoryStore;
44
+ now: () => number;
45
+ hooks: RuntimeDirectoryRecoveryHooks;
46
+ /** WS-10 §13's caps, applied at step 6. Absent = both sinks keep everything. */
47
+ retention?: RuntimeDirectoryRetention;
48
+ }
49
+ export declare function recoverDirectory(input: RecoverDirectoryInput): Promise<RuntimeDirectoryRecovery>;