@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.
- package/README.md +43 -17
- package/bin/muse-mcp-bridge.cjs +3 -0
- package/dist/adapters/index.cjs +6 -1
- package/dist/adapters/muse.cjs +433 -0
- package/dist/agent-auth.cjs +2 -1
- package/dist/config.cjs +8 -1
- package/dist/conversation/capability.cjs +1 -1
- package/dist/exit-conditions.cjs +1 -1
- package/dist/muse-mcp-bridge.cjs +167 -0
- package/dist/session.cjs +30 -8
- package/node_modules/@muse-code/sdk/LICENSE +21 -0
- package/node_modules/@muse-code/sdk/README.md +88 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
- package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
- package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
- package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
- package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
- package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
- package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
- package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
- package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
- package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
- package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
- package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
- package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
- package/node_modules/@muse-code/sdk/package.json +47 -0
- 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";
|