@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,156 @@
1
+ /**
2
+ * `TurnHandle` — one turn's view of the fold (spec 14990 FR-018, INV-014;
3
+ * tasks.md T030).
4
+ *
5
+ * A handle owns two things: the SS3.1.4 turn-wait, and the async iterators
6
+ * that ride the fold for this turn. It authors no wire traffic and holds no
7
+ * durable state — `Session` feeds it folded events and it fans them out.
8
+ *
9
+ * INV-006 governs the wait: the SDK never locally times out, fails, or
10
+ * completes a turn. Every settlement below traces to a server-authored fact,
11
+ * with one sanctioned exception that is not a terminal at all —
12
+ * terminal-unknown after an ephemeral host death, which SS2.13.3b makes a
13
+ * client MUST ("stop waiting for a terminal") and which stays an annotation
14
+ * rather than a synthesized event.
15
+ */
16
+ import type { Item, ItemDeltaParams, TurnCompletedParams, TurnUnqueuedParams } from "../msp.js";
17
+ import type { DeepReadonly } from "../fold/session-fold.js";
18
+ /**
19
+ * What the iterators yield: the fold's stored item, sealed.
20
+ *
21
+ * `DeepReadonly` because these ARE the objects the fold holds — the generated
22
+ * wire types carry no `readonly`, so an unsealed yield would let a consumer
23
+ * write `item.revision = 0` and defeat the INV-003 guard through the read
24
+ * surface (PR #23087 review round). The seal is compile-time only; nothing is
25
+ * frozen or cloned on the hot path.
26
+ */
27
+ export type FoldedItem = DeepReadonly<Item>;
28
+ /**
29
+ * How a turn ended, from a waiter's point of view.
30
+ *
31
+ * SS3.1.4 names exactly two resolutions of the turn-wait, and the pre-minted
32
+ * turn is why there are two rather than one:
33
+ *
34
+ * - `completed` — the ordinary terminal, AND the launch-failure exit
35
+ * (`"deferred_start_failed"`: terminal `"failed"`, `error.kind:
36
+ * "launchError"`, no preceding `turn/started`). `isLaunchFailure` reads
37
+ * that WIRE marker; `observedStart` does not discriminate them, because a
38
+ * terminal-first fold is legitimate for turns that really ran.
39
+ * - `unqueued` — the reclaim (SS3.6). No `turn/completed` will ever carry
40
+ * this `turnId`, so a wait folding only `turn/completed` hangs forever.
41
+ *
42
+ * `turn/retracted` and `turn/retryScheduled` are deliberately absent: a
43
+ * retracted turn still reaches its own terminal, and a scheduled retry is
44
+ * explicitly non-terminal (SS4.5.1).
45
+ */
46
+ export type TurnOutcome = {
47
+ readonly kind: "completed";
48
+ readonly params: TurnCompletedParams;
49
+ /**
50
+ * Did THIS session observe a `turn/started` for this turn?
51
+ *
52
+ * A session-local observation, NOT a launch-failure marker. The fold
53
+ * deliberately accepts a terminal-first `turn/completed` for turns that
54
+ * really did run — single-shot turns, gap fills, and any consumer that
55
+ * attached mid-stream or replayed a canned transcript from partway
56
+ * through — so `false` here covers those too.
57
+ *
58
+ * The launch failure has its own WIRE marker and that is the one to
59
+ * branch on: `terminal: "failed"` with `error.kind: "launchError"`
60
+ * (tdd SS3.1.4/SS4.5.1). `isLaunchFailure` below reads it.
61
+ */
62
+ readonly observedStart: boolean;
63
+ } | {
64
+ readonly kind: "unqueued";
65
+ readonly params: TurnUnqueuedParams;
66
+ }
67
+ /**
68
+ * SS2.13.3b: an ephemeral host died, so the client stops waiting. Carries no
69
+ * `reason`: the arm has exactly one cause, so a single-valued field would
70
+ * add nothing `kind` does not already say while freezing a permanent
71
+ * obligation into a public type (Constitution XI).
72
+ */
73
+ | {
74
+ readonly kind: "terminalUnknown";
75
+ };
76
+ /**
77
+ * The `"deferred_start_failed"` exit (SS3.1.4/SS3.2): a pre-minted turn whose
78
+ * launch errored at the terminal boundary, so the runtime wrote its terminal
79
+ * directly and no `turn/started` was ever emitted BY THE HOST.
80
+ *
81
+ * Read off the wire, never off local observation: a client that had not been
82
+ * listening when the turn started is not looking at a launch failure.
83
+ */
84
+ export declare function isLaunchFailure(outcome: TurnOutcome): boolean;
85
+ /** The turn's replayable backlog, supplied by `Session` from the fold. */
86
+ export type ItemReplay = () => readonly FoldedItem[];
87
+ /**
88
+ * A turn, as a CONSUMER sees it — the SS7.1 turn surface and nothing else.
89
+ *
90
+ * `Session.turn()` returns this rather than the concrete `TurnHandle` because
91
+ * the handle's "fed by Session" mutators (`settleCompleted`, `settleUnqueued`,
92
+ * `fail`, `pushItem`, …) would otherwise be public: an embedder could call
93
+ * `settleCompleted` and fabricate a terminal the server never authored, which
94
+ * is exactly what INV-006 forbids. They stay reachable only through
95
+ * `Session.apply`, i.e. only from a server-authored event.
96
+ */
97
+ export interface Turn {
98
+ readonly turnId: string;
99
+ /** Did THIS session observe a `turn/started`? Not a launch-failure marker. */
100
+ readonly observedStart: boolean;
101
+ readonly completed: Promise<TurnOutcome>;
102
+ items(): AsyncIterableIterator<FoldedItem>;
103
+ deltas(): AsyncIterableIterator<ItemDeltaParams>;
104
+ }
105
+ export declare class TurnHandle implements Turn {
106
+ #private;
107
+ readonly turnId: string;
108
+ constructor(turnId: string, replayItems: ItemReplay);
109
+ /** Settles on `turn/completed`, `turn/unqueued`, or the ephemeral discard. */
110
+ get completed(): Promise<TurnOutcome>;
111
+ /**
112
+ * How many iterators this handle is still fanning out to.
113
+ *
114
+ * Exposed so the deregistration contract is assertable: an iterator a
115
+ * consumer broke out of, or simply dropped, must leave this set rather than
116
+ * accumulate for the rest of the turn.
117
+ */
118
+ get liveStreamCount(): number;
119
+ /** Did a `turn/started` ever fold for this turn? */
120
+ get observedStart(): boolean;
121
+ /**
122
+ * This turn's items: everything the fold already holds for it, at its
123
+ * current revision and in first-opened order, then the live tail — one yield
124
+ * per fold-observed revision CHANGE. A stale re-emission mutates nothing
125
+ * (INV-003) and therefore yields nothing.
126
+ *
127
+ * The iterator ends when the turn settles; it never ends on a local timeout.
128
+ */
129
+ items(): AsyncIterableIterator<FoldedItem>;
130
+ /**
131
+ * This turn's `item/delta` frames, live only.
132
+ *
133
+ * Deliberately NOT replayed, and the asymmetry with `items()` is a fact
134
+ * about the fold rather than an oversight: the store holds each item at its
135
+ * latest revision plus the ACCUMULATED delta text per field path
136
+ * (`ItemStore.accumulated`), never the delta event sequence. A consumer
137
+ * attaching mid-turn reads the accumulated value from the fold; there is no
138
+ * retained event list to replay, and inventing one would be a second copy of
139
+ * state that INV-002's determinism would then have to defend.
140
+ */
141
+ deltas(): AsyncIterableIterator<ItemDeltaParams>;
142
+ markStarted(): void;
143
+ pushItem(item: FoldedItem): void;
144
+ pushDelta(params: ItemDeltaParams): void;
145
+ settleCompleted(params: TurnCompletedParams): void;
146
+ settleUnqueued(params: TurnUnqueuedParams): void;
147
+ /** SS2.13.3b: an ephemeral host died; stop waiting for a terminal. */
148
+ settleTerminalUnknown(): void;
149
+ /**
150
+ * A DURABLE host died abnormally. The waiter gets no answer here — FM-001
151
+ * puts the terminals on resume — so it is rejected rather than resolved: a
152
+ * resolution would be the invented terminal INV-006 forbids, and silence
153
+ * would be the SS3.1.4 hang.
154
+ */
155
+ fail(error: unknown): void;
156
+ }
@@ -0,0 +1,271 @@
1
+ /**
2
+ * `TurnHandle` — one turn's view of the fold (spec 14990 FR-018, INV-014;
3
+ * tasks.md T030).
4
+ *
5
+ * A handle owns two things: the SS3.1.4 turn-wait, and the async iterators
6
+ * that ride the fold for this turn. It authors no wire traffic and holds no
7
+ * durable state — `Session` feeds it folded events and it fans them out.
8
+ *
9
+ * INV-006 governs the wait: the SDK never locally times out, fails, or
10
+ * completes a turn. Every settlement below traces to a server-authored fact,
11
+ * with one sanctioned exception that is not a terminal at all —
12
+ * terminal-unknown after an ephemeral host death, which SS2.13.3b makes a
13
+ * client MUST ("stop waiting for a terminal") and which stays an annotation
14
+ * rather than a synthesized event.
15
+ */
16
+ /**
17
+ * Bound through the CLOSED extraction of each generated vocabulary. Both
18
+ * `TurnTerminal` and `TurnErrorKind` end in `| (string & {})`, so a bare
19
+ * `"launchEror"` compiles green against them — and this discriminator is
20
+ * barrel-exported, so the typo would ship. Every sibling literal in this
21
+ * package is bound the same way for the same reason.
22
+ */
23
+ const TERMINAL_FAILED = "failed";
24
+ const LAUNCH_ERROR = "launchError";
25
+ /**
26
+ * The `"deferred_start_failed"` exit (SS3.1.4/SS3.2): a pre-minted turn whose
27
+ * launch errored at the terminal boundary, so the runtime wrote its terminal
28
+ * directly and no `turn/started` was ever emitted BY THE HOST.
29
+ *
30
+ * Read off the wire, never off local observation: a client that had not been
31
+ * listening when the turn started is not looking at a launch failure.
32
+ */
33
+ export function isLaunchFailure(outcome) {
34
+ return (outcome.kind === "completed" &&
35
+ outcome.params.terminal === TERMINAL_FAILED &&
36
+ outcome.params.error?.kind === LAUNCH_ERROR);
37
+ }
38
+ /**
39
+ * A single-consumer push stream. Buffers when nobody is awaiting and hands the
40
+ * buffer over before reporting an end or a failure, so a consumer never loses
41
+ * events it was already owed.
42
+ */
43
+ class PushStream {
44
+ #buffer = [];
45
+ #onFinished;
46
+ /**
47
+ * FIFO, not a single slot. `items()`/`deltas()` hand out a plain
48
+ * `AsyncIterableIterator`, so `Promise.all([it.next(), it.next()])` prefetch
49
+ * is a reasonable thing for a consumer to write — and a single slot let the
50
+ * second call overwrite the first waiter, leaving the first promise never
51
+ * settled and handing its value to the second caller. A silently
52
+ * never-settling promise is the hang class this file exists to prevent.
53
+ */
54
+ #waiters = [];
55
+ #ended = false;
56
+ #failure;
57
+ /**
58
+ * `onFinished` deregisters this stream from its owner. Without it a consumer
59
+ * that `break`s out of a `for await` — or simply drops the iterator — stays
60
+ * in the fan-out set for the rest of the turn, so every later event is
61
+ * pushed into a stream nobody reads. A render loop that re-attaches per
62
+ * frame would grow one dead stream per frame.
63
+ */
64
+ constructor(onFinished) {
65
+ this.#onFinished = onFinished;
66
+ }
67
+ push(value) {
68
+ if (this.#ended)
69
+ return;
70
+ const waiter = this.#waiters.shift();
71
+ if (waiter === undefined) {
72
+ this.#buffer.push(value);
73
+ return;
74
+ }
75
+ waiter.resolve({ done: false, value });
76
+ }
77
+ end() {
78
+ if (this.#ended)
79
+ return;
80
+ this.#ended = true;
81
+ // Drain EVERY queued waiter, not just the oldest.
82
+ for (const waiter of this.#waiters.splice(0)) {
83
+ waiter.resolve({ done: true, value: undefined });
84
+ }
85
+ }
86
+ fail(error) {
87
+ if (this.#ended)
88
+ return;
89
+ this.#ended = true;
90
+ this.#failure = error;
91
+ for (const waiter of this.#waiters.splice(0))
92
+ waiter.reject(error);
93
+ }
94
+ next() {
95
+ // Buffered values come out first even after an end or a failure: the
96
+ // consumer was already owed them, and swallowing them here would be the
97
+ // silent hole the iterators exist to prevent.
98
+ if (this.#buffer.length > 0) {
99
+ return Promise.resolve({ done: false, value: this.#buffer.shift() });
100
+ }
101
+ if (this.#failure !== undefined)
102
+ return Promise.reject(this.#failure);
103
+ if (this.#ended)
104
+ return Promise.resolve({ done: true, value: undefined });
105
+ return new Promise((resolve, reject) => {
106
+ this.#waiters.push({ reject, resolve });
107
+ });
108
+ }
109
+ /** `break`/`return` inside a `for await`: drop the backlog and finish. */
110
+ return() {
111
+ this.#buffer.length = 0;
112
+ this.end();
113
+ this.#onFinished();
114
+ return Promise.resolve({ done: true, value: undefined });
115
+ }
116
+ [Symbol.asyncIterator]() {
117
+ return this;
118
+ }
119
+ }
120
+ export class TurnHandle {
121
+ turnId;
122
+ #replayItems;
123
+ #itemStreams = new Set();
124
+ #deltaStreams = new Set();
125
+ #completed;
126
+ #settle;
127
+ #fail;
128
+ #observedStart = false;
129
+ #settled = false;
130
+ /**
131
+ * Retained so an iterator opened AFTER the failure reports it too. Without
132
+ * it a late `items()` saw only `#settled` and ended cleanly — handing the
133
+ * consumer a partial replay plus a tidy `done`, i.e. an unfinished turn
134
+ * rendered as finished, which is the invented completion INV-006 forbids.
135
+ */
136
+ #failure;
137
+ constructor(turnId, replayItems) {
138
+ this.turnId = turnId;
139
+ this.#replayItems = replayItems;
140
+ this.#completed = new Promise((resolve, reject) => {
141
+ this.#settle = resolve;
142
+ this.#fail = reject;
143
+ });
144
+ // A wait nobody awaits must not crash the embedding host on rejection.
145
+ // Later awaiters still receive it (the same posture `MuseServeChild` takes).
146
+ this.#completed.catch(() => { });
147
+ }
148
+ /** Settles on `turn/completed`, `turn/unqueued`, or the ephemeral discard. */
149
+ get completed() {
150
+ return this.#completed;
151
+ }
152
+ /**
153
+ * How many iterators this handle is still fanning out to.
154
+ *
155
+ * Exposed so the deregistration contract is assertable: an iterator a
156
+ * consumer broke out of, or simply dropped, must leave this set rather than
157
+ * accumulate for the rest of the turn.
158
+ */
159
+ get liveStreamCount() {
160
+ return this.#itemStreams.size + this.#deltaStreams.size;
161
+ }
162
+ /** Did a `turn/started` ever fold for this turn? */
163
+ get observedStart() {
164
+ return this.#observedStart;
165
+ }
166
+ /**
167
+ * This turn's items: everything the fold already holds for it, at its
168
+ * current revision and in first-opened order, then the live tail — one yield
169
+ * per fold-observed revision CHANGE. A stale re-emission mutates nothing
170
+ * (INV-003) and therefore yields nothing.
171
+ *
172
+ * The iterator ends when the turn settles; it never ends on a local timeout.
173
+ */
174
+ items() {
175
+ const stream = new PushStream(() => this.#itemStreams.delete(stream));
176
+ for (const item of this.#replayItems())
177
+ stream.push(item);
178
+ this.#admit(stream, this.#itemStreams);
179
+ return stream;
180
+ }
181
+ /**
182
+ * This turn's `item/delta` frames, live only.
183
+ *
184
+ * Deliberately NOT replayed, and the asymmetry with `items()` is a fact
185
+ * about the fold rather than an oversight: the store holds each item at its
186
+ * latest revision plus the ACCUMULATED delta text per field path
187
+ * (`ItemStore.accumulated`), never the delta event sequence. A consumer
188
+ * attaching mid-turn reads the accumulated value from the fold; there is no
189
+ * retained event list to replay, and inventing one would be a second copy of
190
+ * state that INV-002's determinism would then have to defend.
191
+ */
192
+ deltas() {
193
+ const stream = new PushStream(() => this.#deltaStreams.delete(stream));
194
+ this.#admit(stream, this.#deltaStreams);
195
+ return stream;
196
+ }
197
+ /**
198
+ * Register a fresh stream, or finish it the way this turn already finished.
199
+ * The failure branch comes FIRST: a turn that failed is not a turn that
200
+ * ended. `PushStream` drains the replay before reporting either, so a late
201
+ * consumer still receives everything it was owed.
202
+ */
203
+ #admit(stream, into) {
204
+ if (this.#failure !== undefined)
205
+ stream.fail(this.#failure);
206
+ else if (this.#settled)
207
+ stream.end();
208
+ else
209
+ into.add(stream);
210
+ }
211
+ // ---- fed by Session -----------------------------------------------------
212
+ markStarted() {
213
+ this.#observedStart = true;
214
+ }
215
+ pushItem(item) {
216
+ for (const stream of this.#itemStreams)
217
+ stream.push(item);
218
+ }
219
+ pushDelta(params) {
220
+ for (const stream of this.#deltaStreams)
221
+ stream.push(params);
222
+ }
223
+ settleCompleted(params) {
224
+ this.#finish({ kind: "completed", observedStart: this.#observedStart, params });
225
+ }
226
+ settleUnqueued(params) {
227
+ this.#finish({ kind: "unqueued", params });
228
+ }
229
+ /** SS2.13.3b: an ephemeral host died; stop waiting for a terminal. */
230
+ settleTerminalUnknown() {
231
+ this.#finish({ kind: "terminalUnknown" });
232
+ }
233
+ /**
234
+ * A DURABLE host died abnormally. The waiter gets no answer here — FM-001
235
+ * puts the terminals on resume — so it is rejected rather than resolved: a
236
+ * resolution would be the invented terminal INV-006 forbids, and silence
237
+ * would be the SS3.1.4 hang.
238
+ */
239
+ fail(error) {
240
+ if (this.#settled)
241
+ return;
242
+ this.#settled = true;
243
+ this.#failure = error;
244
+ this.#fail?.(error);
245
+ this.#clear();
246
+ for (const stream of this.#itemStreams)
247
+ stream.fail(error);
248
+ for (const stream of this.#deltaStreams)
249
+ stream.fail(error);
250
+ this.#itemStreams.clear();
251
+ this.#deltaStreams.clear();
252
+ }
253
+ #finish(outcome) {
254
+ // First settlement wins: a duplicate terminal is a losing racer's echo.
255
+ if (this.#settled)
256
+ return;
257
+ this.#settled = true;
258
+ this.#settle?.(outcome);
259
+ this.#clear();
260
+ for (const stream of this.#itemStreams)
261
+ stream.end();
262
+ for (const stream of this.#deltaStreams)
263
+ stream.end();
264
+ this.#itemStreams.clear();
265
+ this.#deltaStreams.clear();
266
+ }
267
+ #clear() {
268
+ this.#settle = undefined;
269
+ this.#fail = undefined;
270
+ }
271
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * `turn/start` submission and same-`commandId` replay (spec 14990 FR-018 /
3
+ * T030 obligations (a) and (d); tdd SS3.1.1, SS3.2, SS4.13).
4
+ *
5
+ * Split out of `session.ts` for the same reason `approval.ts` was: this is a
6
+ * wire-shaping and idempotency concern, not a fold concern, and `Session` was
7
+ * already the package's largest module.
8
+ *
9
+ * It holds the ONE piece of state `PendingCommandSet` deliberately cannot: the
10
+ * `turn/start` params per `commandId`. The set stores the COMPOSER input and is
11
+ * wire-blind by design (INV-001), so a replay — which must re-send the SAME
12
+ * logical command, byte for byte, to be the sanctioned retry rather than a
13
+ * double execution — needs the params to live on this side of that boundary.
14
+ */
15
+ import type { Connection } from "../connection/connection.js";
16
+ import type { PendingCommandSet } from "../pending/pending-command-set.js";
17
+ import type { CommandErrorResponse, PendingCommandAck, PendingRetirement, ReplayAnswer } from "../pending/pending-command-set.js";
18
+ import type { TurnStartParams } from "../msp.js";
19
+ /**
20
+ * What `Session.sendUserTurn` sends, COMPOSED from the generated params rather
21
+ * than restated (INV-001).
22
+ *
23
+ * `commandId` is excluded because `Connection` is the single minter (INV-013);
24
+ * `sessionId` because the session already knows its own and a caller-supplied
25
+ * one could only disagree with it.
26
+ */
27
+ export type SendUserTurnOptions<I> = Readonly<Omit<TurnStartParams, "commandId" | "sessionId">> & {
28
+ /**
29
+ * What to hand back to the composer if this submit is ever retired — SS4.13
30
+ * restores the INPUT, not the wire parts. Optional: a consumer that renders
31
+ * nothing optimistically has nothing to restore.
32
+ */
33
+ readonly composerInput?: I;
34
+ };
35
+ /**
36
+ * An `MspError` as the pending set reads it (SS4.13's settlement test).
37
+ *
38
+ * `undefined` for anything that is NOT a server-authored MSP error — a
39
+ * `ProtocolError`, a dead transport — because those admit nothing AND prove
40
+ * nothing about the intake, so the entry must HOLD rather than be judged.
41
+ */
42
+ export declare function commandErrorResponse(error: unknown): CommandErrorResponse | undefined;
43
+ export declare class TurnSubmitter<I> {
44
+ #private;
45
+ constructor(sessionId: string, connection: Connection | undefined, pending: PendingCommandSet<I>);
46
+ get wired(): boolean;
47
+ requireConnection(verb: string): Connection;
48
+ /**
49
+ * Record the optimistic SS4.13 entry, then send `turn/start`.
50
+ *
51
+ * The ORDER is the point. An entry created only on the ack is invisible for
52
+ * exactly the window it exists to cover — between the user pressing enter and
53
+ * the host answering — and `anchorAfterItemId` is fixed here, at submission
54
+ * time, because SS4.13's insertion point may never be relocated by events
55
+ * that fold in afterwards.
56
+ */
57
+ submit(options: SendUserTurnOptions<I>, anchorAfterItemId: string | null): Promise<PendingCommandAck>;
58
+ /**
59
+ * Re-send one remembered `turn/start` under its ORIGINAL `commandId`
60
+ * (SS3.1.1 — always safe, and the only re-send that is not a double
61
+ * execution).
62
+ *
63
+ * `undefined` means "no answer this set can act on": either this session
64
+ * never authored the command (a caller-seeded entry), or the failure was a
65
+ * transport/protocol one, which proves nothing about the intake.
66
+ */
67
+ replay(commandId: string): Promise<ReplayAnswer | undefined>;
68
+ /** Replay one entry and feed the answer back through the live SS4.13 rules. */
69
+ driveReplay(commandId: string, into: PendingRetirement<I>[]): Promise<void>;
70
+ /** Drop the replay memory for retired entries so it tracks the pending set. */
71
+ forgetRetired(retirements: readonly PendingRetirement<I>[]): void;
72
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * `turn/start` submission and same-`commandId` replay (spec 14990 FR-018 /
3
+ * T030 obligations (a) and (d); tdd SS3.1.1, SS3.2, SS4.13).
4
+ *
5
+ * Split out of `session.ts` for the same reason `approval.ts` was: this is a
6
+ * wire-shaping and idempotency concern, not a fold concern, and `Session` was
7
+ * already the package's largest module.
8
+ *
9
+ * It holds the ONE piece of state `PendingCommandSet` deliberately cannot: the
10
+ * `turn/start` params per `commandId`. The set stores the COMPOSER input and is
11
+ * wire-blind by design (INV-001), so a replay — which must re-send the SAME
12
+ * logical command, byte for byte, to be the sanctioned retry rather than a
13
+ * double execution — needs the params to live on this side of that boundary.
14
+ */
15
+ import { MspError } from "../connection/connection.js";
16
+ /**
17
+ * Every member forwarded to the wire. Composition alone does not stop drift —
18
+ * the guard chain in `#params` is hand-listed — so a regenerated member fails
19
+ * the BUILD here until it is forwarded.
20
+ */
21
+ const TURN_START_FORWARDED = [
22
+ "input",
23
+ "displayText",
24
+ "ifBusy",
25
+ "reasoningEffort",
26
+ // No "tokenBudget" (#45454): experimental-gated, and this SDK serves the
27
+ // stable surface only.
28
+ "workspaceRoots",
29
+ // v1 stable member, forwarded like any other.
30
+ "userShell",
31
+ ];
32
+ /**
33
+ * An `MspError` as the pending set reads it (SS4.13's settlement test).
34
+ *
35
+ * `undefined` for anything that is NOT a server-authored MSP error — a
36
+ * `ProtocolError`, a dead transport — because those admit nothing AND prove
37
+ * nothing about the intake, so the entry must HOLD rather than be judged.
38
+ */
39
+ export function commandErrorResponse(error) {
40
+ if (!(error instanceof MspError))
41
+ return undefined;
42
+ const reason = error.data["reason"];
43
+ return {
44
+ code: error.code,
45
+ kind: error.kind,
46
+ ...(typeof reason === "string" ? { reason } : {}),
47
+ };
48
+ }
49
+ export class TurnSubmitter {
50
+ #sessionId;
51
+ #connection;
52
+ #pending;
53
+ /** See the module doc. Pruned on retirement, so it tracks the pending set. */
54
+ #memory = new Map();
55
+ constructor(sessionId, connection, pending) {
56
+ this.#sessionId = sessionId;
57
+ this.#connection = connection;
58
+ this.#pending = pending;
59
+ }
60
+ get wired() {
61
+ return this.#connection !== undefined;
62
+ }
63
+ requireConnection(verb) {
64
+ const connection = this.#connection;
65
+ if (connection === undefined) {
66
+ // A plain `Error`: calling a submit verb on a fold-only Session is API
67
+ // misuse, not the foreign-frame STATE `MuseForeignSessionError` names.
68
+ throw new Error(`${verb} needs a connection; this Session was constructed fold-only`);
69
+ }
70
+ return connection;
71
+ }
72
+ /**
73
+ * Record the optimistic SS4.13 entry, then send `turn/start`.
74
+ *
75
+ * The ORDER is the point. An entry created only on the ack is invisible for
76
+ * exactly the window it exists to cover — between the user pressing enter and
77
+ * the host answering — and `anchorAfterItemId` is fixed here, at submission
78
+ * time, because SS4.13's insertion point may never be relocated by events
79
+ * that fold in afterwards.
80
+ */
81
+ async submit(options, anchorAfterItemId) {
82
+ const connection = this.requireConnection("sendUserTurn");
83
+ // Delegated, not re-implemented: the connection owns the single mint, so
84
+ // INV-013's single-minter property survives this verb needing the id early.
85
+ const commandId = connection.mintCommandId();
86
+ const params = this.#params(commandId, options);
87
+ this.#pending.submitted({
88
+ anchorAfterItemId,
89
+ commandId,
90
+ input: options.composerInput,
91
+ ...(params.displayText === undefined ? {} : { displayText: params.displayText }),
92
+ });
93
+ this.#memory.set(commandId, params);
94
+ try {
95
+ const ack = await this.#send(connection, params, commandId);
96
+ this.#pending.acked(commandId, ack);
97
+ return ack;
98
+ }
99
+ catch (error) {
100
+ const response = commandErrorResponse(error);
101
+ // Only a durable -32030 settles; every other error admitted nothing and
102
+ // the entry HOLDS for the caller's same-commandId retry (SS4.13).
103
+ if (response !== undefined) {
104
+ const settled = this.#pending.ackErrored(commandId, response);
105
+ if (settled !== "held")
106
+ this.forgetRetired([settled]);
107
+ }
108
+ // Rethrown, deliberately: the caller who submitted still holds its own
109
+ // input, so this path needs no second retirement channel.
110
+ throw error;
111
+ }
112
+ }
113
+ /**
114
+ * Re-send one remembered `turn/start` under its ORIGINAL `commandId`
115
+ * (SS3.1.1 — always safe, and the only re-send that is not a double
116
+ * execution).
117
+ *
118
+ * `undefined` means "no answer this set can act on": either this session
119
+ * never authored the command (a caller-seeded entry), or the failure was a
120
+ * transport/protocol one, which proves nothing about the intake.
121
+ */
122
+ async replay(commandId) {
123
+ const params = this.#memory.get(commandId);
124
+ if (params === undefined)
125
+ return undefined;
126
+ const connection = this.requireConnection("turn/start replay");
127
+ try {
128
+ return { ack: await this.#send(connection, params, commandId), kind: "ack" };
129
+ }
130
+ catch (error) {
131
+ const response = commandErrorResponse(error);
132
+ if (response === undefined)
133
+ return undefined;
134
+ return { error: response, kind: "error" };
135
+ }
136
+ }
137
+ /** Replay one entry and feed the answer back through the live SS4.13 rules. */
138
+ async driveReplay(commandId, into) {
139
+ const answer = await this.replay(commandId);
140
+ if (answer === undefined)
141
+ return;
142
+ const settled = this.#pending.replayAnswered(commandId, answer);
143
+ if (settled !== "held")
144
+ into.push(settled);
145
+ }
146
+ /** Drop the replay memory for retired entries so it tracks the pending set. */
147
+ forgetRetired(retirements) {
148
+ for (const retirement of retirements)
149
+ this.#memory.delete(retirement.commandId);
150
+ }
151
+ async #send(connection, params, commandId) {
152
+ const raw = await connection.command("turn/start", params, { commandId });
153
+ const result = raw;
154
+ // `turnId` comes from the ACK and never from the `commandId`: SS3.2 makes
155
+ // the ack authoritative, and a queued submit's turn may already exist.
156
+ return { disposition: result.disposition, turnId: result.turnId };
157
+ }
158
+ /**
159
+ * OPTIONAL MEANS OMITTED, NEVER `null` (tdd SS1.2), and the guards are LOOSE
160
+ * (`!= null`) for the same reason `MuseClient`'s are: the types bind only
161
+ * strict TypeScript consumers, and a plain-JS caller reaching the built
162
+ * `dist` can pass an explicit `null` a `!== undefined` guard would forward.
163
+ */
164
+ #params(commandId, options) {
165
+ const params = {
166
+ commandId,
167
+ input: options.input,
168
+ sessionId: this.#sessionId,
169
+ };
170
+ if (options.displayText != null)
171
+ params.displayText = options.displayText;
172
+ if (options.ifBusy != null)
173
+ params.ifBusy = options.ifBusy;
174
+ if (options.reasoningEffort != null)
175
+ params.reasoningEffort = options.reasoningEffort;
176
+ // No tokenBudget forward (#45454): experimental-gated, stable SDK omits it.
177
+ if (options.workspaceRoots != null)
178
+ params.workspaceRoots = options.workspaceRoots;
179
+ if (options.userShell != null)
180
+ params.userShell = options.userShell;
181
+ return params;
182
+ }
183
+ }