@sagentlab/navarch-runtime 0.1.112 → 0.1.117

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 (56) hide show
  1. package/README.md +56 -18
  2. package/bin/muse-mcp-bridge.cjs +3 -0
  3. package/dist/adapters/index.cjs +6 -1
  4. package/dist/adapters/muse.cjs +433 -0
  5. package/dist/agent-auth.cjs +2 -1
  6. package/dist/cli.cjs +4 -1
  7. package/dist/config.cjs +9 -1
  8. package/dist/conversation/capability.cjs +1 -1
  9. package/dist/conversation/loop.cjs +16 -0
  10. package/dist/conversation/mirror.cjs +225 -0
  11. package/dist/conversation/workspace.cjs +11 -4
  12. package/dist/exit-conditions.cjs +1 -1
  13. package/dist/muse-mcp-bridge.cjs +167 -0
  14. package/dist/session.cjs +30 -8
  15. package/dist/worktree-guard.cjs +137 -3
  16. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  17. package/node_modules/@muse-code/sdk/README.md +88 -0
  18. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  19. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  20. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  21. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  22. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  23. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  37. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  38. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  39. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  40. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  41. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  45. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  46. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  47. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  48. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  49. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  50. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  51. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  52. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  53. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  54. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  55. package/node_modules/@muse-code/sdk/package.json +47 -0
  56. package/package.json +8 -4
