@sagentlab/navarch-runtime 0.1.116 → 0.1.118

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 (52) hide show
  1. package/README.md +43 -17
  2. package/bin/muse-mcp-bridge.cjs +3 -0
  3. package/bin/worktree-guard-hook.cjs +74 -2
  4. package/dist/adapters/index.cjs +6 -1
  5. package/dist/adapters/muse.cjs +433 -0
  6. package/dist/agent-auth.cjs +2 -1
  7. package/dist/config.cjs +8 -1
  8. package/dist/conversation/capability.cjs +1 -1
  9. package/dist/exit-conditions.cjs +1 -1
  10. package/dist/muse-mcp-bridge.cjs +167 -0
  11. package/dist/session.cjs +30 -8
  12. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  13. package/node_modules/@muse-code/sdk/README.md +88 -0
  14. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  15. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  16. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  17. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  18. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  19. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  20. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  21. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  22. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  23. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  37. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  38. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  39. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  40. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  41. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  45. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  46. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  47. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  48. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  49. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  50. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  51. package/node_modules/@muse-code/sdk/package.json +47 -0
  52. package/package.json +8 -4
@@ -0,0 +1,427 @@
1
+ /**
2
+ * `SessionFold` — the SS4 client fold, bound to the generated wire types
3
+ * (spec 14990 FR-007, tasks.md T019).
4
+ *
5
+ * `ItemStore` and `SessionStateStore` are generic because they were written
6
+ * before the session-view plane entered the generated declarations. This
7
+ * module is where the generic parameters are BOUND: items are `@muse-code/msp`'s
8
+ * `Item`, state-family values are the generated `session/*` params objects,
9
+ * and every event this fold accepts is typed as the params object the wire
10
+ * actually carries. Nothing here restates a wire shape (INV-001).
11
+ *
12
+ * Beyond the two-store composition, the fold owns the parts of the view that
13
+ * are neither an item nor a state family:
14
+ *
15
+ * - the TURN LIFECYCLE — `turn/started`, `turn/completed`, `turn/retracted`,
16
+ * `turn/unqueued`, and the non-terminal `turn/retryScheduled` and
17
+ * `turn/foregroundCompleted`;
18
+ * - the APPROVAL and USER-INPUT view events as fold INPUTS — the pending
19
+ * sets and their first durable terminal. The decision flow (choosing and
20
+ * sending a resolution) is the facade's, not the fold's;
21
+ * - the DELIVERY marker `view/gap` — it seeds no store, it moves the fold's
22
+ * CURRENCY (`pendingGap`/`current`, FM-003). The recovery that fills the
23
+ * hole needs client→server I/O and is therefore the facade's (T032).
24
+ *
25
+ * INV-006 governs all of it: the fold never invents a terminal. A retract and
26
+ * a reclaim are not `TurnTerminal` values and are never rendered as one, an
27
+ * unrecognized `TurnTerminal` is kept verbatim, and a second resolution never
28
+ * displaces the first durable one.
29
+ */
30
+ import { ItemStore } from "./item-store.js";
31
+ import { SessionStateStore } from "./state-store.js";
32
+ /**
33
+ * The whole client-side session view: items, session state, turns, and the
34
+ * pending approval/user-input sets.
35
+ */
36
+ export class SessionFold {
37
+ #itemStore = new ItemStore();
38
+ #sessionStateStore = new SessionStateStore();
39
+ /** Items, snapshot-only: the mutators are `apply()`'s, not the consumer's. */
40
+ get items() {
41
+ return this.#itemStore;
42
+ }
43
+ /**
44
+ * Discharge the SS2.13.3b ephemeral host-death obligation over the item
45
+ * half: every item still in progress becomes terminal-unknown and the fold
46
+ * refuses further events (spec FM-002; slice 3's T030b).
47
+ *
48
+ * A FOLD-level method rather than a `FoldItems` entry on purpose. `items` is
49
+ * snapshot-only — an allowlist whose whole job is that a store mutator can
50
+ * never leak onto the consumer surface (PR #23087 review round) — and this
51
+ * IS a mutator. Widening the allowlist to reach it would undo that; routing
52
+ * it here keeps exactly one caller (`Session.hostExited`) and leaves the
53
+ * read surface untouched. The in-progress probe stays the caller's, so the
54
+ * store remains wire-shape-blind (INV-001).
55
+ */
56
+ markEphemeralHostDeath(isInProgress) {
57
+ return this.#itemStore.markEphemeralHostDeath(isInProgress);
58
+ }
59
+ /**
60
+ * The session-state families, keyed by notification method name. Named
61
+ * `sessionState` rather than `state` because the surface contract reserves
62
+ * `state` for the FR-008 aggregate that arrives with snapshot ingestion.
63
+ * Snapshot-only, like `items`.
64
+ */
65
+ get sessionState() {
66
+ // The ONE narrowing cast, here rather than at every consumer. The store
67
+ // is generic over a `string` family key (it predates the generated
68
+ // session-view plane, INV-001), but `apply`'s seven `session/*` arms are
69
+ // its only writers in this fold — an unrecognized method reaches the
70
+ // ignore arm, never the store — so every key it can hold IS a
71
+ // `SessionStateMethod`.
72
+ return this.#sessionStateStore;
73
+ }
74
+ #turns = new Map();
75
+ #pendingApprovals = new Map();
76
+ #resolvedApprovals = new Map();
77
+ #pendingUserInputs = new Map();
78
+ #settledUserInputs = new Map();
79
+ #activeTurnId;
80
+ #pendingGap;
81
+ /** The turn currently running, if one is. */
82
+ get activeTurnId() {
83
+ return this.#activeTurnId;
84
+ }
85
+ /**
86
+ * The outstanding delivery hole, or `undefined` when there is none (tdd
87
+ * SS4.8, FM-003).
88
+ *
89
+ * COALESCED, keeping the FIRST `after`: consecutive overflows are one hole
90
+ * from a filler's point of view, which is the client mirror of the server's
91
+ * own D-16487-1 bracket rule. A recovery walks `(after, next)` and calls
92
+ * `gapFilled(next)` with the target it actually reached, so a hole that grew
93
+ * while the walk was in flight cannot be reported filled.
94
+ */
95
+ get pendingGap() {
96
+ return this.#pendingGap;
97
+ }
98
+ /**
99
+ * Is this fold current with the session view?
100
+ *
101
+ * FM-003's "only then reports current": false from the moment a `view/gap`
102
+ * folds until its hole is filled. Live events keep folding meanwhile — they
103
+ * are real facts and dropping them would add a second hole — so this flag is
104
+ * the ONLY statement that the transcript has a hole in it.
105
+ */
106
+ get current() {
107
+ return this.#pendingGap === undefined;
108
+ }
109
+ /**
110
+ * A recovery filled the hole up to `next`. Returns whether it cleared.
111
+ *
112
+ * EQUALITY, never a relational compare (tdd SS4.1): the argument is the
113
+ * target the walk reached, and it clears only when that is still the
114
+ * outstanding target. A gap that arrived mid-walk left a newer `next`, so
115
+ * this answers `false` and the fold stays not-current — the truthful answer,
116
+ * since the second hole is unfilled.
117
+ *
118
+ * A fold-level mutator with the same rationale as `markEphemeralHostDeath`:
119
+ * `items`/`sessionState` are snapshot-only allowlists, and this belongs to
120
+ * neither store.
121
+ */
122
+ gapFilled(next) {
123
+ if (this.#pendingGap?.next !== next)
124
+ return false;
125
+ this.#pendingGap = undefined;
126
+ return true;
127
+ }
128
+ apply(event) {
129
+ // `params` is optional on the wire, and every recognized arm below
130
+ // dereferences it. Drop the frame here rather than let one arm throw on
131
+ // an absent params object — SS1.5.4 tolerance is drop-don't-throw, and a
132
+ // throw would take out the consumer's notification pump.
133
+ //
134
+ // NOT just `=== undefined`. `params: null` is a very common sloppy
135
+ // JSON-RPC spelling of "no params" and threw the same raw TypeError one
136
+ // value over; a non-object (`42`, a string) got further still and minted a
137
+ // phantom turn keyed `undefined`. An ARRAY needs its own clause because
138
+ // JSON-RPC 2.0 permits positional params and `typeof [] === "object"`, so
139
+ // it slipped both: `params: []` reached the arms and SETTLED a turn keyed
140
+ // `undefined`. The classification lives HERE so both this overload pair
141
+ // and `Session.apply` inherit it from one place.
142
+ if (event.params === null || typeof event.params !== "object" || Array.isArray(event.params)) {
143
+ return { kind: "ignoredMissingParams", method: event.method };
144
+ }
145
+ const typed = event;
146
+ switch (typed.method) {
147
+ case "item/started":
148
+ case "item/updated":
149
+ case "item/completed":
150
+ return { kind: "item", outcome: this.#itemStore.apply(typed.params.item) };
151
+ case "item/delta":
152
+ return {
153
+ kind: "itemDelta",
154
+ outcome: this.#itemStore.applyDelta(typed.params.itemId, typed.params.delta, typed.params.field ?? "text"),
155
+ };
156
+ case "turn/started":
157
+ return this.#turnStarted(typed.params);
158
+ case "turn/completed":
159
+ return this.#turnCompleted(typed.params);
160
+ case "turn/retracted":
161
+ return this.#turnRetracted(typed.params);
162
+ case "turn/unqueued":
163
+ return this.#turnUnqueued(typed.params);
164
+ case "turn/retryScheduled":
165
+ return this.#turnRetryScheduled(typed.params);
166
+ case "turn/foregroundCompleted":
167
+ return this.#turnForegroundCompleted(typed.params);
168
+ case "approval/requested":
169
+ return this.#approvalRequested(typed.params);
170
+ case "approval/updated":
171
+ return this.#approvalUpdated(typed.params);
172
+ case "approval/resolved":
173
+ return this.#approvalResolved(typed.params);
174
+ case "userInput/requested":
175
+ return this.#userInputRequested(typed.params);
176
+ case "userInput/settled":
177
+ return this.#userInputSettled(typed.params);
178
+ case "view/gap":
179
+ return this.#deliveryGap(typed.params);
180
+ case "session/modelRouteUnserved":
181
+ return { kind: "modelRouteUnserved", params: typed.params };
182
+ case "session/modelChanged":
183
+ case "session/reasoningEffortChanged":
184
+ case "session/goalChanged":
185
+ case "session/todoListChanged":
186
+ case "session/branchChanged":
187
+ case "session/tokenUsage":
188
+ case "session/contextUsage":
189
+ case "session/approvalModeChanged":
190
+ case "session/nameChanged":
191
+ return {
192
+ kind: "sessionState",
193
+ outcome: this.#sessionStateStore.apply(typed.method, typed.params, typed.params.viewCursor),
194
+ };
195
+ default: {
196
+ const unrecognized = event;
197
+ return { kind: "ignoredUnrecognizedMethod", method: unrecognized.method };
198
+ }
199
+ }
200
+ }
201
+ /** Turns in first-observed order. */
202
+ turns() {
203
+ return [...this.#turns.values()].map((turn) => SessionFold.#turnSnapshot(turn));
204
+ }
205
+ turn(turnId) {
206
+ const held = this.#turns.get(turnId);
207
+ return held === undefined ? undefined : SessionFold.#turnSnapshot(held);
208
+ }
209
+ /** Approvals awaiting a durable terminal, in first-observed order. */
210
+ pendingApprovals() {
211
+ return [...this.#pendingApprovals.entries()].map(([approvalId, held]) => ({
212
+ approvalId,
213
+ requested: held.requested,
214
+ ...(held.latestUpdate !== undefined ? { latestUpdate: held.latestUpdate } : {}),
215
+ }));
216
+ }
217
+ /** The winning durable terminal per approval, in first-observed order. */
218
+ resolvedApprovals() {
219
+ return [...this.#resolvedApprovals.values()];
220
+ }
221
+ /**
222
+ * Prompts awaiting a durable settlement, in first-observed order. The
223
+ * entries ARE the generated request params — the wire shape already carries
224
+ * `userInputId`, so a wrapper would only duplicate it (unlike
225
+ * `PendingApproval`, which earns its wrapper with `latestUpdate`).
226
+ */
227
+ pendingUserInputs() {
228
+ return [...this.#pendingUserInputs.values()];
229
+ }
230
+ /** The winning durable settlement per prompt, in first-observed order. */
231
+ settledUserInputs() {
232
+ return [...this.#settledUserInputs.values()];
233
+ }
234
+ // ---- turn lifecycle -----------------------------------------------------
235
+ #turnStarted(params) {
236
+ // A turn the fold has already seen leave `running` never re-opens. A
237
+ // redelivered `turn/started` after a terminal/retract/reclaim would mint
238
+ // `{state: "running", terminal: …}` — a shape `TurnEntry`'s own docs say
239
+ // cannot exist — and re-point `activeTurnId` at a turn no completion will
240
+ // follow, so a renderer keyed on it shows a running turn forever.
241
+ // `#turnFor` mints fresh entries as `running`, so a first sighting (and a
242
+ // redelivery while genuinely running) still folds.
243
+ const turn = this.#turnFor(params.turnId);
244
+ if (turn.state !== "running") {
245
+ return { kind: "ignoredStaleFrame", method: "turn/started", id: params.turnId };
246
+ }
247
+ turn.commandId = params.commandId;
248
+ turn.state = "running";
249
+ this.#activeTurnId = params.turnId;
250
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
251
+ }
252
+ #turnCompleted(params) {
253
+ // A turn the fold never saw start still settles here: single-shot and
254
+ // gap-filled turns arrive terminal-first (tdd SS4.4.1's sibling rule).
255
+ const turn = this.#turnFor(params.turnId);
256
+ // …but a RETRACTED or RECLAIMED turn stays that way. Replaying the
257
+ // completion frame that preceded an accepted retract would flip the entry
258
+ // back to `settled` and lose the retract fact entirely (the same
259
+ // redelivered-frame class as `#turnStarted` above).
260
+ if (turn.state === "retracted" || turn.state === "unqueued") {
261
+ return { kind: "ignoredStaleFrame", method: "turn/completed", id: params.turnId };
262
+ }
263
+ turn.state = "settled";
264
+ turn.terminal = params.terminal;
265
+ if (params.error !== undefined)
266
+ turn.error = params.error;
267
+ // The retry hint is a "retrying in Ns" countdown, and it clears on the
268
+ // turn's next event — which a completion is (tdd SS4.5.1). Leaving it set
269
+ // makes a renderer show the countdown on a finished turn.
270
+ turn.retryScheduled = undefined;
271
+ // Same for the foreground hint: a finished turn is not background-waiting
272
+ // (tdd SS4.5.12).
273
+ turn.foregroundCompleted = undefined;
274
+ this.#clearActive(params.turnId);
275
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
276
+ }
277
+ #turnRetracted(params) {
278
+ const turn = this.#turnFor(params.turnId);
279
+ turn.commandId = params.commandId;
280
+ // A retract is NOT a `TurnTerminal`; leaving `terminal` unset is the whole
281
+ // point (INV-006). On accepted, the retracted user message re-arrives via
282
+ // `item/updated` with `retracted: true`, which the item half folds
283
+ // (a declined outcome also lands here for now: the entry reads retracted
284
+ // and the later turn/completed arrives as a stale frame, so its terminal
285
+ // is dropped too - a known gap in this fold, behavior unchanged).
286
+ turn.state = "retracted";
287
+ this.#clearActive(params.turnId);
288
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
289
+ }
290
+ #turnUnqueued(params) {
291
+ // The reclaim of a QUEUED submit: this turn was pre-minted at admission
292
+ // and never launched, so no `turn/started` preceded it and no
293
+ // `turn/completed` will follow (tdd SS3.6, SS4.5.1). It therefore never
294
+ // becomes the active turn and never carries a terminal.
295
+ const turn = this.#turnFor(params.turnId);
296
+ turn.commandId = params.commandId;
297
+ turn.state = "unqueued";
298
+ this.#clearActive(params.turnId);
299
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
300
+ }
301
+ #turnRetryScheduled(params) {
302
+ // Non-terminal by contract: it never resolves a turn-wait (tdd SS3.1.4).
303
+ const turn = this.#turnFor(params.turnId);
304
+ // …but it is still a turn frame, so it takes the same redelivery guard as
305
+ // the other arms. Without it, replaying a whole page re-plants the retry
306
+ // hint on a turn that already left `running` — and the clear in
307
+ // `#turnCompleted` can no longer undo that, because the replayed
308
+ // completion is itself dropped. The turn would show "retrying in Ns"
309
+ // forever.
310
+ if (turn.state !== "running") {
311
+ return { kind: "ignoredStaleFrame", method: "turn/retryScheduled", id: params.turnId };
312
+ }
313
+ turn.retryScheduled = params;
314
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
315
+ }
316
+ #turnForegroundCompleted(params) {
317
+ // Non-terminal by contract: it never resolves a turn-wait (tdd SS4.5.12).
318
+ const turn = this.#turnFor(params.turnId);
319
+ // …but it is still a turn frame, so it takes the same redelivery guard
320
+ // as the retry hint: a replayed page must not re-plant the
321
+ // background-wait hint on a turn that already left `running`.
322
+ if (turn.state !== "running") {
323
+ return { kind: "ignoredStaleFrame", method: "turn/foregroundCompleted", id: params.turnId };
324
+ }
325
+ turn.foregroundCompleted = params;
326
+ return { kind: "turn", turnId: params.turnId, state: turn.state };
327
+ }
328
+ #turnFor(turnId) {
329
+ const held = this.#turns.get(turnId);
330
+ if (held !== undefined)
331
+ return held;
332
+ const fresh = { turnId, state: "running" };
333
+ this.#turns.set(turnId, fresh);
334
+ return fresh;
335
+ }
336
+ #clearActive(turnId) {
337
+ if (this.#activeTurnId === turnId)
338
+ this.#activeTurnId = undefined;
339
+ }
340
+ static #turnSnapshot(turn) {
341
+ return {
342
+ turnId: turn.turnId,
343
+ state: turn.state,
344
+ ...(turn.commandId !== undefined ? { commandId: turn.commandId } : {}),
345
+ ...(turn.terminal !== undefined ? { terminal: turn.terminal } : {}),
346
+ ...(turn.error !== undefined ? { error: turn.error } : {}),
347
+ ...(turn.retryScheduled !== undefined ? { retryScheduled: turn.retryScheduled } : {}),
348
+ ...(turn.foregroundCompleted !== undefined
349
+ ? { foregroundCompleted: turn.foregroundCompleted }
350
+ : {}),
351
+ };
352
+ }
353
+ // ---- the delivery-plane marker ------------------------------------------
354
+ #deliveryGap(params) {
355
+ // Coalesced on the FIRST `after`: a second overflow while the first hole
356
+ // is still open extends the range rather than replacing it. Replacing it
357
+ // would move the lower bound past events that were never delivered, so a
358
+ // walk from the newer `after` would skip them permanently — a silent hole
359
+ // manufactured by the very state that exists to name one.
360
+ this.#pendingGap = {
361
+ after: this.#pendingGap?.after ?? params.after,
362
+ next: params.next,
363
+ sessionId: params.sessionId,
364
+ };
365
+ // The FRAME's own bounds, not the coalesced ones: this is a report of what
366
+ // arrived. `pendingGap` is the outstanding hole.
367
+ return { after: params.after, kind: "deliveryGap", next: params.next };
368
+ }
369
+ // ---- approvals and user input as fold inputs ----------------------------
370
+ #approvalRequested(params) {
371
+ // A request for an approval that already has its durable terminal (a
372
+ // redelivered or pre-join frame) never re-opens it: no second resolution
373
+ // is coming, so the resurrected entry would pend forever (tdd SS5's
374
+ // first-terminal-wins, request side).
375
+ if (this.#resolvedApprovals.has(params.approvalId)) {
376
+ return { kind: "ignoredStaleFrame", method: "approval/requested", id: params.approvalId };
377
+ }
378
+ // A re-request REPLACES the entry, `latestUpdate` included: a re-issued
379
+ // request already embodies the latest refresh (tdd SS5.6.3), so pairing
380
+ // the new request with the OLD update would show stale stage/choices and
381
+ // a decide against them bounces -32053.
382
+ this.#pendingApprovals.set(params.approvalId, { requested: params });
383
+ return { kind: "approvalPending", approvalId: params.approvalId, requested: params };
384
+ }
385
+ #approvalUpdated(params) {
386
+ // An update REFRESHES a pending view; it never opens one. An update for an
387
+ // approval this fold never saw requested (a pre-join frame) is dropped
388
+ // rather than synthesized into a request it cannot honestly describe —
389
+ // and the outcome says so, rather than claiming a pending entry.
390
+ //
391
+ // A POST-RESOLVE update lands here too, because the resolve deleted the
392
+ // entry. Most are redeliveries, but a FAILED `localPersistent`
393
+ // `policyPersistence` report is a first delivery of a new fact (tdd
394
+ // SS5.5/SS6.8). This fold drops it and holds no accessor for the reason;
395
+ // surfacing is deferred to the facade slice — issue #23379.
396
+ const held = this.#pendingApprovals.get(params.approvalId);
397
+ if (held === undefined) {
398
+ return { kind: "ignoredStaleFrame", method: "approval/updated", id: params.approvalId };
399
+ }
400
+ held.latestUpdate = params;
401
+ return { kind: "approvalPending", approvalId: params.approvalId, requested: held.requested };
402
+ }
403
+ #approvalResolved(params) {
404
+ this.#pendingApprovals.delete(params.approvalId);
405
+ const firstTerminal = !this.#resolvedApprovals.has(params.approvalId);
406
+ // The FIRST durable terminal decision is the one that stands (tdd SS5); a
407
+ // later one is a losing racer's echo, never an overwrite.
408
+ if (firstTerminal)
409
+ this.#resolvedApprovals.set(params.approvalId, params);
410
+ return { kind: "approvalResolved", approvalId: params.approvalId, firstTerminal };
411
+ }
412
+ #userInputRequested(params) {
413
+ // Symmetric with `#approvalRequested`: a settled prompt stays settled.
414
+ if (this.#settledUserInputs.has(params.userInputId)) {
415
+ return { kind: "ignoredStaleFrame", method: "userInput/requested", id: params.userInputId };
416
+ }
417
+ this.#pendingUserInputs.set(params.userInputId, params);
418
+ return { kind: "userInputPending", userInputId: params.userInputId };
419
+ }
420
+ #userInputSettled(params) {
421
+ this.#pendingUserInputs.delete(params.userInputId);
422
+ const firstSettlement = !this.#settledUserInputs.has(params.userInputId);
423
+ if (firstSettlement)
424
+ this.#settledUserInputs.set(params.userInputId, params);
425
+ return { kind: "userInputSettled", userInputId: params.userInputId, firstSettlement };
426
+ }
427
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The session-state half of the SS4 client fold (spec 14990 FR-007, INV-005).
3
+ *
4
+ * State events are NOT items: they carry replace-wholesale session facts,
5
+ * cursor-ordered, latest-wins (tdd SS4.6). An explicit `null` is a fact — it
6
+ * clears the family — never "unchanged" (the `session/goalChanged` and
7
+ * `session/branchChanged` rules). `session/tokenUsage` is the one
8
+ * accumulate-only family; its running totals arrive server-computed in
9
+ * `cumulative`, so the client stores rather than sums (tdd SS4.6.5).
10
+ *
11
+ * Generic for the same reason `ItemStore` is: the concrete params types are
12
+ * enrolled by #14953 and INV-001 forbids restating them locally. The family
13
+ * KEY is the notification method name, which is protocol vocabulary the
14
+ * generated layer already owns as `MspNotification`; this store treats it as
15
+ * an opaque string so a family added additively (tdd SS1.5.4) folds without
16
+ * a code change.
17
+ */
18
+ /** A stored family value. `null` is a real value: the family was cleared. */
19
+ export type StateValue<V> = V | null;
20
+ /**
21
+ * What one state event changed: the family it targeted, the value it
22
+ * replaced, the value now stored, and whether it was applied or refused as
23
+ * a replay.
24
+ */
25
+ export interface StateApplyOutcome<V> {
26
+ readonly family: string;
27
+ readonly previous: StateValue<V> | undefined;
28
+ readonly current: StateValue<V>;
29
+ /** False when the incoming value was refused as an exact-cursor replay. */
30
+ readonly applied: boolean;
31
+ }
32
+ /**
33
+ * Last-write-wins per family, in arrival order.
34
+ *
35
+ * Cursors are opaque strings and MUST NOT be parsed or ordered (tdd SS4.1),
36
+ * so ordering here is by arrival: the server emits view events in cursor
37
+ * order on every connection, and a page's events are ascending. The one
38
+ * cursor use is EXACT-EQUALITY replay de-duplication, and it remembers only
39
+ * the family's LATEST cursor — it refuses only a back-to-back replay of the
40
+ * family's most recent event. The SS4.8 splice caller must skip
41
+ * already-applied page events itself, which it does by construction: the
42
+ * splice pages forward from `after` (the last delivered cursor) and
43
+ * discards paged events at cursors >= `next` as duplicates of the live
44
+ * buffer, so no sanctioned path re-delivers an older event. No relational
45
+ * comparison exists: string order diverges from cursor order at every digit
46
+ * rollover ("v:s:10" < "v:s:9" as strings), which would drop genuinely
47
+ * newer events.
48
+ */
49
+ export declare class SessionStateStore<V = unknown> {
50
+ #private;
51
+ /**
52
+ * Apply a state event. `cursor` is the event's `viewCursor`; when omitted
53
+ * (a snapshot seed, which carries one cursor for the whole state) the value
54
+ * is taken unconditionally.
55
+ */
56
+ apply(family: string, value: StateValue<V>, cursor?: string): StateApplyOutcome<V>;
57
+ /**
58
+ * Read a family. `undefined` means no fact has ever landed — distinct from
59
+ * `null`, which means a fact cleared it. Absent is never fabricated
60
+ * (tdd SS4.9.1).
61
+ */
62
+ get(family: string): StateValue<V> | undefined;
63
+ has(family: string): boolean;
64
+ /** Every family that holds a value, in insertion order. */
65
+ families(): readonly string[];
66
+ /** Seed from a snapshot's state block: authoritative, replaces wholesale. */
67
+ seed(entries: Iterable<readonly [string, StateValue<V>]>, cursor?: string): void;
68
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * The session-state half of the SS4 client fold (spec 14990 FR-007, INV-005).
3
+ *
4
+ * State events are NOT items: they carry replace-wholesale session facts,
5
+ * cursor-ordered, latest-wins (tdd SS4.6). An explicit `null` is a fact — it
6
+ * clears the family — never "unchanged" (the `session/goalChanged` and
7
+ * `session/branchChanged` rules). `session/tokenUsage` is the one
8
+ * accumulate-only family; its running totals arrive server-computed in
9
+ * `cumulative`, so the client stores rather than sums (tdd SS4.6.5).
10
+ *
11
+ * Generic for the same reason `ItemStore` is: the concrete params types are
12
+ * enrolled by #14953 and INV-001 forbids restating them locally. The family
13
+ * KEY is the notification method name, which is protocol vocabulary the
14
+ * generated layer already owns as `MspNotification`; this store treats it as
15
+ * an opaque string so a family added additively (tdd SS1.5.4) folds without
16
+ * a code change.
17
+ */
18
+ /**
19
+ * Last-write-wins per family, in arrival order.
20
+ *
21
+ * Cursors are opaque strings and MUST NOT be parsed or ordered (tdd SS4.1),
22
+ * so ordering here is by arrival: the server emits view events in cursor
23
+ * order on every connection, and a page's events are ascending. The one
24
+ * cursor use is EXACT-EQUALITY replay de-duplication, and it remembers only
25
+ * the family's LATEST cursor — it refuses only a back-to-back replay of the
26
+ * family's most recent event. The SS4.8 splice caller must skip
27
+ * already-applied page events itself, which it does by construction: the
28
+ * splice pages forward from `after` (the last delivered cursor) and
29
+ * discards paged events at cursors >= `next` as duplicates of the live
30
+ * buffer, so no sanctioned path re-delivers an older event. No relational
31
+ * comparison exists: string order diverges from cursor order at every digit
32
+ * rollover ("v:s:10" < "v:s:9" as strings), which would drop genuinely
33
+ * newer events.
34
+ */
35
+ export class SessionStateStore {
36
+ #values = new Map();
37
+ #cursors = new Map();
38
+ /**
39
+ * Apply a state event. `cursor` is the event's `viewCursor`; when omitted
40
+ * (a snapshot seed, which carries one cursor for the whole state) the value
41
+ * is taken unconditionally.
42
+ */
43
+ apply(family, value, cursor) {
44
+ const previous = this.#values.get(family);
45
+ if (cursor !== undefined) {
46
+ const seen = this.#cursors.get(family);
47
+ // An equal cursor is a replay of the same event (idempotent): refuse.
48
+ // That is the ONLY cursor comparison — cursors are opaque (SS4.1) and
49
+ // any relational order (string order included) mis-sorts at a digit
50
+ // rollover; arrival order carries the LWW truth.
51
+ if (seen !== undefined && cursor === seen) {
52
+ return {
53
+ family,
54
+ previous,
55
+ current: previous ?? null,
56
+ applied: false,
57
+ };
58
+ }
59
+ this.#cursors.set(family, cursor);
60
+ }
61
+ this.#values.set(family, value);
62
+ return { family, previous, current: value, applied: true };
63
+ }
64
+ /**
65
+ * Read a family. `undefined` means no fact has ever landed — distinct from
66
+ * `null`, which means a fact cleared it. Absent is never fabricated
67
+ * (tdd SS4.9.1).
68
+ */
69
+ get(family) {
70
+ return this.#values.get(family);
71
+ }
72
+ has(family) {
73
+ return this.#values.has(family);
74
+ }
75
+ /** Every family that holds a value, in insertion order. */
76
+ families() {
77
+ return [...this.#values.keys()];
78
+ }
79
+ /** Seed from a snapshot's state block: authoritative, replaces wholesale. */
80
+ seed(entries, cursor) {
81
+ this.#values.clear();
82
+ this.#cursors.clear();
83
+ for (const [family, value] of entries) {
84
+ this.#values.set(family, value);
85
+ if (cursor !== undefined)
86
+ this.#cursors.set(family, cursor);
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * `@muse-code/sdk` — the MSP TypeScript facade (tdd SS7.1).
4
+ *
5
+ * (The tag above is load-bearing: without it TypeDoc attaches this header to
6
+ * the first re-export, whose generated reference page then publishes this
7
+ * prose instead of the export's own TSDoc.)
8
+ *
9
+ * Slices 1 and 2 (spec `14990-muse-sdk`, plan.md) ship the workspace, the schema
10
+ * fingerprint pin, and the transport-less core: the item/state fold algebra,
11
+ * `SessionFold` binding it to the generated wire types, the complete SS4.13
12
+ * pending-command fold, and the duplex-agnostic NDJSON connection with its
13
+ * typed stdio handshake.
14
+ *
15
+ * Slice 3 ships the SS7.1 facade: `MuseClient` (incl. `spawn`), `Session`,
16
+ * `TurnHandle`'s iterators and INV-014 turn-wait, the SS2.13.3b host-death
17
+ * discharge on BOTH notifications (process exit and transport EOF), the
18
+ * `sendUserTurn` and `approval/decide` submit verbs, the SS4.13 retirement
19
+ * drivers those verbs made reachable, and the SS4.8 gap splice-fill inside the
20
+ * iterators (FR-020).
21
+ *
22
+ * Not here yet, each with its named dependency; the condition and the command
23
+ * that re-derives it live once, in `specs/14990-muse-sdk/tasks.md` under "The
24
+ * slice-3 blocker".
25
+ * - Snapshot ingestion (FR-008) — sequenced behind spec 210 T061's corpus
26
+ * regeneration, not behind a shape.
27
+ * - Corpus replay + checkpoint equality (slice 1c) — runnable now; own PR.
28
+ */
29
+ export { EXPECTED_SCHEMA_FINGERPRINT, checkServedFingerprint, fingerprintMismatchMessage, } from "./fingerprint.js";
30
+ export type { FingerprintWarning } from "./fingerprint.js";
31
+ export { ItemStore } from "./fold/item-store.js";
32
+ export type { DeltaApplyOutcome, ItemApplyOutcome, RevisionedItem, TerminalUnknownItemAnnotation, } from "./fold/item-store.js";
33
+ export { SessionStateStore } from "./fold/state-store.js";
34
+ export type { StateApplyOutcome, StateValue } from "./fold/state-store.js";
35
+ export { SessionFold } from "./fold/session-fold.js";
36
+ export type {
37
+ /**
38
+ * Exported because it is CONSUMER SURFACE, not an internal pin: every
39
+ * getter on `SessionFold` returns `DeepReadonly<…>`, so a consumer naming
40
+ * one of those values needs the type. Without it they are stuck with
41
+ * `ReturnType<>` gymnastics or a deep `dist/` import (#23556 item 4).
42
+ */
43
+ DeepReadonly, FoldItems, FoldOutcome, FoldSessionState, PendingApproval, SessionStateMethod, SessionStateParams, StaleDroppableMethod, TurnEntry, TurnState, ViewEvent, } from "./fold/session-fold.js";
44
+ export { COMMAND_REJECTED_CODE, COMMAND_REJECTED_KIND, PendingCommandSet, } from "./pending/pending-command-set.js";
45
+ export type { CommandErrorResponse, PendingCommandAck, PendingCommandEntry, PendingCommandSetOptions, PendingDisposition, PendingJoinPlan, PendingRetirement, ReplayAnswer, SnapshotJoinFacts, } from "./pending/pending-command-set.js";
46
+ export { Connection, createUuidV7Mint, MspError, ProtocolError, } from "./connection/connection.js";
47
+ export type { CommandOptions, ConnectionOptions, DuplexTransport, NotificationHandler, ProtocolErrorHandler, ServerRequestHandler, } from "./connection/connection.js";
48
+ export { MspHandshake, MuseServeChild, SpawnedMspConnection, spawnMspConnection, } from "./connection/spawn.js";
49
+ export { MuseClient } from "./facade/client.js";
50
+ export type { MuseClientOptions, MuseClientSpawnOptions, ResumeSessionOptions, StartSessionOptions, } from "./facade/client.js";
51
+ export type { ExitClassification, MuseServeChildOptions, ProcessExit, SpawnMspConnectionOptions, } from "./connection/spawn.js";
52
+ export { DiscardedSessions } from "./facade/discarded.js";
53
+ export { MuseForeignSessionError, MuseGapFillError, MuseSessionDiscardedError, PermissionProfileNotApplied, } from "./errors.js";
54
+ export type { GapFillFailureReason } from "./errors.js";
55
+ export type { GapFillFailureHandler } from "./facade/gap-fill.js";
56
+ export { MuseHostDiedError, readSessionDurability } from "./facade/host-death.js";
57
+ export { MuseHostStartError } from "./facade/host-start.js";
58
+ export type { HostStartFailure } from "./facade/host-start.js";
59
+ export type { AbnormalExit, HostDeathDischarge, HostDeathNotification, SessionDurabilityProfile, TransportEof, } from "./facade/host-death.js";
60
+ export { Session } from "./facade/session.js";
61
+ export type { PendingCommandView, SessionApplyOutcome, SessionApplyRefusal, SessionFoldView, SessionGapBuffered, SessionGapOverlap, SessionOpening, SessionOptions, } from "./facade/session.js";
62
+ export type { ApprovalDecisionInput, ApprovalFailure, ApprovalFailureHandler, ApprovalHandler, } from "./facade/approval.js";
63
+ export type { SendUserTurnOptions } from "./facade/turn-submit.js";
64
+ export { isLaunchFailure } from "./facade/turn-handle.js";
65
+ export type { FoldedItem, Turn, TurnOutcome } from "./facade/turn-handle.js";