@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.
Files changed (56) hide show
  1. package/README.md +56 -18
  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/cli.cjs +4 -1
  7. package/dist/config.cjs +9 -1
  8. package/dist/conversation/capability.cjs +1 -1
  9. package/dist/conversation/loop.cjs +16 -0
  10. package/dist/conversation/mirror.cjs +225 -0
  11. package/dist/conversation/workspace.cjs +11 -4
  12. package/dist/exit-conditions.cjs +1 -1
  13. package/dist/muse-mcp-bridge.cjs +167 -0
  14. package/dist/session.cjs +30 -8
  15. package/dist/worktree-guard.cjs +137 -3
  16. package/node_modules/@muse-code/sdk/LICENSE +21 -0
  17. package/node_modules/@muse-code/sdk/README.md +88 -0
  18. package/node_modules/@muse-code/sdk/dist/src/connection/connection.d.ts +149 -0
  19. package/node_modules/@muse-code/sdk/dist/src/connection/connection.js +581 -0
  20. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.d.ts +188 -0
  21. package/node_modules/@muse-code/sdk/dist/src/connection/spawn.js +496 -0
  22. package/node_modules/@muse-code/sdk/dist/src/errors.d.ts +71 -0
  23. package/node_modules/@muse-code/sdk/dist/src/errors.js +76 -0
  24. package/node_modules/@muse-code/sdk/dist/src/facade/approval.d.ts +96 -0
  25. package/node_modules/@muse-code/sdk/dist/src/facade/approval.js +293 -0
  26. package/node_modules/@muse-code/sdk/dist/src/facade/client.d.ts +171 -0
  27. package/node_modules/@muse-code/sdk/dist/src/facade/client.js +442 -0
  28. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.d.ts +36 -0
  29. package/node_modules/@muse-code/sdk/dist/src/facade/discarded.js +36 -0
  30. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.d.ts +80 -0
  31. package/node_modules/@muse-code/sdk/dist/src/facade/gap-fill.js +388 -0
  32. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.d.ts +139 -0
  33. package/node_modules/@muse-code/sdk/dist/src/facade/host-death.js +96 -0
  34. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.d.ts +62 -0
  35. package/node_modules/@muse-code/sdk/dist/src/facade/host-start.js +81 -0
  36. package/node_modules/@muse-code/sdk/dist/src/facade/session.d.ts +309 -0
  37. package/node_modules/@muse-code/sdk/dist/src/facade/session.js +654 -0
  38. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.d.ts +156 -0
  39. package/node_modules/@muse-code/sdk/dist/src/facade/turn-handle.js +271 -0
  40. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.d.ts +72 -0
  41. package/node_modules/@muse-code/sdk/dist/src/facade/turn-submit.js +183 -0
  42. package/node_modules/@muse-code/sdk/dist/src/fingerprint.d.ts +37 -0
  43. package/node_modules/@muse-code/sdk/dist/src/fingerprint.js +54 -0
  44. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.d.ts +110 -0
  45. package/node_modules/@muse-code/sdk/dist/src/fold/item-store.js +169 -0
  46. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.d.ts +536 -0
  47. package/node_modules/@muse-code/sdk/dist/src/fold/session-fold.js +427 -0
  48. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.d.ts +68 -0
  49. package/node_modules/@muse-code/sdk/dist/src/fold/state-store.js +89 -0
  50. package/node_modules/@muse-code/sdk/dist/src/index.d.ts +65 -0
  51. package/node_modules/@muse-code/sdk/dist/src/index.js +57 -0
  52. package/node_modules/@muse-code/sdk/dist/src/msp.d.ts +3380 -0
  53. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.d.ts +361 -0
  54. package/node_modules/@muse-code/sdk/dist/src/pending/pending-command-set.js +468 -0
  55. package/node_modules/@muse-code/sdk/package.json +47 -0
  56. package/package.json +8 -4
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Typed errors shared by the stores and the facade.
3
+ *
4
+ * They live here rather than in `facade/` because the stores throw them too,
5
+ * and a store importing from the facade would invert the layering. Typed
6
+ * rather than bare `Error` because each names a STATE an embedder must branch
7
+ * on; the only alternative is matching message text, which silently turns
8
+ * those strings into the package's contract.
9
+ */
10
+ /**
11
+ * The session was discarded after an ephemeral host's abnormal death, and the
12
+ * operation attempted would either resurrect it or replay against a new host
13
+ * (tdd SS2.13.3b, SS3.1.3).
14
+ */
15
+ export declare class MuseSessionDiscardedError extends Error {
16
+ constructor(message: string);
17
+ }
18
+ /** An event whose `sessionId` names a different session than the one folding it. */
19
+ export declare class MuseForeignSessionError extends Error {
20
+ constructor(message: string);
21
+ }
22
+ /**
23
+ * Why one SS4.8 splice-fill did not complete. Each arm names a different owner
24
+ * (this SDK's own composition, the transport, the host), because the repair
25
+ * differs:
26
+ *
27
+ * - `noConnection` — the `Session` was built fold-only, so it has no seam to
28
+ * page through. Composition, not a runtime fault.
29
+ * - `pageFailed` — a `view/page` round trip failed, or answered with a frame
30
+ * this session may not fold. `cause` carries it.
31
+ * - `pageStalled` — the walk made no progress: a page that neither carried
32
+ * events nor ended the view, or a `nextCursor` that did not advance. The
33
+ * wire is an external boundary and this is the only bound on the walk;
34
+ * an attempt cap would silently truncate a legitimate long fill.
35
+ */
36
+ export type GapFillFailureReason = "noConnection" | "pageFailed" | "pageStalled";
37
+ /**
38
+ * A gap the client could not fill (spec 14990 FR-020, tdd SS4.8).
39
+ *
40
+ * REPORTED to `Session.onGapError`, never thrown: the fill is driven from a
41
+ * notification, so a throw would escape into the consumer's pump with nothing
42
+ * to catch it. Typed rather than a bare `Error` because the hole it names is a
43
+ * STATE a consumer must branch on — the fold stays not-current until a later
44
+ * recovery, and `(after, next)` names exactly what is missing. Reporting it is
45
+ * what makes the hole loud instead of silent, which is the half of FR-020 the
46
+ * fill itself cannot deliver.
47
+ */
48
+ export declare class MuseGapFillError extends Error {
49
+ readonly reason: GapFillFailureReason;
50
+ /** The gap's own bounds, verbatim and opaque (tdd SS4.1). */
51
+ readonly after: string;
52
+ readonly next: string;
53
+ constructor(reason: GapFillFailureReason, after: string, next: string, cause?: unknown);
54
+ }
55
+ /**
56
+ * A required session-start permission-profile selection the client could not
57
+ * prove applied (TEST-42624-10, amendment 42624 § Compatibility): the start
58
+ * result carried no `permissionState`, and the `session/read` read-back
59
+ * served a different profile — or none at all. Never a silent default: the
60
+ * session stays open and the caller decides (set/retry or close) via
61
+ * `sessionId`, with no auto-close and no auto-set.
62
+ */
63
+ export declare class PermissionProfileNotApplied extends Error {
64
+ /** The profile id the caller required. */
65
+ readonly requested: string;
66
+ /** The committed profile id the read-back served, or null when unknown. */
67
+ readonly served: string | null;
68
+ /** The open session, so the caller can set, retry, or close it. */
69
+ readonly sessionId: string;
70
+ constructor(requested: string, served: string | null, sessionId: string);
71
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Typed errors shared by the stores and the facade.
3
+ *
4
+ * They live here rather than in `facade/` because the stores throw them too,
5
+ * and a store importing from the facade would invert the layering. Typed
6
+ * rather than bare `Error` because each names a STATE an embedder must branch
7
+ * on; the only alternative is matching message text, which silently turns
8
+ * those strings into the package's contract.
9
+ */
10
+ /**
11
+ * The session was discarded after an ephemeral host's abnormal death, and the
12
+ * operation attempted would either resurrect it or replay against a new host
13
+ * (tdd SS2.13.3b, SS3.1.3).
14
+ */
15
+ export class MuseSessionDiscardedError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = "MuseSessionDiscardedError";
19
+ }
20
+ }
21
+ /** An event whose `sessionId` names a different session than the one folding it. */
22
+ export class MuseForeignSessionError extends Error {
23
+ constructor(message) {
24
+ super(message);
25
+ this.name = "MuseForeignSessionError";
26
+ }
27
+ }
28
+ /**
29
+ * A gap the client could not fill (spec 14990 FR-020, tdd SS4.8).
30
+ *
31
+ * REPORTED to `Session.onGapError`, never thrown: the fill is driven from a
32
+ * notification, so a throw would escape into the consumer's pump with nothing
33
+ * to catch it. Typed rather than a bare `Error` because the hole it names is a
34
+ * STATE a consumer must branch on — the fold stays not-current until a later
35
+ * recovery, and `(after, next)` names exactly what is missing. Reporting it is
36
+ * what makes the hole loud instead of silent, which is the half of FR-020 the
37
+ * fill itself cannot deliver.
38
+ */
39
+ export class MuseGapFillError extends Error {
40
+ reason;
41
+ /** The gap's own bounds, verbatim and opaque (tdd SS4.1). */
42
+ after;
43
+ next;
44
+ constructor(reason, after, next, cause) {
45
+ super(`view/gap (${after}, ${next}) could not be filled: ${reason}`, { cause });
46
+ this.name = "MuseGapFillError";
47
+ this.reason = reason;
48
+ this.after = after;
49
+ this.next = next;
50
+ }
51
+ }
52
+ /**
53
+ * A required session-start permission-profile selection the client could not
54
+ * prove applied (TEST-42624-10, amendment 42624 § Compatibility): the start
55
+ * result carried no `permissionState`, and the `session/read` read-back
56
+ * served a different profile — or none at all. Never a silent default: the
57
+ * session stays open and the caller decides (set/retry or close) via
58
+ * `sessionId`, with no auto-close and no auto-set.
59
+ */
60
+ export class PermissionProfileNotApplied extends Error {
61
+ /** The profile id the caller required. */
62
+ requested;
63
+ /** The committed profile id the read-back served, or null when unknown. */
64
+ served;
65
+ /** The open session, so the caller can set, retry, or close it. */
66
+ sessionId;
67
+ constructor(requested, served, sessionId) {
68
+ super(`required permissionProfile ${JSON.stringify(requested)} was not applied:` +
69
+ ` the committed selection is ${served === null ? "unknown" : JSON.stringify(served)}` +
70
+ ` (session ${sessionId})`);
71
+ this.name = "PermissionProfileNotApplied";
72
+ this.requested = requested;
73
+ this.served = served;
74
+ this.sessionId = sessionId;
75
+ }
76
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The FR-019 approval round trip: inbound `approval/requested` → the consumer's
3
+ * handler → outbound `approval/decide` (spec 14990 T031, tdd SS5.4).
4
+ *
5
+ * Split out of `session.ts` rather than added to it: the router owns a handler,
6
+ * a per-stage latch, and a decision-shaping guard chain that have nothing to do
7
+ * with folding a view, and `Session` was already the package's largest module.
8
+ *
9
+ * SHAPE NOTE, established from the bundle rather than assumed. `approval/request`
10
+ * as a SERVER REQUEST is not enrolled — the schema carries `approval/requested`
11
+ * as a NOTIFICATION only — so the round trip is notification-in /
12
+ * command-out, which is exactly the pair FR-019 names. The server-request form
13
+ * would be a #206 enrollment request (INV-001), never a local interface.
14
+ */
15
+ import type { Connection } from "../connection/connection.js";
16
+ import type { DeepReadonly } from "../fold/session-fold.js";
17
+ import type { ApprovalDecideParams, ApprovalRequestParams, ApprovalUpdatedParams } from "../msp.js";
18
+ /**
19
+ * What an approval handler answers with: a choice the SERVER offered, and
20
+ * optional feedback. Composed from the generated decide params, so "the client
21
+ * picks a `choiceId`" stays one fact rather than two that can drift.
22
+ *
23
+ * D-006 is select-never-create, and this TYPE cannot enforce it — `choiceId` is
24
+ * a `string` on the wire — so the router checks the answer against the
25
+ * request's own `availableChoices` before the frame is built.
26
+ */
27
+ export type ApprovalDecisionInput = Readonly<Pick<ApprovalDecideParams, "choiceId" | "feedback">>;
28
+ /** FR-019: the consumer's decision callback. May be sync or async. */
29
+ export type ApprovalHandler = (request: ApprovalRequestParams) => ApprovalDecisionInput | Promise<ApprovalDecisionInput>;
30
+ /**
31
+ * Why one approval round trip did not complete.
32
+ *
33
+ * REPORTED, not thrown: the trip is driven from a notification, so a throw
34
+ * would escape into the consumer's pump with nothing to catch it — the same
35
+ * hazard `Session.apply` already refuses a malformed frame for. Each arm names
36
+ * a DIFFERENT owner (the consumer's handler, the consumer's choice, the host),
37
+ * because the repair differs.
38
+ */
39
+ export type ApprovalFailure = {
40
+ readonly kind: "unofferedChoice";
41
+ readonly approvalId: string;
42
+ readonly choiceId: string;
43
+ readonly availableChoiceIds: readonly string[];
44
+ } | {
45
+ readonly kind: "handlerThrew";
46
+ readonly approvalId: string;
47
+ readonly error: unknown;
48
+ } | {
49
+ readonly kind: "submitFailed";
50
+ readonly approvalId: string;
51
+ readonly error: unknown;
52
+ };
53
+ /** Receives each `ApprovalFailure`; the failure's `kind` names what went wrong and whose repair it is. */
54
+ export type ApprovalFailureHandler = (failure: ApprovalFailure) => void;
55
+ export declare class ApprovalRouter {
56
+ #private;
57
+ constructor(sessionId: string, connection: Connection | undefined);
58
+ /**
59
+ * Register the decision callback (FR-019).
60
+ *
61
+ * REPLACES any previous handler rather than adding to a list: two handlers
62
+ * would race to decide one approval and only one decision can win, so the
63
+ * loser's would be silently discarded.
64
+ */
65
+ onApproval(handler: ApprovalHandler): void;
66
+ /** Observe round trips that did not complete. See {@link ApprovalFailure}. */
67
+ onApprovalError(handler: ApprovalFailureHandler): void;
68
+ /**
69
+ * An `approval/requested` folded. Returns the decision round trip to await,
70
+ * or `undefined` when this client authors nothing for it.
71
+ *
72
+ * No handler at all is the supported default posture, NOT a degraded one
73
+ * (D-008): the client runs under the server's own default-deny, and this SDK
74
+ * parks nothing — no queued decision, no synthesized denial, no local hold.
75
+ */
76
+ requested(params: ApprovalRequestParams): Promise<void> | undefined;
77
+ /**
78
+ * An `approval/updated` folded onto a STILL-PENDING approval (#36949).
79
+ *
80
+ * A multi-stage approval advances `currentRequirementId` through this frame
81
+ * ALONE on the notification plane — the SS5.6 item-3 re-issue on stage
82
+ * advance is the protected `approval/request` SERVER-REQUEST dual, which is
83
+ * not enrolled here (SHAPE NOTE above) — so waiting for a re-issued
84
+ * `approval/requested` notification leaves the new stage undecided and the
85
+ * turn pending forever.
86
+ *
87
+ * The update alone carries none of the request-shape members the handler is
88
+ * typed on, but the fold retains the original request and every identity
89
+ * member is stable across the stages of ONE approval, so the merged request
90
+ * below is honest (`REFRESH_FROM_*` above pins the split). Feeding it
91
+ * through {@link requested} reuses the per-stage latch, so an update whose
92
+ * requirement was already decided — and any later re-issued request for the
93
+ * stage this update decided — authors nothing.
94
+ */
95
+ updated(requested: DeepReadonly<ApprovalRequestParams>, params: ApprovalUpdatedParams): Promise<void> | undefined;
96
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * The FR-019 approval round trip: inbound `approval/requested` → the consumer's
3
+ * handler → outbound `approval/decide` (spec 14990 T031, tdd SS5.4).
4
+ *
5
+ * Split out of `session.ts` rather than added to it: the router owns a handler,
6
+ * a per-stage latch, and a decision-shaping guard chain that have nothing to do
7
+ * with folding a view, and `Session` was already the package's largest module.
8
+ *
9
+ * SHAPE NOTE, established from the bundle rather than assumed. `approval/request`
10
+ * as a SERVER REQUEST is not enrolled — the schema carries `approval/requested`
11
+ * as a NOTIFICATION only — so the round trip is notification-in /
12
+ * command-out, which is exactly the pair FR-019 names. The server-request form
13
+ * would be a #206 enrollment request (INV-001), never a local interface.
14
+ */
15
+ /**
16
+ * Every `DecideParams` member the guard chain below forwards. Composition alone
17
+ * does not stop drift — the chain is hand-listed — so a regenerated member
18
+ * fails the BUILD here until it is forwarded, exactly as `MuseClient`'s two
19
+ * chains do.
20
+ */
21
+ const DECIDE_FORWARDED = [
22
+ "approvalId",
23
+ "choiceId",
24
+ "requirementId",
25
+ "sessionId",
26
+ "feedback",
27
+ ];
28
+ /**
29
+ * The #36949 refreshed-request merge, hand-listed by SOURCE. Stage-scoped
30
+ * members come from the `approval/updated` frame in hand; identity members
31
+ * come from the fold's retained request (stable across the stages of one
32
+ * approval). The `AssertNever` makes a regenerated member — REQUIRED or
33
+ * OPTIONAL — a build error until someone says which side of the merge it
34
+ * comes from: an optional addition (e.g. `subagentOrigin`, #36182) would
35
+ * otherwise just vanish from the refreshed request, `tsc`-green.
36
+ */
37
+ const REFRESH_FROM_UPDATE = [
38
+ "availableChoices",
39
+ "currentRequirementId",
40
+ "sessionId",
41
+ "sourceRange",
42
+ "subagentOrigin",
43
+ "subject",
44
+ "viewCursor",
45
+ ];
46
+ const REFRESH_FROM_REQUEST = [
47
+ "approvalId",
48
+ "itemId",
49
+ "judgeEscalated",
50
+ "protectedWrite",
51
+ "rawArgs",
52
+ "taskId",
53
+ "toolCallId",
54
+ "toolName",
55
+ "turnId",
56
+ ];
57
+ export class ApprovalRouter {
58
+ #sessionId;
59
+ #connection;
60
+ #handler;
61
+ #onFailure;
62
+ /**
63
+ * Approval STAGES already decided, keyed by `approvalId` + `requirementId`.
64
+ *
65
+ * Not `approvalId` alone: `requirementId` is SS5.4's multi-stage race guard,
66
+ * so a stage-2 request must get its own decision or the approval pends
67
+ * forever. Not per-request either: a redelivered `approval/requested` for the
68
+ * SAME stage must author no second decide, or a two-stage approval races
69
+ * itself. The key is kept on FAILURE too — a genuinely advancing stage brings
70
+ * a new `requirementId` and therefore a new key, so "ask the consumer once
71
+ * per stage" holds absolutely.
72
+ */
73
+ #decidedStages = new Set();
74
+ /**
75
+ * Approvals whose `approval/decide` ACK came back `terminal: true`: the host
76
+ * closed the whole approval on that decision (a rule that also satisfied the
77
+ * remaining stages), so no later stage can need this consumer. Kept beside
78
+ * the stage latch because the runtime records only the RESOLVED stage
79
+ * (#37538), letting the view fold emit one trailing `stageResolved` update
80
+ * with a frontier the host already closed, right before `approval/resolved`.
81
+ */
82
+ #terminalApprovals = new Set();
83
+ /**
84
+ * The most recently sent decide each approval has on the wire, settled when
85
+ * its ack lands or the decide fails. A trailing `stageResolved` update read
86
+ * in the SAME chunk as a `terminal: true` ack is routed before
87
+ * {@link #decide} resumes from that ack, so {@link updated} waits for it
88
+ * instead of re-asking. Scoped to the round trip: an open handler or a
89
+ * decide that never started blocks nobody.
90
+ */
91
+ #inFlight = new Map();
92
+ constructor(sessionId, connection) {
93
+ this.#sessionId = sessionId;
94
+ this.#connection = connection;
95
+ }
96
+ /**
97
+ * Register the decision callback (FR-019).
98
+ *
99
+ * REPLACES any previous handler rather than adding to a list: two handlers
100
+ * would race to decide one approval and only one decision can win, so the
101
+ * loser's would be silently discarded.
102
+ */
103
+ onApproval(handler) {
104
+ this.#handler = handler;
105
+ }
106
+ /** Observe round trips that did not complete. See {@link ApprovalFailure}. */
107
+ onApprovalError(handler) {
108
+ this.#onFailure = handler;
109
+ }
110
+ /**
111
+ * An `approval/requested` folded. Returns the decision round trip to await,
112
+ * or `undefined` when this client authors nothing for it.
113
+ *
114
+ * No handler at all is the supported default posture, NOT a degraded one
115
+ * (D-008): the client runs under the server's own default-deny, and this SDK
116
+ * parks nothing — no queued decision, no synthesized denial, no local hold.
117
+ */
118
+ requested(params) {
119
+ const handler = this.#handler;
120
+ if (handler === undefined)
121
+ return undefined;
122
+ const stage = ApprovalRouter.#stageKey(params);
123
+ if (this.#decidedStages.has(stage))
124
+ return undefined;
125
+ // Latched BEFORE the first await: a redelivery folded on the very next
126
+ // frame must find the stage already claimed, and an `await` boundary here
127
+ // would let both pass the check.
128
+ this.#decidedStages.add(stage);
129
+ return this.#decide(params, handler);
130
+ }
131
+ /**
132
+ * An `approval/updated` folded onto a STILL-PENDING approval (#36949).
133
+ *
134
+ * A multi-stage approval advances `currentRequirementId` through this frame
135
+ * ALONE on the notification plane — the SS5.6 item-3 re-issue on stage
136
+ * advance is the protected `approval/request` SERVER-REQUEST dual, which is
137
+ * not enrolled here (SHAPE NOTE above) — so waiting for a re-issued
138
+ * `approval/requested` notification leaves the new stage undecided and the
139
+ * turn pending forever.
140
+ *
141
+ * The update alone carries none of the request-shape members the handler is
142
+ * typed on, but the fold retains the original request and every identity
143
+ * member is stable across the stages of ONE approval, so the merged request
144
+ * below is honest (`REFRESH_FROM_*` above pins the split). Feeding it
145
+ * through {@link requested} reuses the per-stage latch, so an update whose
146
+ * requirement was already decided — and any later re-issued request for the
147
+ * stage this update decided — authors nothing.
148
+ */
149
+ updated(requested, params) {
150
+ // `alreadyTerminal` says the host holds a durable terminal whose resolve
151
+ // frame has not landed yet; a decision against it can only bounce -32051,
152
+ // so asking the consumer for one would be a pointless question.
153
+ if (params.change.kind === "alreadyTerminal")
154
+ return undefined;
155
+ // Same fact, learned from OUR OWN decide ack: `terminal: true` means the
156
+ // host closed the whole approval on that decision, and the one trailing
157
+ // `stageResolved` update the fold can still emit (#37538) names a frontier
158
+ // that no longer exists — re-asking would prompt the consumer for a stage
159
+ // the host already closed, and the decide could only bounce -32051.
160
+ if (this.#terminalApprovals.has(requested.approvalId))
161
+ return undefined;
162
+ const refreshed = {
163
+ approvalId: requested.approvalId,
164
+ availableChoices: params.availableChoices,
165
+ currentRequirementId: params.currentRequirementId,
166
+ itemId: requested.itemId,
167
+ judgeEscalated: requested.judgeEscalated,
168
+ protectedWrite: requested.protectedWrite,
169
+ rawArgs: requested.rawArgs,
170
+ sessionId: params.sessionId,
171
+ sourceRange: params.sourceRange,
172
+ subject: params.subject,
173
+ taskId: requested.taskId,
174
+ toolCallId: requested.toolCallId,
175
+ toolName: requested.toolName,
176
+ turnId: requested.turnId,
177
+ viewCursor: params.viewCursor,
178
+ };
179
+ // Omitted rather than nulled, like `feedback` above: absent on the
180
+ // parent's own approvals by contract (#36182 `skip_serializing_if`).
181
+ if (params.subagentOrigin !== undefined)
182
+ refreshed.subagentOrigin = params.subagentOrigin;
183
+ const acked = this.#inFlight.get(requested.approvalId);
184
+ if (acked !== undefined)
185
+ return this.#afterAck(acked, refreshed);
186
+ return this.requested(refreshed);
187
+ }
188
+ /** {@link updated}'s same-chunk arm: decide only once our own ack is in. */
189
+ async #afterAck(acked, refreshed) {
190
+ await acked;
191
+ if (this.#terminalApprovals.has(refreshed.approvalId))
192
+ return;
193
+ await this.requested(refreshed);
194
+ }
195
+ static #stageKey(params) {
196
+ const requirement = params.currentRequirementId;
197
+ return `${params.approvalId} ${requirement.approvalId}:${String(requirement.sourceIndex)}`;
198
+ }
199
+ async #decide(params, handler) {
200
+ const connection = this.#connection;
201
+ // Checked BEFORE the handler runs: asking a consumer to decide and then
202
+ // dropping the answer is worse than not asking, and on a fold-only session
203
+ // that is all this could do.
204
+ if (connection === undefined) {
205
+ // A plain `Error`: constructing fold-only and then registering a decide
206
+ // handler is API misuse, not the foreign-frame STATE
207
+ // `MuseForeignSessionError` names — a typed class here would send an
208
+ // embedder's routing-bug branch chasing a session mismatch that never
209
+ // happened.
210
+ this.#report({
211
+ approvalId: params.approvalId,
212
+ error: new Error("approval/decide needs a connection; this Session was constructed fold-only"),
213
+ kind: "submitFailed",
214
+ });
215
+ return;
216
+ }
217
+ let decision;
218
+ try {
219
+ decision = await handler(params);
220
+ }
221
+ catch (error) {
222
+ this.#report({ approvalId: params.approvalId, error, kind: "handlerThrew" });
223
+ return;
224
+ }
225
+ const availableChoiceIds = params.availableChoices.map((choice) => choice.choiceId);
226
+ if (!availableChoiceIds.includes(decision.choiceId)) {
227
+ // D-006 select-never-create, enforced on THIS side of the transport. An
228
+ // invented choice bounces -32052 a round trip later, by which time the
229
+ // stage may have advanced — so the diagnosis a consumer would actually
230
+ // see is a stale-requirement error about a frame it should never have
231
+ // sent.
232
+ this.#report({
233
+ approvalId: params.approvalId,
234
+ availableChoiceIds,
235
+ choiceId: decision.choiceId,
236
+ kind: "unofferedChoice",
237
+ });
238
+ return;
239
+ }
240
+ const decideParams = {
241
+ approvalId: params.approvalId,
242
+ choiceId: decision.choiceId,
243
+ // Echoed from the request, never remembered: SS5.4 makes a stale value a
244
+ // -32053, and the request in hand is the only current statement of it.
245
+ requirementId: params.currentRequirementId,
246
+ sessionId: this.#sessionId,
247
+ };
248
+ // Loose guard, and omitted rather than nulled (SS1.2): `feedback` is valid
249
+ // only on choices with `acceptsFeedback`, so an explicit null forwarded by
250
+ // a plain-JS caller would be rejected by the host.
251
+ if (decision.feedback != null)
252
+ decideParams.feedback = decision.feedback;
253
+ // The wait covers only this round trip: an update for this approval read
254
+ // before the ack lands waits for it (see `#inFlight`).
255
+ const approvalId = params.approvalId;
256
+ let settle;
257
+ const acked = new Promise((resolve) => {
258
+ settle = resolve;
259
+ });
260
+ this.#inFlight.set(approvalId, acked);
261
+ try {
262
+ // No explicit `commandId`: unlike `sendUserTurn`, nothing here needs the
263
+ // id before the ack, so `Connection.command()` mints and stamps it — and
264
+ // its own SS3.1.1 retry then reuses that id for free.
265
+ const ack = await connection.command("approval/decide", decideParams);
266
+ // A `terminal: true` ack closed the WHOLE approval; remember it so the
267
+ // trailing stale-frontier update #37538 permits cannot re-prompt.
268
+ if (ack["terminal"] === true)
269
+ this.#terminalApprovals.add(approvalId);
270
+ }
271
+ catch (error) {
272
+ this.#report({ approvalId, error, kind: "submitFailed" });
273
+ }
274
+ finally {
275
+ settle();
276
+ if (this.#inFlight.get(approvalId) === acked)
277
+ this.#inFlight.delete(approvalId);
278
+ }
279
+ }
280
+ #report(failure) {
281
+ try {
282
+ this.#onFailure?.(failure);
283
+ }
284
+ catch {
285
+ // The consumer's failure OBSERVER threw. Swallowed, deliberately: a
286
+ // rejection escaping here rides `SessionApplyOutcome.io`, whose contract
287
+ // is "IT NEVER REJECTS" — and since most consumers never await `io`, it
288
+ // would surface as an unhandled rejection that kills the embedder, the
289
+ // exact failure that contract exists to prevent. The failure itself was
290
+ // already handed to the observer; there is no one left to tell.
291
+ }
292
+ }
293
+ }