@@ -0,0 +1,171 @@
1
+ /**
2
+ * `MuseClient` — the SS7.1 facade's session-opening half (FR-018, T030).
3
+ *
4
+ * It turns the two v1 session verbs into correctly-shaped `session/start` /
5
+ * `session/resume` params and hands back a WIRED `Session`. It frames nothing
6
+ * and mints nothing: `Connection.command()` stamps the `commandId` from the
7
+ * single injected mint, so INV-013's single-minter property is preserved by
8
+ * DELEGATION rather than re-implemented here.
9
+ *
10
+ * IT OWNS TWO FACTS NO OTHER LAYER HOLDS, and that is why obligations (b) and
11
+ * (c) of T030 live here rather than on `Session`:
12
+ *
13
+ * - WHETHER A CLOSE WAS OURS. `Connection` is duplex-generic; a transport
14
+ * cannot tell a peer's hang-up from its own shutdown. SS2.13.3b counts
15
+ * "process exit **or transport EOF**" as a death, so somebody has to know
16
+ * the difference, and only the layer that owns `close()` does.
17
+ * - WHAT AN EARLIER DEATH DISCARDED. `DiscardedSessions` is client-scoped, so
18
+ * a `commandId` refusal and a withheld reattach survive the `Session` object
19
+ * that died — which is exactly what "do not replay the SESSION's commandIds"
20
+ * requires of a client that then opens a new session.
21
+ *
22
+ * OPTIONAL MEANS OMITTED, NEVER `null` (tdd SS1.2). Every member this module
23
+ * can leave unset is optional-non-nullable in the generated params, so an
24
+ * unset member is dropped from the frame rather than nulled. The guards below
25
+ * are LOOSE (`!= null`) on purpose: the types only bind strict TypeScript
26
+ * consumers, and a plain-JS caller reaching the built `dist` can pass an
27
+ * explicit `null` that a `!== undefined` guard would forward verbatim. `false
28
+ * != null` is true, so `excludeItems: false` still serializes.
29
+ *
30
+ * The params and the caller options are both COMPOSED from the generated
31
+ * types rather than restated (INV-001: narrow and compose, never restate).
32
+ * Composition alone does NOT stop drift, though: the guard chains below are
33
+ * hand-listed, so a regenerated member would be settable by a caller and then
34
+ * silently dropped on the way to the wire. The `AssertNever` pins next to each
35
+ * chain close that gap — add a member upstream and the BUILD fails here until
36
+ * it is forwarded (or explicitly allow-listed as unforwardable).
37
+ */
38
+ import type { Connection } from "../connection/connection.js";
39
+ import type { ExitClassification, SpawnedMspConnection } from "../connection/spawn.js";
40
+ import type { SessionDurabilityProfile } from "./host-death.js";
41
+ import { Session } from "./session.js";
42
+ import type { InitializeParams, InitializeResult, SessionResumeParams, SessionStartParams } from "../msp.js";
43
+ /**
44
+ * Caller-facing options for `session/start` (tdd SS2.5.1).
45
+ *
46
+ * `config` is excluded as well as `commandId` until the facade implements the
47
+ * required `sessionMcp` grant check and forwarding.
48
+ */
49
+ export type StartSessionOptions = Readonly<Omit<SessionStartParams, "commandId" | "config"> & {
50
+ /**
51
+ * Require the `permissionProfile` selection (TEST-42624-10): when set,
52
+ * the client proves the committed selection equals the requested id —
53
+ * from the start result's `session.permissionState`, else a
54
+ * `session/read` read-back — and throws `PermissionProfileNotApplied`
55
+ * instead of proceeding on the host default. Client-local: never sent
56
+ * on the wire. Setting it without `permissionProfile` is a usage error.
57
+ */
58
+ requirePermissionProfile?: boolean;
59
+ }>;
60
+ /** Caller-facing options for `session/resume` (tdd SS2.5.2). */
61
+ export type ResumeSessionOptions = Readonly<Omit<SessionResumeParams, "commandId" | "config">>;
62
+ /**
63
+ * Options for building a `MuseClient` around a connection you already own.
64
+ * `MuseClient.spawn` fills these in for you; supply them yourself only when
65
+ * you compose the client with your own transport and handshake.
66
+ */
67
+ export interface MuseClientOptions {
68
+ /**
69
+ * The handshake's `sessionDurability`, read through `readSessionDurability`.
70
+ *
71
+ * REQUIRED, for the same reason `SessionOptions.durability` is: it decides
72
+ * what happens to every in-flight item and command when the host dies
73
+ * (SS2.13.3b), and a default would let a caller skip reading the handshake
74
+ * and silently inherit the wrong obligation. `MuseClient.spawn` supplies it.
75
+ */
76
+ readonly durability: SessionDurabilityProfile;
77
+ /**
78
+ * The owned host, set by `MuseClient.spawn`. A client built around a
79
+ * connection somebody else owns has none, and says so by omitting it — which
80
+ * is why `close()` falls back to closing just the connection.
81
+ */
82
+ readonly host?: SpawnedMspConnection;
83
+ }
84
+ /** What `MuseClient.spawn` needs: FR-018's `{ museBin, args?, env? }`, plus the handshake. */
85
+ export interface MuseClientSpawnOptions {
86
+ readonly museBin: string;
87
+ readonly args?: readonly string[];
88
+ readonly cwd?: string;
89
+ /** REPLACES the child's environment (PATH included) — spread `process.env` to extend it. */
90
+ readonly env?: NodeJS.ProcessEnv;
91
+ /** Client identification, forwarded verbatim into `initialize` (SS1.4). */
92
+ readonly clientInfo: InitializeParams["clientInfo"];
93
+ /** Capability posture; absent means all defaults. */
94
+ readonly capabilities?: InitializeParams["capabilities"];
95
+ /** Raw stderr chunks, drained from process birth and never parsed (INV-010). */
96
+ readonly onStderr?: (chunk: string) => void;
97
+ /** See `MuseServeChildOptions.shutdownTimeoutMs`. */
98
+ readonly shutdownTimeoutMs?: number;
99
+ }
100
+ /**
101
+ * Your entry point to the SDK.
102
+ *
103
+ * Spawn a host with `MuseClient.spawn`, then open conversations with
104
+ * `startSession` or `resumeSession`. The client owns the connection: it
105
+ * routes every server event to the session it belongs to and reports a host
106
+ * death to all of them. Call `close` to shut the host down cleanly.
107
+ */
108
+ export declare class MuseClient {
109
+ #private;
110
+ constructor(connection: Connection, options: MuseClientOptions);
111
+ /**
112
+ * Spawn an owned host, run the SS1.4 handshake, and hand back a client whose
113
+ * durability profile came from that handshake (FR-018).
114
+ *
115
+ * The profile is READ rather than asked of the caller, which is the whole
116
+ * reason this factory exists beside the bare constructor: SS2.13.1 makes the
117
+ * absent/unrecognized distinction load-bearing, and a caller re-deriving it
118
+ * by hand is a caller that can get it wrong.
119
+ *
120
+ * Rejects with {@link MuseHostStartError} when the host is lost before the
121
+ * handshake completes (the binary could not be spawned, or the process
122
+ * ended first — exit code or signal plus the bounded stderr tail;
123
+ * `answeredInitialize` says whether it died after a usable answer). A host
124
+ * that answers with a reply the SDK refuses keeps its own error: an
125
+ * `MspError` for an SS1.6 refusal, or a `ProtocolError` (with the whole
126
+ * refused frame on `line`) for a reply the SDK cannot use, such as one with
127
+ * no schema fingerprint.
128
+ */
129
+ static spawn(options: MuseClientSpawnOptions): Promise<MuseClient>;
130
+ /**
131
+ * The handshake result, when this client was built by `spawn`.
132
+ *
133
+ * A plain `Error`, not a typed one: reading this off a client that was not
134
+ * built by `spawn` is API misuse, not a protocol STATE an embedder branches
135
+ * on — a typed class here would send the SS2.13.3b recovery path chasing a
136
+ * host death that never happened.
137
+ */
138
+ get initializeResult(): InitializeResult;
139
+ /** The host's SS2.11 exit row, when this client was built by `spawn`. */
140
+ get exit(): Promise<ExitClassification>;
141
+ /** The durability profile every session this client opens inherits. */
142
+ get durability(): SessionDurabilityProfile;
143
+ /** Open a new root session (tdd SS2.5.1). */
144
+ startSession(options?: StartSessionOptions): Promise<Session>;
145
+ /**
146
+ * Load an existing session and subscribe this connection to its view
147
+ * (tdd SS2.5.2).
148
+ *
149
+ * WITHHELD after an ephemeral host death (SS2.13.3b clause 2, "do not attempt
150
+ * to reattach"), and refused on THIS side of the transport: a reattach
151
+ * attempt that reaches the wire has already violated the clause, whatever the
152
+ * server answers.
153
+ */
154
+ resumeSession(options: ResumeSessionOptions): Promise<Session>;
155
+ /**
156
+ * Shut the host down in an orderly way (SS2.1.2); the shutdown this causes
157
+ * is not reported to your sessions as a host death.
158
+ *
159
+ * The EOF this close produces is NOT a death, which is what `#closing`
160
+ * records.
161
+ *
162
+ * `#closing` covers only the EOF; it must not swallow the EXIT ROW. When the
163
+ * host ignores stdin EOF, `#child.close()` escalates to SIGTERM/SIGKILL and
164
+ * the exit classifies as a crash — an abnormal death under SS2.13.3b no
165
+ * matter who started the close, and one no transport event will ever report
166
+ * here because `#closing` already claimed the EOF. So the classification the
167
+ * close itself returns is forwarded to every session: `hostExited` answers
168
+ * `notADeath` for `cleanShutdown`, keeping the orderly arm inert.
169
+ */
170
+ close(): Promise<void>;
171
+ }
@@ -0,0 +1,442 @@
1
+ /**
2
+ * `MuseClient` — the SS7.1 facade's session-opening half (FR-018, T030).
3
+ *
4
+ * It turns the two v1 session verbs into correctly-shaped `session/start` /
5
+ * `session/resume` params and hands back a WIRED `Session`. It frames nothing
6
+ * and mints nothing: `Connection.command()` stamps the `commandId` from the
7
+ * single injected mint, so INV-013's single-minter property is preserved by
8
+ * DELEGATION rather than re-implemented here.
9
+ *
10
+ * IT OWNS TWO FACTS NO OTHER LAYER HOLDS, and that is why obligations (b) and
11
+ * (c) of T030 live here rather than on `Session`:
12
+ *
13
+ * - WHETHER A CLOSE WAS OURS. `Connection` is duplex-generic; a transport
14
+ * cannot tell a peer's hang-up from its own shutdown. SS2.13.3b counts
15
+ * "process exit **or transport EOF**" as a death, so somebody has to know
16
+ * the difference, and only the layer that owns `close()` does.
17
+ * - WHAT AN EARLIER DEATH DISCARDED. `DiscardedSessions` is client-scoped, so
18
+ * a `commandId` refusal and a withheld reattach survive the `Session` object
19
+ * that died — which is exactly what "do not replay the SESSION's commandIds"
20
+ * requires of a client that then opens a new session.
21
+ *
22
+ * OPTIONAL MEANS OMITTED, NEVER `null` (tdd SS1.2). Every member this module
23
+ * can leave unset is optional-non-nullable in the generated params, so an
24
+ * unset member is dropped from the frame rather than nulled. The guards below
25
+ * are LOOSE (`!= null`) on purpose: the types only bind strict TypeScript
26
+ * consumers, and a plain-JS caller reaching the built `dist` can pass an
27
+ * explicit `null` that a `!== undefined` guard would forward verbatim. `false
28
+ * != null` is true, so `excludeItems: false` still serializes.
29
+ *
30
+ * The params and the caller options are both COMPOSED from the generated
31
+ * types rather than restated (INV-001: narrow and compose, never restate).
32
+ * Composition alone does NOT stop drift, though: the guard chains below are
33
+ * hand-listed, so a regenerated member would be settable by a caller and then
34
+ * silently dropped on the way to the wire. The `AssertNever` pins next to each
35
+ * chain close that gap — add a member upstream and the BUILD fails here until
36
+ * it is forwarded (or explicitly allow-listed as unforwardable).
37
+ */
38
+ import { initializeAnswered, spawnMspConnection } from "../connection/spawn.js";
39
+ import { MuseSessionDiscardedError, PermissionProfileNotApplied } from "../errors.js";
40
+ import { DiscardedSessions } from "./discarded.js";
41
+ import { isAbnormalHostDeath, readSessionDurability } from "./host-death.js";
42
+ import { MspError, ProtocolError } from "../connection/connection.js";
43
+ import { hostStartFailure } from "./host-start.js";
44
+ import { Session } from "./session.js";
45
+ /**
46
+ * Every `StartParams` member `startSession` forwards. `config` is temporarily
47
+ * excluded until the facade can pair forwarding with the required
48
+ * `sessionMcp` grant check.
49
+ */
50
+ const START_FORWARDED = [
51
+ "approvalMode",
52
+ "providerId",
53
+ "sessionId",
54
+ "workspaceRoot",
55
+ "workspaceRoots",
56
+ "modelId",
57
+ "permissionProfile",
58
+ ];
59
+ /**
60
+ * Every `ResumeParams` member `resumeSession` forwards. `config` has the same
61
+ * temporary capability-check boundary as the start facade above.
62
+ */
63
+ const RESUME_FORWARDED = ["sessionId", "cursor", "excludeItems", "history"];
64
+ /**
65
+ * The committed profile id a session snapshot claims, or null when the
66
+ * snapshot is silent (the stable bundle strips the member; absent and null
67
+ * both mean unknown, never applied). Structural on purpose: the stable
68
+ * `Session` type carries no `permissionState`, and an unknown wire shape
69
+ * must read as unknown rather than throw.
70
+ */
71
+ function committedProfileId(snapshot) {
72
+ if (typeof snapshot !== "object" || snapshot === null)
73
+ return null;
74
+ const state = snapshot.permissionState;
75
+ if (typeof state !== "object" || state === null)
76
+ return null;
77
+ const id = state.profileId;
78
+ return typeof id === "string" ? id : null;
79
+ }
80
+ /**
81
+ * Your entry point to the SDK.
82
+ *
83
+ * Spawn a host with `MuseClient.spawn`, then open conversations with
84
+ * `startSession` or `resumeSession`. The client owns the connection: it
85
+ * routes every server event to the session it belongs to and reports a host
86
+ * death to all of them. Call `close` to shut the host down cleanly.
87
+ */
88
+ export class MuseClient {
89
+ #connection;
90
+ #durability;
91
+ #discarded;
92
+ /** Sessions this client opened, so one death notification reaches them all. */
93
+ #sessions = new Set();
94
+ /**
95
+ * Frames naming a session this client has not registered YET, held only
96
+ * while a `session/start`/`session/resume` is in flight (#49790): the host
97
+ * flushes the resume replay BEFORE the result, and the promise awaiting that
98
+ * result resumes only after the whole chunk has been routed. `#route` holds,
99
+ * `#openSession` drains, `#open` clears when the last open settles.
100
+ */
101
+ #held = new Map();
102
+ #opening = 0;
103
+ /** The spawned host, when `spawn` built this client. */
104
+ #child;
105
+ #initializeResult;
106
+ /**
107
+ * Set by `close()` BEFORE the connection is torn down.
108
+ *
109
+ * This flag is the entire discriminator for obligation (b): the same
110
+ * transport EOF is an orderly SS2.1.2 shutdown when we caused it and an
111
+ * abnormal death when we did not, and nothing below this layer can tell.
112
+ */
113
+ #closing = false;
114
+ /** Latched by an abnormal EOF, so a later `resumeSession` can be withheld. */
115
+ #hostDied = false;
116
+ constructor(connection, options) {
117
+ this.#connection = connection;
118
+ this.#durability = options.durability;
119
+ // Client-owned, never injected: SS2.13.3b is a one-client obligation and
120
+ // no current consumer spans two clients over one host lineage, so a
121
+ // sharing knob would be declared-ahead surface (Constitution XI).
122
+ this.#discarded = new DiscardedSessions();
123
+ this.#child = options.host?.child;
124
+ this.#initializeResult = options.host?.initializeResult;
125
+ // THE ONE INBOUND PUMP (FR-018/FR-019). `Session.apply` is the only event
126
+ // entry point and `#connection` is private, so without this a spawn-built
127
+ // consumer has no way to hear a single view frame: every turn wait hangs
128
+ // (SS3.1.4) and `onApproval` can never fire. `Connection.onNotification`
129
+ // holds ONE handler — this client claims it, which is part of what
130
+ // "MuseClient owns the connection" means; a consumer that needs the raw
131
+ // feed builds around a bare `Connection` and `Session` instead.
132
+ this.#connection.onNotification((notification) => this.#route(notification));
133
+ // Registered at construction, not at the first session: an EOF can land
134
+ // before any session exists, and the flag has to be read at that moment.
135
+ void this.#connection.closed.then(() => this.#transportClosed());
136
+ }
137
+ /**
138
+ * Deliver one view notification to the session(s) it names.
139
+ *
140
+ * A frame naming NO session is dropped here rather than fed to every
141
+ * session: `Session.apply` already tolerates unknown methods, but only the
142
+ * frames that name a `sessionId` have an owner to route to. A frame naming
143
+ * an UNKNOWN session is dropped for the same reason — routing it anywhere
144
+ * would trip the foreign-frame throw on a session it does not belong to —
145
+ * UNLESS an open is in flight, when it may be that open's own session named
146
+ * by the server before the awaiting promise has run (#49790): then it is
147
+ * held for `#openSession` to fold, or for `#open` to clear.
148
+ */
149
+ #route(notification) {
150
+ const named = notification.params?.sessionId;
151
+ if (typeof named !== "string")
152
+ return;
153
+ let delivered = false;
154
+ for (const session of this.#sessions) {
155
+ if (session.sessionId === named) {
156
+ session.apply(notification);
157
+ delivered = true;
158
+ }
159
+ }
160
+ if (delivered || this.#opening === 0)
161
+ return;
162
+ const held = this.#held.get(named);
163
+ if (held === undefined)
164
+ this.#held.set(named, [notification]);
165
+ else
166
+ held.push(notification);
167
+ }
168
+ /**
169
+ * Spawn an owned host, run the SS1.4 handshake, and hand back a client whose
170
+ * durability profile came from that handshake (FR-018).
171
+ *
172
+ * The profile is READ rather than asked of the caller, which is the whole
173
+ * reason this factory exists beside the bare constructor: SS2.13.1 makes the
174
+ * absent/unrecognized distinction load-bearing, and a caller re-deriving it
175
+ * by hand is a caller that can get it wrong.
176
+ *
177
+ * Rejects with {@link MuseHostStartError} when the host is lost before the
178
+ * handshake completes (the binary could not be spawned, or the process
179
+ * ended first — exit code or signal plus the bounded stderr tail;
180
+ * `answeredInitialize` says whether it died after a usable answer). A host
181
+ * that answers with a reply the SDK refuses keeps its own error: an
182
+ * `MspError` for an SS1.6 refusal, or a `ProtocolError` (with the whole
183
+ * refused frame on `line`) for a reply the SDK cannot use, such as one with
184
+ * no schema fingerprint.
185
+ */
186
+ static async spawn(options) {
187
+ const handshake = spawnMspConnection({
188
+ command: options.museBin,
189
+ ...(options.args === undefined ? {} : { args: options.args }),
190
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
191
+ ...(options.env === undefined ? {} : { env: options.env }),
192
+ ...(options.onStderr === undefined ? {} : { onStderr: options.onStderr }),
193
+ ...(options.shutdownTimeoutMs === undefined
194
+ ? {}
195
+ : { shutdownTimeoutMs: options.shutdownTimeoutMs }),
196
+ });
197
+ let spawned;
198
+ try {
199
+ spawned = await handshake.initialize({
200
+ clientInfo: options.clientInfo,
201
+ ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),
202
+ });
203
+ }
204
+ catch (error) {
205
+ // A failed handshake must not leak the process it already spawned.
206
+ await handshake.close().catch(() => undefined);
207
+ // A host that ANSWERED `initialize` is not a lost host, however it ends
208
+ // once we close it: its answer is the actionable error. "Answered" is
209
+ // evidence that a response frame came back — an SS1.6 `MspError`, or a
210
+ // `ProtocolError` carrying the frame the SDK refused (a malformed or
211
+ // version-skewed reply) — never the error's class alone (#49912).
212
+ // Decided before the exit is read, so our own close ladder can never
213
+ // dress an answer up as a death.
214
+ const answered = error instanceof MspError || (error instanceof ProtocolError && error.line !== undefined);
215
+ if (answered)
216
+ throw error;
217
+ // No refused frame: the host was lost — before answering, or after a
218
+ // usable answer while we sent `initialized` (the error then says so).
219
+ // close() ended at its observed exit (or its spawn failure), so the
220
+ // evidence is settled: the typed error carries the spawn error or the
221
+ // SS2.11 row plus the stderr tail instead of the transport's first
222
+ // symptom (#49791, #49912).
223
+ throw await hostStartFailure(handshake.child, error, handshake[initializeAnswered]());
224
+ }
225
+ return new MuseClient(spawned.connection, {
226
+ durability: readSessionDurability(spawned.initializeResult),
227
+ host: spawned,
228
+ });
229
+ }
230
+ /**
231
+ * The handshake result, when this client was built by `spawn`.
232
+ *
233
+ * A plain `Error`, not a typed one: reading this off a client that was not
234
+ * built by `spawn` is API misuse, not a protocol STATE an embedder branches
235
+ * on — a typed class here would send the SS2.13.3b recovery path chasing a
236
+ * host death that never happened.
237
+ */
238
+ get initializeResult() {
239
+ if (this.#initializeResult === undefined) {
240
+ throw new Error("initializeResult is only available on a client built by MuseClient.spawn");
241
+ }
242
+ return this.#initializeResult;
243
+ }
244
+ /** The host's SS2.11 exit row, when this client was built by `spawn`. */
245
+ get exit() {
246
+ if (this.#child === undefined) {
247
+ // Plain `Error` for the same reason as `initializeResult`: API misuse,
248
+ // not a protocol state.
249
+ throw new Error("exit is only available on a client built by MuseClient.spawn");
250
+ }
251
+ return this.#child.exit;
252
+ }
253
+ /** The durability profile every session this client opens inherits. */
254
+ get durability() {
255
+ return this.#durability;
256
+ }
257
+ /** Open a new root session (tdd SS2.5.1). */
258
+ async startSession(options = {}) {
259
+ const params = {};
260
+ if (options.approvalMode != null)
261
+ params.approvalMode = options.approvalMode;
262
+ if (options.providerId != null)
263
+ params.providerId = options.providerId;
264
+ if (options.sessionId != null)
265
+ params.sessionId = options.sessionId;
266
+ if (options.workspaceRoot != null)
267
+ params.workspaceRoot = options.workspaceRoot;
268
+ if (options.workspaceRoots != null)
269
+ params.workspaceRoots = options.workspaceRoots;
270
+ if (options.modelId != null)
271
+ params.modelId = options.modelId;
272
+ // The initial permission-profile selection, forwarded verbatim like the
273
+ // other start members (undefined stays omitted).
274
+ if (options.permissionProfile != null)
275
+ params.permissionProfile = options.permissionProfile;
276
+ // TEST-42624-10: a caller that requires its selection proves it, never
277
+ // assuming the host default. The flag without the member is API misuse
278
+ // (plain Error, like the other misuse guards here), raised pre-wire.
279
+ const required = options.requirePermissionProfile === true;
280
+ if (required && options.permissionProfile == null) {
281
+ throw new Error("requirePermissionProfile needs permissionProfile: require the selection you pass");
282
+ }
283
+ // Keyed by the id the SERVER named, never the one the caller asked for:
284
+ // `sessionId` is a REQUEST on start, and a session keyed by a hopeful id
285
+ // would reject every one of its own events as foreign.
286
+ const session = await this.#open("session/start", params, (raw) => ({
287
+ result: raw,
288
+ verb: "session/start",
289
+ }));
290
+ if (!required)
291
+ return session;
292
+ return this.#proveSelection(session, options.permissionProfile);
293
+ }
294
+ /**
295
+ * Prove a required start selection (TEST-42624-10): the start result's own
296
+ * `session.permissionState` first (absent = unknown, never applied); else a
297
+ * point-in-time `session/read`, which never subscribes. Only `profileId ==
298
+ * requested` proceeds; anything else throws `PermissionProfileNotApplied`
299
+ * BEFORE any turn is sent. The refused session is dropped from the client
300
+ * (no orphan keeps folding for an id the caller can never reach — the SDK
301
+ * has no set/close-by-id verb); the error's `sessionId` is the caller's
302
+ * handle to resume via `sessionId` or start again. A failed read-back
303
+ * propagates: the client still never proceeds on the default.
304
+ */
305
+ async #proveSelection(session, requested) {
306
+ const opening = session.opening;
307
+ const started = opening !== undefined && opening.verb === "session/start"
308
+ ? committedProfileId(opening.result.session)
309
+ : null;
310
+ if (started !== null) {
311
+ if (started === requested)
312
+ return session;
313
+ this.#sessions.delete(session);
314
+ throw new PermissionProfileNotApplied(requested, started, session.sessionId);
315
+ }
316
+ const read = await this.#connection.command("session/read", {
317
+ sessionId: session.sessionId,
318
+ });
319
+ const served = committedProfileId(read["session"]);
320
+ if (served === requested)
321
+ return session;
322
+ this.#sessions.delete(session);
323
+ throw new PermissionProfileNotApplied(requested, served, session.sessionId);
324
+ }
325
+ /**
326
+ * Load an existing session and subscribe this connection to its view
327
+ * (tdd SS2.5.2).
328
+ *
329
+ * WITHHELD after an ephemeral host death (SS2.13.3b clause 2, "do not attempt
330
+ * to reattach"), and refused on THIS side of the transport: a reattach
331
+ * attempt that reaches the wire has already violated the clause, whatever the
332
+ * server answers.
333
+ */
334
+ async resumeSession(options) {
335
+ this.#assertReattachAllowed(options.sessionId);
336
+ const params = { sessionId: options.sessionId };
337
+ if (options.cursor != null)
338
+ params.cursor = options.cursor;
339
+ if (options.excludeItems != null)
340
+ params.excludeItems = options.excludeItems;
341
+ if (options.history != null)
342
+ params.history = options.history;
343
+ return this.#open("session/resume", params, (raw) => ({
344
+ result: raw,
345
+ verb: "session/resume",
346
+ }));
347
+ }
348
+ /**
349
+ * Shut the host down in an orderly way (SS2.1.2); the shutdown this causes
350
+ * is not reported to your sessions as a host death.
351
+ *
352
+ * The EOF this close produces is NOT a death, which is what `#closing`
353
+ * records.
354
+ *
355
+ * `#closing` covers only the EOF; it must not swallow the EXIT ROW. When the
356
+ * host ignores stdin EOF, `#child.close()` escalates to SIGTERM/SIGKILL and
357
+ * the exit classifies as a crash — an abnormal death under SS2.13.3b no
358
+ * matter who started the close, and one no transport event will ever report
359
+ * here because `#closing` already claimed the EOF. So the classification the
360
+ * close itself returns is forwarded to every session: `hostExited` answers
361
+ * `notADeath` for `cleanShutdown`, keeping the orderly arm inert.
362
+ */
363
+ async close() {
364
+ this.#closing = true;
365
+ if (this.#child !== undefined) {
366
+ const exit = await this.#child.close();
367
+ if (isAbnormalHostDeath(exit)) {
368
+ for (const session of this.#sessions)
369
+ session.hostExited(exit);
370
+ }
371
+ return;
372
+ }
373
+ await this.#connection.close();
374
+ }
375
+ /**
376
+ * The window `#route` holds unknown-session frames for: from the request to
377
+ * the Session's registration. Counted, not flagged, so two concurrent opens
378
+ * keep it open until the LAST settles; cleared on settle so a frame that
379
+ * named no session this client opened is held no longer than the open it
380
+ * rode in with (#49790).
381
+ */
382
+ async #open(verb, params, opening) {
383
+ this.#opening += 1;
384
+ try {
385
+ const opened = opening(await this.#connection.command(verb, params));
386
+ return this.#openSession(opened.result.session.sessionId, opened);
387
+ }
388
+ finally {
389
+ this.#opening -= 1;
390
+ if (this.#opening === 0)
391
+ this.#held.clear();
392
+ }
393
+ }
394
+ #openSession(sessionId, opening) {
395
+ const session = new Session({
396
+ connection: this.#connection,
397
+ discarded: this.#discarded,
398
+ durability: this.#durability,
399
+ opening,
400
+ sessionId,
401
+ });
402
+ this.#sessions.add(session);
403
+ // The frames that named this session while its verb was in flight fold
404
+ // NOW, in arrival order — synchronously, so nothing the pump routes next
405
+ // can overtake them.
406
+ const held = this.#held.get(sessionId);
407
+ if (held !== undefined) {
408
+ this.#held.delete(sessionId);
409
+ for (const frame of held)
410
+ session.apply(frame);
411
+ }
412
+ return session;
413
+ }
414
+ #assertReattachAllowed(sessionId) {
415
+ // Two facts, both recorded by an ephemeral discharge: THIS session was
416
+ // discarded, or this client's own ephemeral host died (in which case there
417
+ // is nothing on the other end to reattach TO, whatever id is asked for).
418
+ if (this.#discarded.sessionIds.has(sessionId)) {
419
+ throw new MuseSessionDiscardedError(`session ${sessionId} was discarded after an ephemeral host death; it cannot be resumed`);
420
+ }
421
+ if (this.#hostDied && !this.#survivesHostDeath()) {
422
+ throw new MuseSessionDiscardedError("this client's ephemeral host died; SS2.13.3b forbids reattaching to it");
423
+ }
424
+ }
425
+ #survivesHostDeath() {
426
+ return this.#durability.kind === "durable";
427
+ }
428
+ /**
429
+ * The transport reached EOF. If this client did not cause it, that is
430
+ * SS2.13.3b's second death notification (T030 obligation b) and every session
431
+ * this client opened discharges through the SAME `Session.hostExited` path
432
+ * the process exit uses — including its latch, so whichever notification
433
+ * arrives second replays the first one's report instead of a half delta.
434
+ */
435
+ #transportClosed() {
436
+ if (this.#closing)
437
+ return;
438
+ this.#hostDied = true;
439
+ for (const session of this.#sessions)
440
+ session.hostExited({ kind: "transportEof" });
441
+ }
442
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * What an ephemeral host death discarded, remembered ACROSS the sessions one
3
+ * client opened (spec 14990 T030 obligation (c); tdd SS2.13.3b).
4
+ *
5
+ * SS2.13.3b's five-clause MUST includes "do not attempt to reattach" and "do
6
+ * not replay the session's `commandId`s". Both are statements about what the
7
+ * CLIENT does next, and "next" outlives the session object that died:
8
+ *
9
+ * - `PendingCommandSet` already refuses a replay of an id IT discarded, but
10
+ * its memory is per-instance, so a FRESH `Session` — the natural thing to
11
+ * build after a host dies — knew nothing and would replay those ids at the
12
+ * new host. That is the exactly-once violation the clause exists to prevent,
13
+ * and it is why T030b's own arm could only name the gap;
14
+ * - nothing at all held the discarded `sessionId`, so `resumeSession` had no
15
+ * fact to withhold on.
16
+ *
17
+ * This is client state, never a wire shape and never durable (INV-007): it
18
+ * lives exactly as long as the `MuseClient` that owns it. It is deliberately
19
+ * NOT a module-global — two clients in one process are two independent trust
20
+ * boundaries, and a process-wide set would let one client's dead host silence
21
+ * another's live one.
22
+ */
23
+ /**
24
+ * The two sets, exposed directly rather than behind add/has pairs.
25
+ *
26
+ * `PendingCommandSet` takes the `commandIds` set BY REFERENCE and writes to it
27
+ * from `discardEphemeral()`, so a wrapper would have to re-expose a mutable
28
+ * seam anyway; a `Set<string>` is the honest shape for one, and it is already
29
+ * the shape that class used internally.
30
+ */
31
+ export declare class DiscardedSessions {
32
+ /** `commandId`s an ephemeral discharge retired. Never replayed again. */
33
+ readonly commandIds: Set<string>;
34
+ /** `sessionId`s whose ephemeral host died. Never reattached. */
35
+ readonly sessionIds: Set<string>;
36
+ }