@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/dist/sdk.d.ts ADDED
@@ -0,0 +1,214 @@
1
+ import type { BrandProfile, Options, Query, SessionKey } from "@yanlinglabs/winter-agent-sdk";
2
+ import { type RouterOfficialInput, type RouterOfficialPolicy, type RouterQuery } from "./door.js";
3
+ import type { SeamContextWithDirectory } from "./seams/context.js";
4
+ import type { OfficialSdkModule } from "./seams/official-sdk-shapes.js";
5
+ import type { GlobalMessagingHandle } from "./messaging/router.js";
6
+ import type { HandoffBarrier, HandoffOutcome } from "./seams/handoff.js";
7
+ import type { KeychainSeam } from "./seams/keychain.js";
8
+ import type { MaterializedResumeDecorator } from "./seams/materialized-resume.js";
9
+ import type { OfficialAdapter } from "./seams/official-adapter.js";
10
+ import type { RuntimeDirectory } from "./seams/directory.js";
11
+ import type { RuntimeDirectoryStore } from "./seams/directory-store.js";
12
+ import type { GlobalMessagingOptions, RuntimeDirectoryOptions } from "./messaging/index.js";
13
+ import type { HandoffBarrierDeps } from "./store/index.js";
14
+ import type { RuntimeKind, RuntimeSelection, SelectionInput } from "./selection/runtime-selection.js";
15
+ import { type VersionMatrixReport } from "./version-matrix.js";
16
+ /**
17
+ * The injected peers.
18
+ *
19
+ * INSTANCES, not names. The router never imports the official SDK as a value — a host that only ever
20
+ * creates Winter sessions never loads it — and taking the Winter peer by injection too means a host
21
+ * that vendors all three packages (WS-02's own model) can be certain no SDK is instantiated twice.
22
+ */
23
+ export interface RuntimeSdkPeers {
24
+ winter: typeof import("@yanlinglabs/winter-agent-sdk");
25
+ /**
26
+ * The official runtime's module instance, when the host has one.
27
+ *
28
+ * TYPED STRUCTURALLY (`OfficialSdkModule`), not as `typeof import("@anthropic-ai/claude-agent-sdk")`
29
+ * as the plan pins — a fourth documented departure, and forced by the plan's own decision to make
30
+ * this peer OPTIONAL. A `typeof import(…)` in a published `.d.ts` makes every consumer's
31
+ * type-checker resolve the module, so a Winter-only host that (correctly) did not install it would
32
+ * see `Cannot find module` coming out of this package. See `seams/official-sdk-shapes.ts` for the
33
+ * full reasoning, the conformance test that keeps the structural shape honest against the real
34
+ * 0.3.250 declarations, and the packing gate that keeps the specifier out of the reachable
35
+ * declaration graph.
36
+ */
37
+ claude?: OfficialSdkModule;
38
+ }
39
+ export interface RuntimeSdkOptions {
40
+ peers: RuntimeSdkPeers;
41
+ /** R-7b-2's seam; default = in-memory (which is also what every hermetic test uses). */
42
+ directoryStore?: RuntimeDirectoryStore;
43
+ /** Host-provided credential reads (WS-14 §12) — never disk, never this package's own keychain. */
44
+ keychain: KeychainSeam;
45
+ /** `pathToClaudeCodeExecutable` for the official branch: the host vendors it; tests use node_modules. */
46
+ vendoredOfficialRuntime?: string;
47
+ /**
48
+ * Flows through to both branches unchanged (D19 clause a).
49
+ *
50
+ * RESOLVED ONCE, at construction, through the INJECTED peer's own `resolveBrand` — so an invalid
51
+ * profile is a typed construction refusal (`InvalidBrandError`, the Winter SDK's own class) beside
52
+ * the version matrix's, rather than a surprise at the first query. `RuntimeSdk.brand` is the
53
+ * result, and it is what the official branch and the router's own name derivations use.
54
+ *
55
+ * PRECEDENCE, in one sentence: a per-query `Options.brand` wins on the Winter leg and is never
56
+ * rewritten; this constructor profile fills in when a query supplies none; Winter's own defaults
57
+ * fill in when neither does.
58
+ */
59
+ brand?: Partial<BrandProfile>;
60
+ /**
61
+ * The handoff barrier's collaborators (whole-branch review, F-3).
62
+ *
63
+ * WITHOUT THIS FIELD THE HANDLE'S `handoff()` COULD NEVER RETURN `resumed`. `HandoffBarrierDeps`
64
+ * carries `participants` — the source owner to drain and the destination to confirm — and the
65
+ * factory was called with no options at all, so `markersFor` set step 8 "no destination runtime was
66
+ * supplied" and every `sdk.handoff()` ended in a lossy fork. The spine's promise was one wiring
67
+ * line per seam; keeping it meant the lane factories were CONSTRUCTED but never CONFIGURABLE.
68
+ *
69
+ * `shared` AND `decorator` ARE NOT OFFERED, on purpose: the barrier builds both so that one store,
70
+ * one decoration registry and one door are structural (see `createHandoffBarrier`'s own F2 note).
71
+ * A host that injected a second store would get the wash-back that check exists to refuse.
72
+ *
73
+ * `winterHome` here is what fills `SeamContext.winterHome`, so the barrier, its decorator and any
74
+ * later seam that reads the context all resolve under the same home.
75
+ */
76
+ handoff?: Omit<HandoffBarrierDeps, "shared" | "decorator">;
77
+ /**
78
+ * The official adapter's own policy (Task 6b).
79
+ *
80
+ * WITHOUT IT THE OFFICIAL LEG IS UNCONFIGURABLE, in the same way F-3 found the other two seams to
81
+ * be: `createOfficialAdapter(context)` was called with no options at all, so a host could not name
82
+ * its `configuredExtras`, its containment dispositions, its crash hook or its reconciler — and the
83
+ * door builds this branch's options and child environment from exactly those. `env.remoteConfig` is
84
+ * the deployment-wide default for R-7b-11; a per-query `runtime.official.remoteConfig` wins over it.
85
+ */
86
+ official?: RouterOfficialPolicy;
87
+ /**
88
+ * The directory's and the router's own options (whole-branch review, F-3).
89
+ *
90
+ * WITHOUT THIS FIELD EVERY OFFICIAL RECEIVER WAS HELD FOREVER. `official.permissionClass` is the
91
+ * ONLY way an official session's permission class can be known — there is no facet to ask — and
92
+ * since D2 an unknown class fails closed, so a host that could not pass it had a `messaging` that
93
+ * held every message to every official session and never released it. The README sentence item 19
94
+ * owes ("fail-closed until `official.permissionClass` is wired") had no field to name.
95
+ */
96
+ messaging?: {
97
+ directory?: RuntimeDirectoryOptions;
98
+ messaging?: GlobalMessagingOptions;
99
+ };
100
+ }
101
+ /** Options members this package OWNS. Never forwarded to either SDK — see `query()`. */
102
+ export declare const ROUTER_ONLY_OPTION_KEYS: readonly ["runtime"];
103
+ export type RouterOnlyOptionKey = (typeof ROUTER_ONLY_OPTION_KEYS)[number];
104
+ /**
105
+ * The runtime-selection inputs the door accepts ALONGSIDE the pinned `Options` — "additive and
106
+ * typed" (the plan's Global Constraints), and stripped before either SDK sees them.
107
+ */
108
+ export interface RouterRuntimeInput {
109
+ /** A selection already persisted for this session. It WINS: a change is a handoff or a visible fork. */
110
+ selection?: RuntimeSelection;
111
+ /** Everything needed to decide one when there is no persisted selection yet. */
112
+ select?: SelectionInput;
113
+ /**
114
+ * This session's own id, on EITHER leg.
115
+ *
116
+ * WHAT IT BUYS: the door can hold a caller to the session's persisted choice. `selection` is
117
+ * documented as "already persisted for this session", so a `selection` that disagrees with the
118
+ * session's record is a REQUEST TO CHANGE RUNTIME, and D13 answers that with a certified handoff or
119
+ * a visible fork — never by serving the new runtime on the old transcript. With an id, the door
120
+ * refuses that in-process on both legs; the official leg additionally reads the durable directory
121
+ * row before it launches anything. Without one, the host is the only party holding the record.
122
+ *
123
+ * The official leg takes its id from `official.sessionId` (which it requires anyway); this field is
124
+ * for the Winter leg and for a host that prefers to say it once.
125
+ */
126
+ sessionId?: string;
127
+ /**
128
+ * What the OFFICIAL leg needs and only the host knows (Task 6b) — the session id its directory row
129
+ * is addressed by, its credential plan for families whose variables are the host's, the vendored
130
+ * runtime's neighbours. Ignored entirely on the Winter leg, where the pinned `Options` already say
131
+ * everything.
132
+ */
133
+ official?: RouterOfficialInput;
134
+ }
135
+ /** `Options` plus the router's own additive input. Nothing is removed and nothing is renamed. */
136
+ export interface RouterOptions extends Options {
137
+ runtime?: RouterRuntimeInput;
138
+ }
139
+ export interface RuntimeSdk {
140
+ /** The resolved brand profile every Winter-owned name in this session derives from (I2). */
141
+ readonly brand: BrandProfile;
142
+ /**
143
+ * The one door, over BOTH runtimes (Task 6b). See this module's header for the deviations from the
144
+ * plan's pinned line, and `src/door.ts` for the official leg's own composition.
145
+ *
146
+ * TWO OVERLOADS, AND THE SPLIT IS A FACT RATHER THAN A CONVENIENCE. The official leg is reachable
147
+ * ONLY through `options.runtime`, so a call that passes none can only ever produce the Winter peer's
148
+ * `Query` — and says so. A call that DOES pass one is decided at run time, and its type is the union
149
+ * of the two runtimes' own handles, because neither of them is the other and this package will not
150
+ * flatten them into a facade (see `RouterQuery`).
151
+ */
152
+ query(args: {
153
+ prompt: string | AsyncIterable<string>;
154
+ options?: Options & {
155
+ runtime?: never;
156
+ };
157
+ }): Query;
158
+ query(args: {
159
+ prompt: string | AsyncIterable<string>;
160
+ options: RouterOptions;
161
+ }): RouterQuery;
162
+ /** D13/D28, pure. Throws `SelectionRefusedError` on a typed refusal (see that class's own note). */
163
+ selectRuntime(input: SelectionInput): RuntimeSelection;
164
+ /** WS-15 §6.1. */
165
+ directory: RuntimeDirectory;
166
+ /**
167
+ * WS-15 §6.2–6.4 — the HANDLE, which is the seam plus the two doors a host cannot work without.
168
+ *
169
+ * WIDENED, NEVER NARROWED (F-3). The plan pins `GlobalMessaging`, and `GlobalMessagingHandle`
170
+ * extends it: every pinned member is present with its pinned signature, and what is added is
171
+ * `attachWinterSession`/`attachOfficialSession` — without which a host can configure a receiver's
172
+ * permission class and still have nothing live to deliver to. Typing the field as the seam meant
173
+ * the one door the package exists for could be reached only through a cast.
174
+ */
175
+ messaging: GlobalMessagingHandle;
176
+ /** WS-05 §12's mechanics; the host renders the outcome (R-7b-3). */
177
+ handoff(session: SessionKey, to: RuntimeKind): Promise<HandoffOutcome>;
178
+ readonly versions: VersionMatrixReport;
179
+ dispose(): Promise<void>;
180
+ }
181
+ /**
182
+ * The collaborators a lane swaps its real implementation into.
183
+ *
184
+ * Not part of `RuntimeSdk` (the plan pins that surface) and not a constructor option (the plan pins
185
+ * those too) — it is this package's own internal wiring, exported for `test/spine/*` and for the
186
+ * lanes' own tests to reach a single seam without standing up the whole handle.
187
+ */
188
+ export interface RuntimeSdkInternals {
189
+ official: OfficialAdapter;
190
+ barrier: HandoffBarrier;
191
+ decorator: MaterializedResumeDecorator;
192
+ /** The exact object every seam factory was handed — what a lane's real factory will receive. */
193
+ context: SeamContextWithDirectory;
194
+ }
195
+ /** Reaches the internals of a handle this package built. Returns undefined for anything else. */
196
+ export declare function runtimeSdkInternals(sdk: RuntimeSdk): RuntimeSdkInternals | undefined;
197
+ /**
198
+ * Builds the object forwarded to a peer's `query()`.
199
+ *
200
+ * THE COMMON CASE FORWARDS THE CALLER'S OWN OBJECT, by reference. "Passes through verbatim" is a
201
+ * property a test can only really check by identity, and a router that copied unconditionally would
202
+ * be quietly deciding which of `Options`' members it knows about — the exact drift D19b's "never a
203
+ * translation layer" rules out. A copy is made ONLY when a router-owned key is present, because that
204
+ * key must not reach an SDK that would not recognise it; every other member keeps its own value
205
+ * identity through the copy.
206
+ */
207
+ export declare function forwardableOptions(options: RouterOptions, brand?: Partial<BrandProfile>): Options;
208
+ /**
209
+ * Constructs the router. Throws `RuntimeSdkVersionError` on a version-matrix miss (D19a).
210
+ *
211
+ * THE MATRIX IS ASSERTED FIRST, before a single seam is built, so a refusal costs nothing and says
212
+ * only what it is about.
213
+ */
214
+ export declare function createRuntimeSdk(opts: RuntimeSdkOptions): RuntimeSdk;
@@ -0,0 +1,26 @@
1
+ import type { BrandProfile } from "@yanlinglabs/winter-agent-sdk";
2
+ import type { RuntimeSdkPeers } from "../sdk.js";
3
+ import type { RuntimeDirectory } from "./directory.js";
4
+ import type { RuntimeDirectoryStore } from "./directory-store.js";
5
+ import type { KeychainSeam } from "./keychain.js";
6
+ /** Everything the directory itself needs. */
7
+ export interface SeamContext {
8
+ peers: RuntimeSdkPeers;
9
+ /** WS-14 §12: the host's credential reads. Nothing in this package caches what it returns. */
10
+ keychain: KeychainSeam;
11
+ /**
12
+ * The RESOLVED profile (I2) — never `Partial`, never a literal, resolved exactly once at
13
+ * construction through the INJECTED peer's own `resolveBrand`. Every Winter-owned name any seam
14
+ * spells derives from this.
15
+ */
16
+ brand: BrandProfile;
17
+ directoryStore: RuntimeDirectoryStore;
18
+ /** WS-14 §5.1: `pathToClaudeCodeExecutable` — the host vendors it; tests point at node_modules. */
19
+ vendoredOfficialRuntime?: string;
20
+ /** Lane C: an explicit home for the shared store's lazy resolver (defaults to the peer's own resolution under `brand`). */
21
+ winterHome?: string;
22
+ }
23
+ /** What every OTHER seam needs: the same, plus the directory, which is built first. */
24
+ export interface SeamContextWithDirectory extends SeamContext {
25
+ directory: RuntimeDirectory;
26
+ }
@@ -0,0 +1,274 @@
1
+ import type { RuntimeSelection } from "../selection/runtime-selection.js";
2
+ import type { RemoteConfigPolicy } from "./official-adapter.js";
3
+ import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, RuntimeAddress, RuntimeKind, RuntimeObjectKind, SerializedRuntimeAddress } from "./messaging-contract.js";
4
+ /**
5
+ * WS-15 §6.1's `transport`, which `runtimeKind × objectKind` CANNOT recover (review r1, M1).
6
+ *
7
+ * An in-daemon Dispatch/Chat session (`winter-thread`) and a spawned Code session (`winter-session`)
8
+ * are both `winter-agent`/`session`, and that is precisely the split the Winter messaging adapter
9
+ * routes on: a direct in-process push versus the spawned session's `Query.messaging` wire facet.
10
+ */
11
+ export type RuntimeTransport = "winter-thread" | "winter-session" | "claude-handle" | "claude-child";
12
+ /**
13
+ * WS-15 §6.1's directory record: one addressable runtime object (a session or one of its children).
14
+ *
15
+ * `selection` is the object's OWN persisted runtime, never its parent's — R-7b-1's whole point, and
16
+ * what makes WS-13c §8 ("the child's own record is authoritative on resume") enforceable rather than
17
+ * aspirational. `generation` is WS-10 §12's monotonic counter, the thing a `toGeneration` on an
18
+ * envelope is checked against so a message addressed to a previous incarnation is refused rather
19
+ * than delivered to its successor.
20
+ */
21
+ export interface RuntimeDirectoryEntry {
22
+ /** The canonical serialization — the key every other method takes. */
23
+ address: SerializedRuntimeAddress;
24
+ /** The structured form of `address`. Stored alongside so a reader never re-parses. */
25
+ parsed: RuntimeAddress;
26
+ runtimeKind: RuntimeKind;
27
+ objectKind: RuntimeObjectKind;
28
+ /** WS-15 §6.1's own field — see `RuntimeTransport`; NOT derivable from the two above. */
29
+ transport: RuntimeTransport;
30
+ /** The display name a `ListAgents` listing shows; absent when the object has never been named. */
31
+ displayName?: string;
32
+ /** WS-15 §6.1's `title` — the session's own title, when it has one. */
33
+ title?: string;
34
+ status: ListedRuntimeObject["status"];
35
+ /** `code` | `dispatch` | `chat` — an open string here because the mode vocabulary is the host's (WS-15 §2). */
36
+ mode: string;
37
+ cwd?: string;
38
+ /** WS-10 §12's incarnation counter. */
39
+ generation: number;
40
+ /** This object's own persisted runtime choice (R-7b-1 / WS-13c §8). */
41
+ selection: RuntimeSelection;
42
+ /** Present for a child; the canonical address of its parent session. */
43
+ parentAddress?: SerializedRuntimeAddress;
44
+ /** The backend (runtime-side) session id, absent while "starting". */
45
+ backendSessionId?: string;
46
+ /**
47
+ * WS-14 §6 rule 2: the `CLAUDE_CONFIG_DIR` OBSERVED in `SpawnOptions.env` — never the value that was
48
+ * configured — recorded before the process is returned, because "this is the only supported way to
49
+ * know the `claude-resume-<uuid>` staging root, since the default spawner exposes no post-cleanup
50
+ * lookup". Rule 5: cleared (set back to absent) only after verified cleanup, which is why it is a
51
+ * field with a lifecycle rather than a write-once one.
52
+ *
53
+ * WRITTEN BY Lane A's supervised spawn proxy, before it hands the process back. READ BY Lane B's
54
+ * `RuntimeDirectory.recover()` (WS-15 §6.4) and by Lane A's own cleanup reconciliation. Absent for
55
+ * every `winter-agent` object — it is the official branch's staging root, nobody else's.
56
+ */
57
+ configDir?: string;
58
+ /**
59
+ * WS-14 §9 / WS-15 §6.4 step 2: PID **plus process start identity**, never a bare PID — an OS
60
+ * recycles pids, so a bare one turns "is my child still alive?" into a coin flip that occasionally
61
+ * answers about somebody else's process.
62
+ *
63
+ * WRITTEN BY Lane A's spawn proxy alongside `configDir`. READ BY Lane B's `recover()` step 2, which
64
+ * marks previously live handles `"unavailable"` until process identity revalidates. Absent for
65
+ * in-daemon (`winter-thread`) objects, which have no child at all.
66
+ */
67
+ processIdentity?: {
68
+ pid: number;
69
+ startedAt: string;
70
+ };
71
+ /**
72
+ * R-7b-11: whether this session's child was allowed to fetch the runtime's REMOTE FEATURE
73
+ * CONFIGURATION. Absent (and for every `winter-agent` row) means the shipped default, `"deny"`.
74
+ *
75
+ * IT IS ON THE ROW BECAUSE THE SURFACE IS NOT DERIVABLE FROM THE VERSION. Two sessions on the same
76
+ * pinned artifact advertise different tool sets depending on this one answer, so a reader asking
77
+ * "what could this session do?" cannot answer it from `selection.engineVersion` alone. Written by
78
+ * the official adapter's record sink at launch, from `OfficialLaunchPlan.remoteConfig`.
79
+ */
80
+ remoteConfig?: RemoteConfigPolicy;
81
+ capabilities: ListedRuntimeObject["capabilities"];
82
+ /**
83
+ * ISO-8601. A DELIBERATE DEPARTURE from WS-15 §6.1's `updatedAt: number`: every other timestamp on
84
+ * this seam that a human ever reads is ISO (`RuntimeSelection.decidedAt`, the lease records below),
85
+ * and one record carrying epoch milliseconds while its neighbours carry ISO is the kind of
86
+ * inconsistency that produces a `new Date(isoString)` bug in a host months later. The epoch-ms
87
+ * spelling is kept where it is load-bearing for arithmetic — `HeldMessageRecord.heldAt/expiresAt`,
88
+ * which the mailbox's own expiry compares numerically, matching the runtime's mailbox exactly.
89
+ */
90
+ updatedAt: string;
91
+ }
92
+ /**
93
+ * Per-address delivery cursors (WS-15 §6.4's recovery, WS-17 row 11's "index.db rebuild preserves
94
+ * runtime mappings, backend IDs, cursors").
95
+ *
96
+ * A cursor is an OPAQUE string to this seam: what it points at is the adapter's business.
97
+ */
98
+ export interface CursorStore {
99
+ get(address: SerializedRuntimeAddress): Promise<string | undefined>;
100
+ set(address: SerializedRuntimeAddress, cursor: string): Promise<void>;
101
+ remove(address: SerializedRuntimeAddress): Promise<void>;
102
+ /** Every cursor, for the restart sweep. */
103
+ all(): Promise<Record<SerializedRuntimeAddress, string>>;
104
+ }
105
+ /**
106
+ * One held message, as the store keeps it.
107
+ *
108
+ * The FULL envelope is stored, not a reference: WS-15 §6.4's restart recovery has to be able to
109
+ * deliver a message the process was holding when it died, and a record that only remembered an id
110
+ * would need the message to have been persisted somewhere else as well.
111
+ */
112
+ export interface HeldMessageRecord {
113
+ messageId: string;
114
+ /** The canonical address the message is held FOR. */
115
+ receiver: SerializedRuntimeAddress;
116
+ reason: string;
117
+ /** WS-10 §13: an explicit hold persists; a default-class hold expires (5-minute dialog expiry). */
118
+ kind: "default" | "explicit";
119
+ /** Epoch milliseconds, matching the runtime's own mailbox. */
120
+ heldAt: number;
121
+ /** Present only for `kind: "default"`. */
122
+ expiresAt?: number;
123
+ message: GlobalAgentMessage;
124
+ }
125
+ /** The held-mailbox sink. Caps and expiry are the ROUTER's (see this file's header). */
126
+ export interface MailboxStore {
127
+ listHeld(receiver: SerializedRuntimeAddress): Promise<HeldMessageRecord[]>;
128
+ hold(record: HeldMessageRecord): Promise<void>;
129
+ takeHeld(receiver: SerializedRuntimeAddress, messageId: string): Promise<HeldMessageRecord | undefined>;
130
+ clear(receiver: SerializedRuntimeAddress): Promise<void>;
131
+ /** Every receiver holding at least one message — the entry point of the restart sweep. */
132
+ receivers(): Promise<SerializedRuntimeAddress[]>;
133
+ }
134
+ /**
135
+ * ONE DELIVERY, from the moment its id exists to the moment its receipt does (WS-15 §6.2).
136
+ *
137
+ * The pipeline the router runs is explicit about what must be durable, and in which order: "allocate
138
+ * and **persist** the request/message ID **before address resolution** (so ambiguous/missing/
139
+ * unavailable outcomes are idempotent too) → **persist the envelope** and resolved target generation
140
+ * → … → **persist the adapter receipt**", so that "a retry returns the stored outcome instead of
141
+ * starting a second turn".
142
+ *
143
+ * `claimedBy` set with no `outcome` is the crash window: WS-15 §6.4 step 5 reconciles exactly those
144
+ * as `delivery_uncertain`, which is why they are two fields and not one status enum — the pair is the
145
+ * evidence, and a single "status" would let a writer describe a state it had not actually reached.
146
+ */
147
+ export interface DeliveryRecord {
148
+ /** WS-10 §12: derived from (sender session, tool-call id), so a retry allocates the SAME id. */
149
+ messageId: string;
150
+ /** The envelope, persisted before resolution — a restart must be able to finish what it started. */
151
+ message: GlobalAgentMessage;
152
+ /** The target generation resolution picked. A later generation is a different incarnation. */
153
+ toGeneration: number;
154
+ /** Set at the atomic claim, before the adapter is invoked; a claim with no receipt is "uncertain". */
155
+ claimedBy?: RuntimeKind;
156
+ /** THE RECEIPT. A retry with the same `messageId` returns this instead of delivering again. */
157
+ outcome?: DeliveryOutcome;
158
+ /** ISO-8601. */
159
+ updatedAt: string;
160
+ }
161
+ /** The idempotency sink. Policy (dedupe windows, retry limits, loop guards) stays with the router. */
162
+ export interface DeliveryRecordStore {
163
+ get(messageId: string): Promise<DeliveryRecord | undefined>;
164
+ put(record: DeliveryRecord): Promise<void>;
165
+ /** WS-15 §6.4 step 5's entry point: claimed, never receipted — every one is `delivery_uncertain`. */
166
+ claimedWithoutReceipt(): Promise<DeliveryRecord[]>;
167
+ /**
168
+ * Drop every RECEIPTED record older than this ISO stamp; returns how many went (Lane B fix r1, n3).
169
+ *
170
+ * WHY A DURABLE SINK NEEDS A REMOVAL DOOR AT ALL. This store is the only unbounded thing in the
171
+ * delivery path — the SDK subpath's own in-memory equivalents are explicitly capped
172
+ * (`rememberBounded`, `MAX_TRACKED_MESSAGE_IDS`) — and on the OFFICIAL branch the growth is worst,
173
+ * because a send with no tool-call id to derive from writes a record under `no-tool-call:<now>:
174
+ * <random>` that can never be looked up again. Without this door a long-lived host's directory
175
+ * store grows forever with rows that answer no question.
176
+ *
177
+ * A CLAIMED-BUT-UNRECEIPTED RECORD IS NEVER PRUNED, at any age. It is WS-15 §6.4 step 5's whole
178
+ * evidence: the pair (claimed, no receipt) is what makes a crashed delivery `delivery_uncertain`
179
+ * rather than invisible, and an "old enough" heuristic would silently convert the one state
180
+ * recovery must report into no state at all.
181
+ */
182
+ prune(receiptedBefore: string): Promise<number>;
183
+ }
184
+ /**
185
+ * A pending `notify_when_idle` (WS-10 §14, WS-15 §6.3).
186
+ *
187
+ * §6.3 is explicit that this one "survives restart only when durably stored with valid target
188
+ * identity/generation" — hence `targetGeneration`, without which a notice could fire for a different
189
+ * incarnation of the same address.
190
+ */
191
+ export interface IdleSubscriptionRecord {
192
+ messageId: string;
193
+ /** Who gets the notice. */
194
+ subscriber: SerializedRuntimeAddress;
195
+ /** What is being watched. */
196
+ target: SerializedRuntimeAddress;
197
+ targetGeneration: number;
198
+ /** Epoch milliseconds — compared numerically against WS-10 §14's 12-hour expiry. */
199
+ createdAt: number;
200
+ expiresAt: number;
201
+ }
202
+ export interface IdleSubscriptionStore {
203
+ list(): Promise<IdleSubscriptionRecord[]>;
204
+ add(record: IdleSubscriptionRecord): Promise<void>;
205
+ remove(messageId: string): Promise<void>;
206
+ }
207
+ /**
208
+ * A display-name lease, live or released (WS-10 §11 rule 5, WS-15 §6.4 step 6).
209
+ *
210
+ * WHY THE HISTORY OUTLIVES THE ENTRY. Rule 5 says a STALE name is refused — not "not found". Telling
211
+ * those two apart after the object is gone requires remembering that the name once meant something,
212
+ * which `RuntimeDirectoryEntry.displayName` cannot do (it disappears with `remove()`). A released
213
+ * lease is that memory, and it is the only thing that lets the router answer "that name referred to a
214
+ * session that has since exited" instead of "no such agent".
215
+ */
216
+ export interface NameLeaseRecord {
217
+ /** The display name, exactly as a model or user would write it. */
218
+ name: string;
219
+ address: SerializedRuntimeAddress;
220
+ /** The holder's generation at the time of the claim. */
221
+ generation: number;
222
+ /** ISO-8601. */
223
+ claimedAt: string;
224
+ /** ISO-8601; absent while the lease is held. */
225
+ releasedAt?: string;
226
+ }
227
+ export interface NameLeaseStore {
228
+ /** Every record for a name — a held one (at most one) plus released ones. Order is the caller's business. */
229
+ lookup(name: string): Promise<NameLeaseRecord[]>;
230
+ claim(record: NameLeaseRecord): Promise<void>;
231
+ release(name: string, address: SerializedRuntimeAddress, releasedAt: string): Promise<void>;
232
+ /** Every currently-held lease — WS-15 §6.4 step 6 sweeps these by generation. */
233
+ held(): Promise<NameLeaseRecord[]>;
234
+ /**
235
+ * Drop every RELEASED lease released before this ISO stamp; returns how many went (Lane B fix r1, n3).
236
+ *
237
+ * A HELD LEASE IS NEVER PRUNED, at any age — it is the live answer to "who owns this name".
238
+ *
239
+ * THE RETENTION IS A PRODUCT DECISION, not a cleanup interval: it is exactly how long a stale name
240
+ * keeps answering "that referred to something that has gone" instead of "no such agent" (WS-10 §11
241
+ * rule 5). Prune too eagerly and a model that addresses a session that exited an hour ago gets the
242
+ * refusal rule 5 exists to avoid; never prune and the memory of every name ever used is permanent.
243
+ */
244
+ prune(releasedBefore: string): Promise<number>;
245
+ }
246
+ /**
247
+ * R-7b-2's seam.
248
+ *
249
+ * The plan pins the first five members; `deliveries`, `subscriptions` and `names` were added in fix
250
+ * round 1 (review r1, I1) because three obligations the plan assigns Lane B — WS-15 §6.2's persisted
251
+ * id/envelope/claim/receipt, §6.3's restart-surviving idle subscriptions, and §6.4 step 6 / WS-10 §11
252
+ * rule 5's name leases — had no durable sink at all, here or in the SDK-side router core (whose
253
+ * outcome map and notification queue are bounded, explicitly non-durable, in-memory `Map`s). The
254
+ * router's store is the only durable place they can live.
255
+ */
256
+ export interface RuntimeDirectoryStore {
257
+ load(): Promise<RuntimeDirectoryEntry[]>;
258
+ upsert(e: RuntimeDirectoryEntry): Promise<void>;
259
+ remove(address: SerializedRuntimeAddress): Promise<void>;
260
+ cursors: CursorStore;
261
+ mailboxes: MailboxStore;
262
+ deliveries: DeliveryRecordStore;
263
+ subscriptions: IdleSubscriptionStore;
264
+ names: NameLeaseStore;
265
+ }
266
+ /**
267
+ * The in-memory `RuntimeDirectoryStore` — the default when a host injects none, and the store every
268
+ * hermetic test uses.
269
+ *
270
+ * FULLY IMPLEMENTED, not a stub: it is the test default, so "the directory works" must be provable
271
+ * without a host. It touches no filesystem, no `~/.winter`, no keychain, and holds nothing across a
272
+ * process — which is also the honest statement of what a host gets if it never supplies one.
273
+ */
274
+ export declare function createInMemoryRuntimeDirectoryStore(): RuntimeDirectoryStore;
@@ -0,0 +1,46 @@
1
+ import type { RuntimeDirectoryEntry } from "./directory-store.js";
2
+ import type { ListedRuntimeObject, RuntimeAddress, SerializedRuntimeAddress } from "./messaging-contract.js";
3
+ export interface DirectoryResolutionContext {
4
+ /** Who is asking. Scopes "a stable child id beats a name" to the asker's own children. */
5
+ from: RuntimeAddress;
6
+ }
7
+ export type DirectoryResolution = {
8
+ kind: "resolved";
9
+ entry: RuntimeDirectoryEntry;
10
+ } | {
11
+ kind: "ambiguous";
12
+ candidates: ListedRuntimeObject[];
13
+ } | {
14
+ kind: "stale-name";
15
+ reason: string;
16
+ candidates: ListedRuntimeObject[];
17
+ } | {
18
+ kind: "not-found";
19
+ reason: string;
20
+ };
21
+ /** WS-15 §6.4's seven-step restart recovery, reported step by step. */
22
+ export interface RuntimeDirectoryRecoveryStep {
23
+ step: 1 | 2 | 3 | 4 | 5 | 6 | 7;
24
+ name: string;
25
+ outcome: string;
26
+ }
27
+ export interface RuntimeDirectoryRecovery {
28
+ steps: RuntimeDirectoryRecoveryStep[];
29
+ entriesLoaded: number;
30
+ /** Entries whose runtime object no longer exists and were marked accordingly (never deleted silently). */
31
+ staleMarked: number;
32
+ cursorsRestored: number;
33
+ heldMessagesFound: number;
34
+ }
35
+ /** WS-15 §6.1. Lane B implements; the spine pins the signature. */
36
+ export interface RuntimeDirectory {
37
+ list(scope?: {
38
+ parent?: SerializedRuntimeAddress;
39
+ }): Promise<RuntimeDirectoryEntry[]>;
40
+ get(address: SerializedRuntimeAddress): Promise<RuntimeDirectoryEntry | undefined>;
41
+ record(entry: RuntimeDirectoryEntry): Promise<void>;
42
+ forget(address: SerializedRuntimeAddress): Promise<void>;
43
+ /** `to` is the raw model- or user-supplied target string (WS-10 §10.1 caps it at 300 chars, no newline, no `*`). */
44
+ resolve(to: string, context: DirectoryResolutionContext): Promise<DirectoryResolution>;
45
+ recover(): Promise<RuntimeDirectoryRecovery>;
46
+ }
@@ -0,0 +1,30 @@
1
+ import type { DeliveryOutcome, GlobalAgentMessage, ListedRuntimeObject, PermissionClassLabel, RuntimeAddress, RuntimeKind, RuntimeMessagingAdapter } from "./messaging-contract.js";
2
+ /** WS-10 §10.1's model-facing send, after the tool layer has validated `to` (≤300 chars, no newline, `"*"` forbidden). */
3
+ export interface SendMessageRequest {
4
+ from: RuntimeAddress;
5
+ /** The raw target string as the model wrote it. Resolution is the directory's job, never the caller's. */
6
+ to: string;
7
+ body: string;
8
+ summary?: string;
9
+ notifyWhenIdle?: boolean;
10
+ /** The tool call this send came from — WS-10 §12 derives the message id from (sender, tool call). */
11
+ originToolCallId?: string;
12
+ }
13
+ /** WS-15 §6.2. Lane B implements; the spine pins the signature. */
14
+ export interface GlobalMessaging {
15
+ /** WS-10 §10.2: what this sender can reach right now. Never enumerates exited transcripts on disk. */
16
+ listReachable(scope: {
17
+ from: RuntimeAddress;
18
+ }): Promise<ListedRuntimeObject[]>;
19
+ send(request: SendMessageRequest): Promise<DeliveryOutcome>;
20
+ deliver(message: GlobalAgentMessage): Promise<DeliveryOutcome>;
21
+ /** WS-10 §14's 12-hour idle subscription. */
22
+ notifyWhenIdle(target: RuntimeAddress, request: {
23
+ from: RuntimeAddress;
24
+ messageId: string;
25
+ }): Promise<DeliveryOutcome>;
26
+ /** WS-10 §13's inbound class for a given object, as the policy matrix reads it. */
27
+ senderPermissionClass(address: RuntimeAddress): Promise<PermissionClassLabel>;
28
+ /** One adapter per runtime kind; registering the same kind twice replaces it. */
29
+ registerAdapter(kind: RuntimeKind, adapter: RuntimeMessagingAdapter): void;
30
+ }