@sagentlab/navarch-runtime 0.1.116 → 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 (51) hide show
  1. package/README.md +43 -17
  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/config.cjs +8 -1
  7. package/dist/conversation/capability.cjs +1 -1
  8. package/dist/exit-conditions.cjs +1 -1
  9. package/dist/muse-mcp-bridge.cjs +167 -0
  10. package/dist/session.cjs +30 -8
  11. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  12. package/node_modules/@muse-code/sdk/README.md +88 -0
  13. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  14. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  15. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  16. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  17. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  18. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  19. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  20. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  21. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  22. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  23. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  37. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  38. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  39. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  40. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  41. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  45. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  46. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  47. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  48. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  49. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  50. package/node_modules/@muse-code/sdk/package.json +47 -0
  51. package/package.json +8 -4
@@ -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 class DiscardedSessions {
32
+ /** `commandId`s an ephemeral discharge retired. Never replayed again. */
33
+ commandIds = new Set();
34
+ /** `sessionId`s whose ephemeral host died. Never reattached. */
35
+ sessionIds = new Set();
36
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * SS4.8 gap recovery by splice-fill (spec 14990 FR-020 / T032, tdd SS4.8).
3
+ *
4
+ * Split out of `session.ts` for the same reason `approval.ts` and
5
+ * `turn-submit.ts` were: this is a delivery-plane concern with its own buffer,
6
+ * page walk, and failure vocabulary, and `Session` was already the package's
7
+ * largest module.
8
+ *
9
+ * THE RECIPE, verbatim from tdd SS4.8 and unchanged here: buffer live events
10
+ * at cursors at or after `next` as they arrive; page forward from `after` with
11
+ * `view/page` until the walk reaches `next`; discard the paged events the
12
+ * buffer already holds; splice the buffer after the paged prefix. D-030's
13
+ * second sanctioned path (drop state and re-anchor at the latest compaction
14
+ * snapshot) is NOT built here — it is the #208 lane's spec work, and building
15
+ * both would be a second concrete path for one current use.
16
+ *
17
+ * CURSORS STAY OPAQUE (tdd SS4.1). Nothing below orders two cursors: "at or
18
+ * after `next`" is delivery ORDER, not a comparison — every live frame that
19
+ * arrives after the gap marker is, by the server's own delivery contract, at
20
+ * or after `next`. The walk stops on cursor EQUALITY with the target or on the
21
+ * server's own end-of-view, and the overlap is discarded by set membership.
22
+ */
23
+ import { MuseGapFillError } from "../errors.js";
24
+ import type { Connection } from "../connection/connection.js";
25
+ import type { SessionFold } from "../fold/session-fold.js";
26
+ import type { PendingRetirement } from "../pending/pending-command-set.js";
27
+ /** One wire frame, exactly as `Session.apply`'s wide overload accepts it. */
28
+ export interface WireFrame {
29
+ readonly method: string;
30
+ readonly params?: unknown;
31
+ }
32
+ /**
33
+ * Where a drained frame's side effects go. The filler re-feeds frames through
34
+ * `Session`'s own fold-and-route path, so the retirements and the client→server
35
+ * I/O they trigger belong to `Session`, not here.
36
+ */
37
+ export interface DrainSink<I> {
38
+ readonly retirements: PendingRetirement<I>[];
39
+ readonly tasks: Promise<readonly PendingRetirement<I>[]>[];
40
+ }
41
+ export interface GapFillerOptions<I> {
42
+ readonly sessionId: string;
43
+ /** Absent on a fold-only `Session`: there is nothing to page through. */
44
+ readonly connection: Connection | undefined;
45
+ readonly fold: SessionFold;
46
+ /** Fold and route ONE frame, collecting what it produced. */
47
+ readonly apply: (frame: WireFrame, into: DrainSink<I>) => void;
48
+ /** SS2.13.3b: a discharged session folds nothing, fill or no fill. */
49
+ readonly discarded: () => boolean;
50
+ }
51
+ /** Reported, never thrown — see {@link MuseGapFillError}. */
52
+ export type GapFillFailureHandler = (failure: MuseGapFillError) => void;
53
+ export declare class GapFiller<I> {
54
+ #private;
55
+ constructor(options: GapFillerOptions<I>);
56
+ /** While true, every live frame but the gap marker itself is held. */
57
+ get filling(): boolean;
58
+ /**
59
+ * Was this frame already applied from a page, as part of the overlap the
60
+ * splice discards? Consuming: a twin is refused exactly once.
61
+ *
62
+ * Returns the matched CURSOR rather than a boolean so the caller can report
63
+ * which event it refused without re-reading (and re-defaulting) a field this
64
+ * method already proved is there.
65
+ */
66
+ claimServedTwin(frame: WireFrame): string | undefined;
67
+ /** Observe fills that did not complete. See {@link MuseGapFillError}. */
68
+ onError(handler: GapFillFailureHandler): void;
69
+ /** Hold one live frame until the paged prefix is in. */
70
+ hold(frame: WireFrame): void;
71
+ /**
72
+ * Recover the fold's outstanding hole.
73
+ *
74
+ * Called on every `view/gap`, including one that lands mid-fill: the running
75
+ * walk reads its target from `fold.pendingGap` on each pass, so a coalesced
76
+ * hole extends the walk in flight rather than starting a second one racing
77
+ * it for the same cursor stream.
78
+ */
79
+ start(): Promise<readonly PendingRetirement<I>[]>;
80
+ }
@@ -0,0 +1,388 @@
1
+ /**
2
+ * SS4.8 gap recovery by splice-fill (spec 14990 FR-020 / T032, tdd SS4.8).
3
+ *
4
+ * Split out of `session.ts` for the same reason `approval.ts` and
5
+ * `turn-submit.ts` were: this is a delivery-plane concern with its own buffer,
6
+ * page walk, and failure vocabulary, and `Session` was already the package's
7
+ * largest module.
8
+ *
9
+ * THE RECIPE, verbatim from tdd SS4.8 and unchanged here: buffer live events
10
+ * at cursors at or after `next` as they arrive; page forward from `after` with
11
+ * `view/page` until the walk reaches `next`; discard the paged events the
12
+ * buffer already holds; splice the buffer after the paged prefix. D-030's
13
+ * second sanctioned path (drop state and re-anchor at the latest compaction
14
+ * snapshot) is NOT built here — it is the #208 lane's spec work, and building
15
+ * both would be a second concrete path for one current use.
16
+ *
17
+ * CURSORS STAY OPAQUE (tdd SS4.1). Nothing below orders two cursors: "at or
18
+ * after `next`" is delivery ORDER, not a comparison — every live frame that
19
+ * arrives after the gap marker is, by the server's own delivery contract, at
20
+ * or after `next`. The walk stops on cursor EQUALITY with the target or on the
21
+ * server's own end-of-view, and the overlap is discarded by set membership.
22
+ */
23
+ var _a;
24
+ import { MuseForeignSessionError, MuseGapFillError } from "../errors.js";
25
+ /**
26
+ * How many events one `view/page` asks for.
27
+ *
28
+ * Any value in the published 1–1000 bound (`ViewPageParams.limit`) is correct
29
+ * — the server MAY serve fewer and the walk follows `nextCursor` either way —
30
+ * so this trades round trips against frame size and is deliberately not a
31
+ * knob: a per-session page size would be a public option with no current
32
+ * consumer asking for one (Constitution XI).
33
+ */
34
+ const PAGE_LIMIT = 200;
35
+ /** The shared already-settled result for a fill that produced no retirement. */
36
+ const NO_RETIREMENTS = Promise.resolve([]);
37
+ export class GapFiller {
38
+ #options;
39
+ /** Live frames held while a fill is in flight, in arrival order. */
40
+ #buffer = [];
41
+ #filling = false;
42
+ #cursor = "";
43
+ #onFailure;
44
+ constructor(options) {
45
+ this.#options = options;
46
+ }
47
+ /**
48
+ * Cursors the page served at or after a target — the events whose LIVE twin
49
+ * the wire has yet to deliver.
50
+ *
51
+ * THE LATE HALF of the recipe's overlap discard, and only that half (PR
52
+ * #24930 review rounds 1 and 2). A twin that has already arrived is in the
53
+ * buffer, and `#drain` discards those against its own local set of EVERY
54
+ * paged cursor. This set covers the other window: the wire promises no order
55
+ * between the marker and the frame at `next`, so a twin can legally arrive
56
+ * after the fill completed, when the buffer is gone. Folding it then re-runs
57
+ * its routing — for a `turn/started`, a duplicate SS4.13 queue-movement
58
+ * replay on the wire.
59
+ *
60
+ * SELF-DRAINING: an entry is removed the moment its twin arrives, and only
61
+ * cursors at or after a target ever enter (the walk stops at the page that
62
+ * carries it, so at most one page's worth per target).
63
+ *
64
+ * RESIDUAL, stated rather than hidden: an entry whose twin the wire never
65
+ * delivers — because a LATER hole swallowed it — has no arrival to retire it
66
+ * and stays, a short string each, bounded by one page's worth per TARGET —
67
+ * and a coalesced fill has several, since each extension's reaching page adds
68
+ * its own batch.
69
+ *
70
+ * Two prunes were tried and both were defects, so neither is coming back
71
+ * without a proof neither had. Clearing per fill (round 2) drops the refusal
72
+ * for a twin that is still on its way. Retiring inside the WALK when it meets
73
+ * the cursor before its own target (round 3) looks like page-order proof that
74
+ * the twin was swallowed, but the walk cannot see the BUFFER — a coalescing
75
+ * gap moves the target past a cursor whose twin is sitting in it, and the
76
+ * entry is deleted out from under the drain (round 4). Deciding staleness any
77
+ * other way means ordering two opaque cursors, which tdd SS4.1 forbids.
78
+ */
79
+ #servedTwins = new Set();
80
+ /** While true, every live frame but the gap marker itself is held. */
81
+ get filling() {
82
+ return this.#filling;
83
+ }
84
+ /**
85
+ * Was this frame already applied from a page, as part of the overlap the
86
+ * splice discards? Consuming: a twin is refused exactly once.
87
+ *
88
+ * Returns the matched CURSOR rather than a boolean so the caller can report
89
+ * which event it refused without re-reading (and re-defaulting) a field this
90
+ * method already proved is there.
91
+ */
92
+ claimServedTwin(frame) {
93
+ const cursor = _a.#viewCursorOf(frame);
94
+ if (cursor === undefined || !this.#servedTwins.has(cursor))
95
+ return undefined;
96
+ this.#servedTwins.delete(cursor);
97
+ return cursor;
98
+ }
99
+ /** Observe fills that did not complete. See {@link MuseGapFillError}. */
100
+ onError(handler) {
101
+ this.#onFailure = handler;
102
+ }
103
+ /** Hold one live frame until the paged prefix is in. */
104
+ hold(frame) {
105
+ this.#buffer.push(frame);
106
+ }
107
+ /**
108
+ * Recover the fold's outstanding hole.
109
+ *
110
+ * Called on every `view/gap`, including one that lands mid-fill: the running
111
+ * walk reads its target from `fold.pendingGap` on each pass, so a coalesced
112
+ * hole extends the walk in flight rather than starting a second one racing
113
+ * it for the same cursor stream.
114
+ */
115
+ start() {
116
+ const gap = this.#options.fold.pendingGap;
117
+ // Only reachable if a caller invokes this with nothing outstanding; the
118
+ // one production caller folds the marker first, which always sets it.
119
+ if (gap === undefined)
120
+ return NO_RETIREMENTS;
121
+ if (this.#filling)
122
+ return NO_RETIREMENTS;
123
+ const connection = this.#options.connection;
124
+ if (connection === undefined) {
125
+ // Nothing is buffered on this path: no fill is coming to release it, and
126
+ // holding the live tail forever would turn one reported hole into a
127
+ // silent second one.
128
+ this.#report(new MuseGapFillError("noConnection", gap.after, gap.next));
129
+ return NO_RETIREMENTS;
130
+ }
131
+ this.#filling = true;
132
+ this.#cursor = gap.after;
133
+ // DELIBERATELY NOT CLEARED (PR #24930 review round 2). An earlier draft
134
+ // cleared here, reasoning that a surviving entry's twin must lie inside the
135
+ // hole this gap names and so would never arrive. That conflated "not
136
+ // delivered yet" with "inside the new hole": a cursor the previous fill
137
+ // served can sit at or AFTER this gap's `next`, in which case its live twin
138
+ // is still coming and clearing loses the only refusal that stops it folding
139
+ // twice. Entries are one-shot and matched by cursor equality, and a cursor
140
+ // names a unique position (tdd SS4.1), so a retained entry can only ever
141
+ // refuse its own true twin.
142
+ return this.#run(connection);
143
+ }
144
+ async #run(connection) {
145
+ const paged = [];
146
+ let filled = false;
147
+ try {
148
+ await this.#walk(connection, paged);
149
+ filled = true;
150
+ }
151
+ catch (error) {
152
+ this.#reportFailure(error);
153
+ }
154
+ // ALWAYS drained, success or failure: the buffered tail is made of events
155
+ // this client really did receive, and swallowing them would add a second
156
+ // hole to the one just reported.
157
+ const drained = this.#drain(paged);
158
+ if (!filled)
159
+ return drained;
160
+ // THE CLEAR WINDOW (PR #24930 review round 1, P0). `#walk` clears the hole
161
+ // and returns, but `#drain` — which lowers `#filling` — runs a microtask
162
+ // later. A `view/gap` folding in between skips the buffer (markers must, or
163
+ // they could not extend a live walk), finds `#filling` still true, and
164
+ // `start()` returns early: no second `view/page`, no failure report, and
165
+ // `pendingGap` set forever. That is the silent hole FR-020 forbids, and it
166
+ // also costs the consumer its D-030 re-anchor cue.
167
+ //
168
+ // Re-checked HERE, in the same synchronous run that lowered the flag, so no
169
+ // frame can land inside the check. Success only: a fill that FAILED keeps
170
+ // its stated no-retry posture, and its hole is already reported.
171
+ if (this.#options.discarded())
172
+ return drained;
173
+ if (this.#options.fold.pendingGap === undefined)
174
+ return drained;
175
+ const restarted = this.start();
176
+ return Promise.all([drained, restarted]).then(([first, second]) => [...first, ...second]);
177
+ }
178
+ /**
179
+ * Page until the fold's outstanding hole is closed.
180
+ *
181
+ * The outer loop is what makes a mid-walk `view/gap` safe: `gapFilled`
182
+ * refuses to clear a target the fold has since extended, so the walk simply
183
+ * keeps going toward the newer `next` on the SAME cursor stream.
184
+ */
185
+ async #walk(connection, paged) {
186
+ for (;;) {
187
+ // A session discharged mid-walk stops here, BEFORE `gapFilled`: there is
188
+ // nothing left to be current with, and clearing the hole would have the
189
+ // fold report a discarded transcript complete. The host is gone, so a
190
+ // further page would also wait on an answer that is never coming — which
191
+ // is a hang, not a fill.
192
+ if (this.#options.discarded())
193
+ return;
194
+ const gap = this.#options.fold.pendingGap;
195
+ if (gap === undefined)
196
+ return;
197
+ const target = gap.next;
198
+ await this.#walkTo(connection, target, gap.after, paged);
199
+ if (this.#options.discarded())
200
+ return;
201
+ if (this.#options.fold.gapFilled(target))
202
+ return;
203
+ }
204
+ }
205
+ async #walkTo(connection, target, after, paged) {
206
+ for (;;) {
207
+ if (this.#options.discarded())
208
+ return;
209
+ const result = await this.#page(connection, this.#cursor);
210
+ let reached = false;
211
+ // The last cursor this page served, skipped events included — where a
212
+ // walk toward an extended target resumes after an end-of-view page.
213
+ let lastServed;
214
+ for (const event of result.events) {
215
+ this.#requireOwnSession(event, after, target);
216
+ const cursor = event.params.viewCursor;
217
+ lastServed = cursor;
218
+ // An EARLIER fill already served — and applied — this durable event.
219
+ // A later walk may legally page through the same range, and re-applying
220
+ // it here is the same double fold the discard exists to prevent (PR
221
+ // #24930 review round 3).
222
+ const alreadyApplied = this.#servedTwins.has(cursor);
223
+ // Equality, never a relational compare: `next` is a cursor, and
224
+ // cursors are opaque (tdd SS4.1). Set BEFORE the skip below, or a
225
+ // target the walk skips is a target the walk never reaches, and it
226
+ // pages straight past its own stopping point.
227
+ if (cursor === target)
228
+ reached = true;
229
+ if (alreadyApplied) {
230
+ // SKIPPED AND NOTHING ELSE (PR #24930 review round 4). An earlier
231
+ // draft also retired the entry here when the walk met it before its
232
+ // target, reasoning that the twin must have been swallowed. The walk
233
+ // cannot see the BUFFER, so that reasoning missed the case where the
234
+ // twin is sitting in it: a coalescing gap moves the target past the
235
+ // cursor, the walk meets it early, and the entry is deleted out from
236
+ // under the drain, which then folds the buffered twin a second time.
237
+ // Leaving the entry alone is what keeps the drain able to refuse it.
238
+ continue;
239
+ }
240
+ paged.push(event);
241
+ // At or after `next` — by page ORDER, not by comparing two cursors.
242
+ // These are the events the live tail also delivers, so their twins are
243
+ // the overlap the splice discards.
244
+ if (reached)
245
+ this.#servedTwins.add(cursor);
246
+ }
247
+ // The server's own end of the view. `next` names a LIVE cursor and
248
+ // `view/page` serves durable-sourced events only (tdd SS4.7.3), so a
249
+ // hole whose tail was ephemeral ends here and never at `target`.
250
+ //
251
+ // The cursor still advances to the last event served (#50252, the Rust
252
+ // SDK's INV-002 rule from #49869): if a `view/gap` extended the target
253
+ // while this page was in flight, `#walk` comes back here toward the new
254
+ // one, and re-asking from where this walk started would re-serve — and
255
+ // fold a second time — every event this page just delivered.
256
+ if (result.nextCursor === null) {
257
+ if (lastServed !== undefined)
258
+ this.#cursor = lastServed;
259
+ return;
260
+ }
261
+ // PROGRESS is the only bound on this walk, and it has to be, because a
262
+ // legitimate fill is unbounded in length: an attempt cap would silently
263
+ // truncate one. A page that carries no event, repeats the cursor it was
264
+ // given, or answers with no cursor at all has not advanced, and paging
265
+ // again would ask the identical question forever.
266
+ if (result.events.length === 0 ||
267
+ typeof result.nextCursor !== "string" ||
268
+ result.nextCursor === this.#cursor) {
269
+ throw new MuseGapFillError("pageStalled", after, target);
270
+ }
271
+ this.#cursor = result.nextCursor;
272
+ if (reached)
273
+ return;
274
+ }
275
+ }
276
+ async #page(connection, cursor) {
277
+ const params = {
278
+ cursor,
279
+ limit: PAGE_LIMIT,
280
+ sessionId: this.#options.sessionId,
281
+ };
282
+ const raw = await connection.request("view/page", params);
283
+ return raw;
284
+ }
285
+ /**
286
+ * A page that serves another session's events aborts the fill.
287
+ *
288
+ * The same check `Session.apply` makes on a live frame, at the one other
289
+ * place a frame can enter the fold. Folding it would corrupt this
290
+ * transcript with another session's history, and the request that asked for
291
+ * it named this `sessionId`, so there is no reading under which the answer
292
+ * is right.
293
+ */
294
+ #requireOwnSession(event, after, target) {
295
+ const named = event.params?.sessionId;
296
+ if (typeof named === "string" && named !== this.#options.sessionId) {
297
+ throw new MuseGapFillError("pageFailed", after, target, new MuseForeignSessionError(`view/page for ${this.#options.sessionId} served an event for session ${named}`));
298
+ }
299
+ }
300
+ /**
301
+ * Splice: the paged prefix, then the buffered tail minus the overlap.
302
+ *
303
+ * Synchronous up to its return, deliberately. Clearing the buffer and the
304
+ * flag in the same run of code that re-feeds them is what makes the handover
305
+ * atomic — an `await` in the middle would let a live frame arrive after the
306
+ * flag dropped and fold ahead of the tail still waiting in the buffer.
307
+ *
308
+ * It is outside the walk's `try` because the buffered tail must be released
309
+ * on FAILURE too, and it needs no `try` of its own: every frame it re-feeds
310
+ * has already passed the one check `Session.apply` can throw on — the live
311
+ * half at buffer time, the paged half in the walk — so this cannot be the
312
+ * rejection `SessionApplyOutcome.io` promises never to be.
313
+ */
314
+ #drain(paged) {
315
+ const buffered = this.#buffer.splice(0);
316
+ this.#filling = false;
317
+ // SS2.13.3b outranks the fill: a session discharged while the walk was in
318
+ // flight folds nothing more, and neither half of the splice is exempt.
319
+ if (this.#options.discarded())
320
+ return NO_RETIREMENTS;
321
+ const sink = { retirements: [], tasks: [] };
322
+ // EVERY paged cursor, not just the ones at or after a target (PR #24930
323
+ // review round 2, P0). A coalesced gap restarts the walk toward the NEW
324
+ // target with `reached` false, so paged events between the two targets
325
+ // never enter `#servedTwins` — yet their twins ARE in this buffer, because
326
+ // a cursor at or after the first target was delivered live before the
327
+ // second hole opened. Claiming on the persistent set alone folded those
328
+ // twice.
329
+ const served = new Set();
330
+ for (const frame of paged) {
331
+ const cursor = _a.#viewCursorOf(frame);
332
+ if (cursor !== undefined)
333
+ served.add(cursor);
334
+ this.#options.apply(frame, sink);
335
+ }
336
+ for (const frame of buffered) {
337
+ // The overlap the recipe discards: the page already served this cursor,
338
+ // so folding the buffered copy would route the same event twice.
339
+ const cursor = _a.#viewCursorOf(frame);
340
+ // BOTH sets (PR #24930 review round 3). `served` is what THIS fill
341
+ // applied; `#servedTwins` carries what EARLIER fills served, including
342
+ // the cursors this walk declined to re-apply — nothing deletes those
343
+ // inside the walk, precisely so this branch can still find them. A twin
344
+ // of either can be sitting in this buffer, because `Session.apply`
345
+ // buffers every live frame while a fill runs, before the late-window
346
+ // claim can see it, so this branch is the only thing left to refuse it.
347
+ if (cursor !== undefined && (served.has(cursor) || this.#servedTwins.has(cursor))) {
348
+ // Consume the persistent entry too, so a twin refused here is not
349
+ // refused a second time if the wire also delivers it later.
350
+ this.#servedTwins.delete(cursor);
351
+ continue;
352
+ }
353
+ this.#options.apply(frame, sink);
354
+ }
355
+ if (sink.tasks.length === 0)
356
+ return Promise.resolve(sink.retirements);
357
+ return Promise.all(sink.tasks).then((batches) => [...sink.retirements, ...batches.flat()]);
358
+ }
359
+ static #viewCursorOf(frame) {
360
+ const cursor = frame.params?.viewCursor;
361
+ return typeof cursor === "string" ? cursor : undefined;
362
+ }
363
+ #reportFailure(error) {
364
+ if (error instanceof MuseGapFillError) {
365
+ this.#report(error);
366
+ return;
367
+ }
368
+ // Anything the round trip itself threw — an `MspError` the host authored,
369
+ // a `ProtocolError`, a dead transport — is the same fact from a consumer's
370
+ // point of view: the hole is still there. The cause is carried rather than
371
+ // flattened so the repair stays diagnosable.
372
+ const gap = this.#options.fold.pendingGap;
373
+ this.#report(new MuseGapFillError("pageFailed", gap?.after ?? "", gap?.next ?? "", error));
374
+ }
375
+ #report(failure) {
376
+ try {
377
+ this.#onFailure?.(failure);
378
+ }
379
+ catch {
380
+ // The consumer's failure OBSERVER threw. Swallowed for the reason
381
+ // `ApprovalRouter.#report` states: this rides
382
+ // `SessionApplyOutcome.io`, whose contract is "IT NEVER REJECTS", and
383
+ // most consumers never await it — so a rejection here would surface as
384
+ // an unhandled rejection and kill the embedder.
385
+ }
386
+ }
387
+ }
388
+ _a = GapFiller;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The ephemeral-profile host-death discard obligation, client side
3
+ * (spec 14990 FM-002 / Scenario 4.3, tasks.md T030b; tdd SS2.13, SS4.4.3).
4
+ *
5
+ * Slice 2b landed the two STORE-LEVEL primitives with no production callers:
6
+ * `PendingCommandSet.discardEphemeral()` and
7
+ * `ItemStore.markEphemeralHostDeath()`. This module holds the two facts the
8
+ * stores deliberately do not know — which durability profile the handshake
9
+ * declared, and whether a given process exit was abnormal — and `Session`
10
+ * composes them into the discharge.
11
+ *
12
+ * The stores stay wire-shape-blind (INV-001): the `Item` probe lives here,
13
+ * where it is checked against the GENERATED type.
14
+ */
15
+ import type { InitializeResult, Item } from "../msp.js";
16
+ import type { ExitClassification } from "../connection/spawn.js";
17
+ import type { TerminalUnknownItemAnnotation } from "../fold/item-store.js";
18
+ import type { PendingRetirement } from "../pending/pending-command-set.js";
19
+ /**
20
+ * Which durability profile the host declared at the handshake (SS2.13.1).
21
+ *
22
+ * Absent and declared-`"durable"` collapse to ONE reading on purpose: nothing
23
+ * downstream needs to tell them apart, and a `source` discriminator would be a
24
+ * field carried for a future that has not arrived. Adding it later is
25
+ * additive; removing it from a barrel-exported type would not be.
26
+ */
27
+ export type SessionDurabilityProfile = {
28
+ readonly kind: "durable";
29
+ } | {
30
+ readonly kind: "ephemeral";
31
+ }
32
+ /** An open-enum value this SDK predates. Guarantees nothing (SS2.13.1). */
33
+ | {
34
+ readonly kind: "unrecognized";
35
+ readonly value: string;
36
+ };
37
+ /**
38
+ * Read `sessionDurability` off the handshake result (SS2.13.1).
39
+ *
40
+ * Two readings are easy to conflate and must not be:
41
+ *
42
+ * - ABSENT is `"durable"`, and that is decidable rather than fabricated: the
43
+ * member is optional only so the addition was additive (SS1.5.4), and no
44
+ * server that omits it has the ephemeral profile.
45
+ * - An UNRECOGNIZED value is NOT durable. SS2.13.1: a client that does not
46
+ * know the value "MUST NOT infer any durability guarantee from it, and MUST
47
+ * NOT fall through to the absent-means-durable rule — that rule keys on the
48
+ * member being missing, not on its value being unfamiliar." The enum is open
49
+ * for a reason (#14401's degraded state is a candidate third value), so the
50
+ * conservative read is "assume nothing survives this host".
51
+ */
52
+ export declare function readSessionDurability(result: InitializeResult): SessionDurabilityProfile;
53
+ /** Does a session on this profile survive its host? Only a durable one does. */
54
+ export declare function survivesHostDeath(profile: SessionDurabilityProfile): boolean;
55
+ /**
56
+ * The transport reached EOF with no orderly SS2.1.2 close (T030 obligation b).
57
+ *
58
+ * SS2.13.3b names TWO notifications of a host death — "process exit **or
59
+ * transport EOF**" — and only the exit half was implementable while nothing in
60
+ * this SDK owned the close. A host that closes stdout while still hung emits no
61
+ * exit on the timescale that matters, so without this arm its session never
62
+ * discharged: every pending command sat in the set as the durable-looking echo
63
+ * SS4.13 forbids, and every turn wait hung.
64
+ *
65
+ * It carries no evidence fields, and that is the honest shape rather than a
66
+ * gap: EOF IS the whole observation. The stderr tail and the SS2.11 row belong
67
+ * to the process boundary, which has not reported yet — and when it does,
68
+ * `Session.hostExited` latches the FIRST discharge, so a later exit replays the
69
+ * EOF's report rather than overwriting it with a richer one. Whoever owns the
70
+ * process still has `MuseServeChild.stderrTail` and its `exit` promise.
71
+ *
72
+ * ORDERLINESS IS NOT DECIDABLE HERE. `Connection` is duplex-generic and a
73
+ * transport cannot tell a peer's hang-up from its own shutdown, so the layer
74
+ * that KNOWS whether the close was its own is the one that builds this:
75
+ * `MuseClient`, which owns both `close()` and the connection.
76
+ */
77
+ export interface TransportEof {
78
+ readonly kind: "transportEof";
79
+ }
80
+ /** How a client learns its host is gone: the process exit, or transport EOF. */
81
+ export type HostDeathNotification = ExitClassification | TransportEof;
82
+ /**
83
+ * Was this notification an abnormal death (SS2.13.3b)?
84
+ *
85
+ * Exit 0 is the ONLY SS2.11 row where the run was cancelled, the drain
86
+ * completed, and the durable `SessionEnd` records were written. Every other
87
+ * row — including the ones with a tidy-looking name like `configError` —
88
+ * left no `SessionEnd`, which is exactly what "abnormal" means here. A
89
+ * transport EOF the client did not cause left no `SessionEnd` either, so it
90
+ * joins the same predicate rather than getting a parallel one that could drift.
91
+ */
92
+ export declare function isAbnormalHostDeath(notification: HostDeathNotification): notification is AbnormalExit;
93
+ /**
94
+ * The in-progress probe the store asks its caller for. `Item.status` is an
95
+ * open enum whose terminal rule is "anything other than `inProgress`", so this
96
+ * is a positive test on the one non-terminal value rather than a negative test
97
+ * against a list that a schema advance would silently widen.
98
+ */
99
+ export declare function isItemInProgress(item: Item): boolean;
100
+ /**
101
+ * Every death notification except the orderly exit row. An orderly close is
102
+ * not a death, and neither is the EOF that follows one — which is why
103
+ * `TransportEof` is only ever constructed by the layer that can tell.
104
+ */
105
+ export type AbnormalExit = Exclude<HostDeathNotification, {
106
+ kind: "cleanShutdown";
107
+ }>;
108
+ /** A durable host died abnormally: its terminals arrive on resume (FM-001). */
109
+ export declare class MuseHostDiedError extends Error {
110
+ #private;
111
+ readonly exit: AbnormalExit;
112
+ constructor(exit: AbnormalExit);
113
+ }
114
+ /**
115
+ * What one `Session.hostExited` call did.
116
+ *
117
+ * Discriminated because the three outcomes leave the session in three
118
+ * different states, and a bare `discharged: boolean` collapsed the two false
119
+ * arms into one: after `notADeath` the session is still usable, after
120
+ * `durableDeath` it is dead until resume with every live wait already
121
+ * rejected. An embedder should not have to re-derive that from the profile
122
+ * and the exit row the method already read.
123
+ */
124
+ export type HostDeathDischarge<I> = {
125
+ readonly kind: "notADeath";
126
+ readonly profile: SessionDurabilityProfile;
127
+ readonly exit: HostDeathNotification;
128
+ } | {
129
+ readonly kind: "durableDeath";
130
+ readonly profile: SessionDurabilityProfile;
131
+ readonly exit: AbnormalExit;
132
+ } | {
133
+ readonly kind: "discharged";
134
+ readonly profile: SessionDurabilityProfile;
135
+ readonly exit: AbnormalExit;
136
+ /** Items still `inProgress` at death, annotated — never a synthesized event. */
137
+ readonly terminalUnknownItems: readonly TerminalUnknownItemAnnotation[];
138
+ readonly retiredCommands: readonly PendingRetirement<I>[];
139
+ };