@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,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
|
+
}
|