@sagentlab/navarch-runtime 0.1.112 → 0.1.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -18
- 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/cli.cjs +4 -1
- package/dist/config.cjs +9 -1
- package/dist/conversation/capability.cjs +1 -1
- package/dist/conversation/loop.cjs +16 -0
- package/dist/conversation/mirror.cjs +225 -0
- package/dist/conversation/workspace.cjs +11 -4
- package/dist/exit-conditions.cjs +1 -1
- package/dist/muse-mcp-bridge.cjs +167 -0
- package/dist/session.cjs +30 -8
- package/dist/worktree-guard.cjs +137 -3
- 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,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
|
+
}
|