@sublang/playbook 8.0.0 → 9.0.0
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 +3 -3
- package/docs/cli.md +15 -15
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +7 -2
- package/package.json +1 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +14 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +18 -4
- package/reference/sdlc/code.playbook/code.fsm.d.ts +4 -1
- package/reference/sdlc/code.playbook/code.fsm.js +11 -4
- package/reference/sdlc/code.playbook/code.fsm.ts +12 -4
- package/reference/sdlc/code.playbook/code.playbook.js +14 -3
- package/reference/sdlc/code.playbook/code.playbook.ts +13 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +44 -10
- package/reference/sdlc/code.playbook/playbook-captain.ts +47 -10
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +2 -0
- package/reference/sdlc/decide.playbook/decide.playbook.js +299 -117
- package/reference/sdlc/decide.playbook/decide.playbook.ts +395 -131
- package/reference/sdlc/review.playbook/review.playbook.js +14 -3
- package/reference/sdlc/review.playbook/review.playbook.ts +13 -3
- package/slc/gears2fsm.md +19 -2
- package/slc/link.md +184 -42
- package/src/runtime.d.ts +1 -0
- package/src/runtime.ts +1 -0
- package/src/xstate-playbook-runtime.d.ts +13 -3
- package/src/xstate-playbook-runtime.js +732 -251
- package/src/xstate-playbook-runtime.ts +873 -280
- package/src/xstate-runtime.d.ts +17 -7
- package/src/xstate-runtime.js +135 -57
- package/src/xstate-runtime.ts +243 -84
package/src/xstate-runtime.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { type AnyActorRef, type PromiseActorLogic, type SnapshotFrom } from 'xstate';
|
|
2
2
|
import type { CaptainResult, JsonValue, NormalizedError, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookSuspendedCall, PlayerResult } from './runtime.js';
|
|
3
3
|
export * from './xstate-playbook-runtime.js';
|
|
4
|
+
/**
|
|
5
|
+
* Immutable cancellation provenance for one runtime operation. The captured
|
|
6
|
+
* signal identities do not change when a mutable runtime advances to another
|
|
7
|
+
* public boundary, while each signal's eventual reason remains observable.
|
|
8
|
+
*/
|
|
9
|
+
interface AbortReasonClassifier {
|
|
10
|
+
isAbortReason(error: unknown): boolean;
|
|
11
|
+
}
|
|
4
12
|
/**
|
|
5
13
|
* Compose invocation-lifetime and imperative-boundary cancellation without
|
|
6
14
|
* installing a second forwarding listener in each generated runtime.
|
|
@@ -22,7 +30,7 @@ export declare function hiddenControlEnvelope(prompt: string): string;
|
|
|
22
30
|
export declare function normalizeError(error: unknown): NormalizedError;
|
|
23
31
|
export interface PlaybookStateMetadata {
|
|
24
32
|
stateId: string;
|
|
25
|
-
description
|
|
33
|
+
description?: string;
|
|
26
34
|
}
|
|
27
35
|
/** Read stable state identity without consulting XState's private `_nodes`. */
|
|
28
36
|
export declare function activePlaybookStateMetadata(snapshot: unknown): readonly PlaybookStateMetadata[];
|
|
@@ -59,12 +67,14 @@ export interface NestedPlaybookBridgeOptions {
|
|
|
59
67
|
/** Active public runtime boundary whose abort also owns a new child call. */
|
|
60
68
|
getBoundarySignal?(): AbortSignal | undefined;
|
|
61
69
|
callPlaybook(request: PlaybookCallRequest, signal: AbortSignal): Promise<PlaybookCallStart>;
|
|
62
|
-
emitStarted(event: PlaybookCallStarted): Promise<void>;
|
|
63
|
-
emitFinished(event: PlaybookCallFinished): Promise<void>;
|
|
64
|
-
drain(): Promise<void>;
|
|
65
|
-
bindResumeSignal?(signal: AbortSignal): void;
|
|
66
|
-
|
|
67
|
-
|
|
70
|
+
emitStarted(event: PlaybookCallStarted, aborts?: AbortReasonClassifier): Promise<void>;
|
|
71
|
+
emitFinished(event: PlaybookCallFinished, aborts?: AbortReasonClassifier): Promise<void>;
|
|
72
|
+
drain(aborts?: AbortReasonClassifier): Promise<void>;
|
|
73
|
+
bindResumeSignal?(signal: AbortSignal, aborts?: AbortReasonClassifier): void;
|
|
74
|
+
/** Bind provenance to the root transition caused by this child result. */
|
|
75
|
+
bindActorSettlement?(aborts: AbortReasonClassifier): void;
|
|
76
|
+
onControlPlaneError?(error: unknown, aborts?: AbortReasonClassifier): void;
|
|
77
|
+
onBackgroundError?(error: unknown, aborts?: AbortReasonClassifier): void;
|
|
68
78
|
}
|
|
69
79
|
export declare class NestedPlaybookCallError extends Error {
|
|
70
80
|
readonly result: PlaybookCallResult;
|
package/src/xstate-runtime.js
CHANGED
|
@@ -31,9 +31,19 @@ function withAbort(promise, signal) {
|
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
// slc/link.md §Abort: cancellation is causal identity with the applicable
|
|
35
|
+
// signal's reason; an `AbortError`-named rejection that is not that exact
|
|
36
|
+
// reason is a control-plane failure to surface, never an abort to swallow.
|
|
34
37
|
function isAbortReason(error, signal) {
|
|
35
|
-
return
|
|
36
|
-
|
|
38
|
+
return signal.aborted && Object.is(error, signal.reason);
|
|
39
|
+
}
|
|
40
|
+
function createAbortReasonClassifier(...sources) {
|
|
41
|
+
const captured = Object.freeze(sources.filter((source) => source !== undefined));
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
|
|
44
|
+
? isAbortReason(error, source)
|
|
45
|
+
: source.isAbortReason(error)),
|
|
46
|
+
});
|
|
37
47
|
}
|
|
38
48
|
const NEVER_ABORTED_SIGNAL = new AbortController().signal;
|
|
39
49
|
/**
|
|
@@ -73,7 +83,7 @@ export function registerPlaybookAbortCleanup(signal, cleanup) {
|
|
|
73
83
|
// the bridge's allSettled drain observes its outcome.
|
|
74
84
|
void cleanup.catch(() => undefined);
|
|
75
85
|
}
|
|
76
|
-
async function drainPlaybookAbortCleanups(signal) {
|
|
86
|
+
async function drainPlaybookAbortCleanups(signal, aborts) {
|
|
77
87
|
const failures = [];
|
|
78
88
|
while (true) {
|
|
79
89
|
const pending = abortCleanups.get(signal);
|
|
@@ -83,8 +93,10 @@ async function drainPlaybookAbortCleanups(signal) {
|
|
|
83
93
|
pending.clear();
|
|
84
94
|
const outcomes = await Promise.allSettled(batch);
|
|
85
95
|
for (const outcome of outcomes) {
|
|
86
|
-
if (outcome.status === 'rejected'
|
|
96
|
+
if (outcome.status === 'rejected' &&
|
|
97
|
+
!aborts.isAbortReason(outcome.reason)) {
|
|
87
98
|
failures.push(outcome.reason);
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
abortCleanups.delete(signal);
|
|
@@ -484,12 +496,23 @@ export function activePlaybookStateMetadata(snapshot) {
|
|
|
484
496
|
throw new TypeError(`${nodeId}.meta.playbook must be an object`);
|
|
485
497
|
}
|
|
486
498
|
const stateId = requireNonEmptyString(meta.playbook.stateId, `${nodeId}.meta.playbook.stateId`);
|
|
487
|
-
|
|
499
|
+
// Description is optional: a state may declare none and stay fully
|
|
500
|
+
// usable, merely carrying no `stateDescription` downstream. A declared
|
|
501
|
+
// description must still be a nonempty string.
|
|
502
|
+
const description = meta.playbook.description === undefined
|
|
503
|
+
? undefined
|
|
504
|
+
: requireNonEmptyString(meta.playbook.description, `${nodeId}.meta.playbook.description`);
|
|
488
505
|
const previous = byStateId.get(stateId);
|
|
489
|
-
if (previous
|
|
506
|
+
if (previous?.description !== undefined &&
|
|
507
|
+
description !== undefined &&
|
|
508
|
+
previous.description !== description) {
|
|
490
509
|
throw new TypeError(`active state id ${stateId} has conflicting descriptions`);
|
|
491
510
|
}
|
|
492
|
-
|
|
511
|
+
const effective = description ?? previous?.description;
|
|
512
|
+
byStateId.set(stateId, {
|
|
513
|
+
stateId,
|
|
514
|
+
...(effective === undefined ? {} : { description: effective }),
|
|
515
|
+
});
|
|
493
516
|
}
|
|
494
517
|
return [...byStateId.values()].sort((left, right) => left.stateId.localeCompare(right.stateId));
|
|
495
518
|
}
|
|
@@ -948,22 +971,26 @@ export function createNestedPlaybookBridge(options) {
|
|
|
948
971
|
let disposed = false;
|
|
949
972
|
const usedCallIds = new Set();
|
|
950
973
|
const pendingListeners = new Set();
|
|
951
|
-
const reportBackgroundError = (error) => {
|
|
974
|
+
const reportBackgroundError = (error, aborts) => {
|
|
975
|
+
if (aborts?.isAbortReason(error))
|
|
976
|
+
return;
|
|
952
977
|
try {
|
|
953
|
-
options.onBackgroundError?.(error);
|
|
978
|
+
options.onBackgroundError?.(error, aborts);
|
|
954
979
|
}
|
|
955
980
|
catch {
|
|
956
981
|
// Background observers are a terminal sink and cannot own cleanup.
|
|
957
982
|
}
|
|
958
983
|
};
|
|
959
|
-
const reportControlPlaneError = (error) => {
|
|
984
|
+
const reportControlPlaneError = (error, aborts) => {
|
|
985
|
+
if (aborts?.isAbortReason(error))
|
|
986
|
+
return;
|
|
960
987
|
try {
|
|
961
|
-
options.onControlPlaneError?.(error);
|
|
988
|
+
options.onControlPlaneError?.(error, aborts);
|
|
962
989
|
}
|
|
963
990
|
catch (callbackError) {
|
|
964
991
|
// Observability callbacks must never prevent terminal cleanup of the
|
|
965
992
|
// invocation they are observing.
|
|
966
|
-
reportBackgroundError(callbackError);
|
|
993
|
+
reportBackgroundError(callbackError, aborts);
|
|
967
994
|
}
|
|
968
995
|
};
|
|
969
996
|
const rejectControlPlane = (error) => {
|
|
@@ -1003,27 +1030,37 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1003
1030
|
if (current === active)
|
|
1004
1031
|
current = undefined;
|
|
1005
1032
|
};
|
|
1006
|
-
|
|
1033
|
+
// A failure causally identical to an applicable abort reason is the
|
|
1034
|
+
// cancellation's own evidence, never a control-plane error.
|
|
1035
|
+
const reportNonAbortControlError = (error, aborts) => {
|
|
1036
|
+
reportControlPlaneError(error, aborts);
|
|
1037
|
+
};
|
|
1038
|
+
const emitFinish = async (active, result, aborts) => {
|
|
1007
1039
|
await options.emitFinished({
|
|
1008
1040
|
callId: active.callId,
|
|
1009
1041
|
stateId: active.input.stateId,
|
|
1010
1042
|
playbookId: active.input.playbookId,
|
|
1011
1043
|
text: active.input.text,
|
|
1012
1044
|
result,
|
|
1013
|
-
});
|
|
1014
|
-
await options.drain();
|
|
1045
|
+
}, aborts);
|
|
1046
|
+
await options.drain(aborts);
|
|
1015
1047
|
};
|
|
1016
1048
|
const finishImmediate = async (active, result, controlError, resultAfterAbortCleanup) => {
|
|
1049
|
+
const aborts = active.aborts;
|
|
1017
1050
|
let effectiveResult = result;
|
|
1018
1051
|
let cleanupControlError;
|
|
1019
1052
|
if (result.status === 'aborted' || active.signal.aborted) {
|
|
1020
1053
|
try {
|
|
1021
|
-
await drainPlaybookAbortCleanups(active.signal);
|
|
1054
|
+
await drainPlaybookAbortCleanups(active.signal, aborts);
|
|
1022
1055
|
}
|
|
1023
1056
|
catch (error) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1057
|
+
// A cleanup rejection identical to an applicable abort reason is
|
|
1058
|
+
// the cancellation's own evidence — no latch, no result override.
|
|
1059
|
+
if (!aborts.isAbortReason(error)) {
|
|
1060
|
+
cleanupControlError = error;
|
|
1061
|
+
reportControlPlaneError(error, aborts);
|
|
1062
|
+
effectiveResult = resultFromThrown(active.input.playbookId, active.childSessionId, error, false);
|
|
1063
|
+
}
|
|
1027
1064
|
}
|
|
1028
1065
|
if (cleanupControlError === undefined && resultAfterAbortCleanup) {
|
|
1029
1066
|
effectiveResult = resultAfterAbortCleanup();
|
|
@@ -1031,10 +1068,10 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1031
1068
|
}
|
|
1032
1069
|
let finishControlError;
|
|
1033
1070
|
try {
|
|
1034
|
-
await emitFinish(active, effectiveResult);
|
|
1071
|
+
await emitFinish(active, effectiveResult, aborts);
|
|
1035
1072
|
}
|
|
1036
1073
|
catch (error) {
|
|
1037
|
-
|
|
1074
|
+
reportNonAbortControlError(error, aborts);
|
|
1038
1075
|
finishControlError = error;
|
|
1039
1076
|
}
|
|
1040
1077
|
finally {
|
|
@@ -1042,6 +1079,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1042
1079
|
// emission fails, do not leave a permanently unresumable call in the
|
|
1043
1080
|
// bridge and prevent disposal or a later invocation.
|
|
1044
1081
|
clear(active);
|
|
1082
|
+
options.bindActorSettlement?.(aborts);
|
|
1045
1083
|
}
|
|
1046
1084
|
if (controlError !== undefined)
|
|
1047
1085
|
throw controlError;
|
|
@@ -1051,7 +1089,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1051
1089
|
throw finishControlError;
|
|
1052
1090
|
return outputOrThrow(effectiveResult);
|
|
1053
1091
|
};
|
|
1054
|
-
const settlePending = async (active, result, controlError) => {
|
|
1092
|
+
const settlePending = async (active, result, controlError, aborts = active.aborts) => {
|
|
1055
1093
|
if (active.phase === 'settling' && active.settlement) {
|
|
1056
1094
|
await active.settlement;
|
|
1057
1095
|
return;
|
|
@@ -1065,32 +1103,40 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1065
1103
|
let cleanupControlError;
|
|
1066
1104
|
if (result.status === 'aborted' || active.signal.aborted) {
|
|
1067
1105
|
if (result.status !== 'aborted' && active.signal.aborted) {
|
|
1068
|
-
effectiveResult = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason
|
|
1069
|
-
new Error('Nested playbook invocation aborted'), true);
|
|
1106
|
+
effectiveResult = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason, true);
|
|
1070
1107
|
}
|
|
1071
1108
|
try {
|
|
1072
|
-
await drainPlaybookAbortCleanups(active.signal);
|
|
1109
|
+
await drainPlaybookAbortCleanups(active.signal, aborts);
|
|
1073
1110
|
}
|
|
1074
1111
|
catch (cleanupError) {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1112
|
+
// A cleanup rejection identical to an applicable abort reason is
|
|
1113
|
+
// the cancellation's own evidence — no latch, no result override.
|
|
1114
|
+
if (!aborts.isAbortReason(cleanupError)) {
|
|
1115
|
+
cleanupControlError = cleanupError;
|
|
1116
|
+
reportControlPlaneError(cleanupError, aborts);
|
|
1117
|
+
effectiveResult = resultFromThrown(active.input.playbookId, active.childSessionId, cleanupError, false);
|
|
1118
|
+
}
|
|
1078
1119
|
}
|
|
1079
1120
|
}
|
|
1080
1121
|
try {
|
|
1081
|
-
await emitFinish(active, effectiveResult);
|
|
1122
|
+
await emitFinish(active, effectiveResult, aborts);
|
|
1082
1123
|
}
|
|
1083
1124
|
catch (error) {
|
|
1084
1125
|
// A finish event is the durable return boundary. If it cannot be
|
|
1085
1126
|
// emitted and drained, the child result must not remain retryable:
|
|
1086
1127
|
// clear the identity and fail the promise actor so its parent takes
|
|
1087
|
-
// onError instead of observing a phantom suspended child.
|
|
1088
|
-
|
|
1128
|
+
// onError instead of observing a phantom suspended child. A finish
|
|
1129
|
+
// rejection that is an applicable abort reason — the invocation's
|
|
1130
|
+
// or the settling resume's — evidences cancellation, not a
|
|
1131
|
+
// control-plane failure (slc/link.md §Abort).
|
|
1132
|
+
reportControlPlaneError(error, aborts);
|
|
1089
1133
|
clear(active);
|
|
1134
|
+
options.bindActorSettlement?.(aborts);
|
|
1090
1135
|
active.deferred.reject(error);
|
|
1091
1136
|
throw error;
|
|
1092
1137
|
}
|
|
1093
1138
|
clear(active);
|
|
1139
|
+
options.bindActorSettlement?.(aborts);
|
|
1094
1140
|
if (controlError !== undefined) {
|
|
1095
1141
|
active.deferred.reject(controlError);
|
|
1096
1142
|
}
|
|
@@ -1126,6 +1172,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1126
1172
|
active.restoreRolledBack = true;
|
|
1127
1173
|
clear(active);
|
|
1128
1174
|
usedCallIds.delete(active.callId);
|
|
1175
|
+
options.bindActorSettlement?.(active.aborts);
|
|
1129
1176
|
active.deferred.reject(error);
|
|
1130
1177
|
return active;
|
|
1131
1178
|
};
|
|
@@ -1136,9 +1183,9 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1136
1183
|
const abortListener = () => {
|
|
1137
1184
|
if (active.phase !== 'suspended')
|
|
1138
1185
|
return;
|
|
1139
|
-
const result = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason
|
|
1186
|
+
const result = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason, true);
|
|
1140
1187
|
void settlePending(active, result).catch((error) => {
|
|
1141
|
-
reportBackgroundError(error);
|
|
1188
|
+
reportBackgroundError(error, active.aborts);
|
|
1142
1189
|
});
|
|
1143
1190
|
};
|
|
1144
1191
|
active.abortListener = abortListener;
|
|
@@ -1152,7 +1199,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1152
1199
|
listener(pendingCall);
|
|
1153
1200
|
}
|
|
1154
1201
|
catch (error) {
|
|
1155
|
-
reportBackgroundError(error);
|
|
1202
|
+
reportBackgroundError(error, active.aborts);
|
|
1156
1203
|
}
|
|
1157
1204
|
}
|
|
1158
1205
|
if (active.signal.aborted)
|
|
@@ -1222,8 +1269,11 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1222
1269
|
}
|
|
1223
1270
|
const controller = new AbortController();
|
|
1224
1271
|
let callSignal;
|
|
1272
|
+
let callAborts;
|
|
1225
1273
|
try {
|
|
1226
|
-
|
|
1274
|
+
const boundarySignal = options.getBoundarySignal?.();
|
|
1275
|
+
callSignal = combineAbortSignals(invocationSignal, boundarySignal, controller.signal);
|
|
1276
|
+
callAborts = createAbortReasonClassifier(invocationSignal, boundarySignal, controller.signal);
|
|
1227
1277
|
}
|
|
1228
1278
|
catch (error) {
|
|
1229
1279
|
failRestoreMode(mode, error);
|
|
@@ -1239,6 +1289,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1239
1289
|
finished: deferred(),
|
|
1240
1290
|
controller,
|
|
1241
1291
|
signal: callSignal,
|
|
1292
|
+
aborts: callAborts,
|
|
1242
1293
|
phase: 'restoring',
|
|
1243
1294
|
childSessionId: seed.childSessionId,
|
|
1244
1295
|
};
|
|
@@ -1253,8 +1304,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1253
1304
|
active.phase !== 'restoring') {
|
|
1254
1305
|
return;
|
|
1255
1306
|
}
|
|
1256
|
-
rollbackRestoredCall(mode, active.signal.reason
|
|
1257
|
-
new Error('Restored nested playbook invocation aborted'));
|
|
1307
|
+
rollbackRestoredCall(mode, active.signal.reason);
|
|
1258
1308
|
};
|
|
1259
1309
|
active.abortListener = restoreAbortListener;
|
|
1260
1310
|
active.signal.addEventListener('abort', restoreAbortListener, {
|
|
@@ -1291,8 +1341,11 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1291
1341
|
usedCallIds.add(callId);
|
|
1292
1342
|
const controller = new AbortController();
|
|
1293
1343
|
let callSignal;
|
|
1344
|
+
let callAborts;
|
|
1294
1345
|
try {
|
|
1295
|
-
|
|
1346
|
+
const boundarySignal = options.getBoundarySignal?.();
|
|
1347
|
+
callSignal = combineAbortSignals(invocationSignal, boundarySignal, controller.signal);
|
|
1348
|
+
callAborts = createAbortReasonClassifier(invocationSignal, boundarySignal, controller.signal);
|
|
1296
1349
|
}
|
|
1297
1350
|
catch (error) {
|
|
1298
1351
|
return rejectControlPlane(error);
|
|
@@ -1304,6 +1357,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1304
1357
|
finished: deferred(),
|
|
1305
1358
|
controller,
|
|
1306
1359
|
signal: callSignal,
|
|
1360
|
+
aborts: callAborts,
|
|
1307
1361
|
phase: 'starting',
|
|
1308
1362
|
};
|
|
1309
1363
|
current = active;
|
|
@@ -1312,26 +1366,39 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1312
1366
|
// for their entering state. Yield through the runtime's global queue so
|
|
1313
1367
|
// that transition/status telemetry is enqueued before call.started.
|
|
1314
1368
|
try {
|
|
1315
|
-
await options.drain();
|
|
1369
|
+
await options.drain(active.aborts);
|
|
1316
1370
|
}
|
|
1317
1371
|
catch (error) {
|
|
1318
|
-
|
|
1372
|
+
reportNonAbortControlError(error, active.aborts);
|
|
1319
1373
|
clear(active);
|
|
1320
1374
|
throw error;
|
|
1321
1375
|
}
|
|
1322
1376
|
try {
|
|
1323
|
-
await options.emitStarted({ callId, ...normalizedInput });
|
|
1377
|
+
await options.emitStarted({ callId, ...normalizedInput }, active.aborts);
|
|
1324
1378
|
}
|
|
1325
1379
|
catch (error) {
|
|
1326
|
-
|
|
1327
|
-
|
|
1380
|
+
// A start-sink rejection identical to the applicable abort
|
|
1381
|
+
// reason is the cancellation itself: the pair finishes
|
|
1382
|
+
// `aborted` and nothing is reported (slc/link.md §Abort).
|
|
1383
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
1384
|
+
? undefined
|
|
1385
|
+
: error;
|
|
1386
|
+
if (controlError !== undefined) {
|
|
1387
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
1388
|
+
}
|
|
1389
|
+
return await finishImmediate(active, resultFromThrown(normalizedInput.playbookId, undefined, error, controlError === undefined), controlError);
|
|
1328
1390
|
}
|
|
1329
1391
|
try {
|
|
1330
|
-
await options.drain();
|
|
1392
|
+
await options.drain(active.aborts);
|
|
1331
1393
|
}
|
|
1332
1394
|
catch (error) {
|
|
1333
|
-
|
|
1334
|
-
|
|
1395
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
1396
|
+
? undefined
|
|
1397
|
+
: error;
|
|
1398
|
+
if (controlError !== undefined) {
|
|
1399
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
1400
|
+
}
|
|
1401
|
+
return await finishImmediate(active, resultFromThrown(normalizedInput.playbookId, undefined, error, controlError === undefined), controlError);
|
|
1335
1402
|
}
|
|
1336
1403
|
const request = {
|
|
1337
1404
|
callId,
|
|
@@ -1354,7 +1421,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1354
1421
|
throw error;
|
|
1355
1422
|
});
|
|
1356
1423
|
const openingCleanup = starting.then(() => undefined, (error) => {
|
|
1357
|
-
if (isAbortReason(error
|
|
1424
|
+
if (active.aborts.isAbortReason(error))
|
|
1358
1425
|
return;
|
|
1359
1426
|
throw error;
|
|
1360
1427
|
});
|
|
@@ -1371,11 +1438,14 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1371
1438
|
rawStart = await withAbort(starting, active.signal);
|
|
1372
1439
|
}
|
|
1373
1440
|
catch (error) {
|
|
1374
|
-
const controlError = active.
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1441
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
1442
|
+
? undefined
|
|
1443
|
+
: error;
|
|
1444
|
+
if (controlError !== undefined) {
|
|
1445
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
1446
|
+
}
|
|
1447
|
+
const result = resultFromThrown(normalizedInput.playbookId, undefined, error, controlError === undefined && active.signal.aborted);
|
|
1448
|
+
return await finishImmediate(active, result, controlError, controlError === undefined && active.signal.aborted
|
|
1379
1449
|
? () => resultFromThrown(normalizedInput.playbookId, startSettled
|
|
1380
1450
|
? assignedChildSessionId(observedStart)
|
|
1381
1451
|
: undefined, error, true)
|
|
@@ -1503,8 +1573,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1503
1573
|
}
|
|
1504
1574
|
const active = mode.active;
|
|
1505
1575
|
if (active.signal.aborted) {
|
|
1506
|
-
const error = active.signal.reason
|
|
1507
|
-
new Error('Restored nested playbook invocation aborted');
|
|
1576
|
+
const error = active.signal.reason;
|
|
1508
1577
|
rollbackRestoredCall(mode, error);
|
|
1509
1578
|
restoreMode = undefined;
|
|
1510
1579
|
throw error;
|
|
@@ -1532,7 +1601,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1532
1601
|
listener(pendingCall);
|
|
1533
1602
|
}
|
|
1534
1603
|
catch (error) {
|
|
1535
|
-
reportBackgroundError(error);
|
|
1604
|
+
reportBackgroundError(error, current?.aborts);
|
|
1536
1605
|
}
|
|
1537
1606
|
}
|
|
1538
1607
|
return () => pendingListeners.delete(listener);
|
|
@@ -1560,8 +1629,17 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1560
1629
|
}
|
|
1561
1630
|
throw error;
|
|
1562
1631
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1632
|
+
// A resume whose signal is already aborted delivers nothing: the
|
|
1633
|
+
// validated child result is not consumed, no finish is emitted, and
|
|
1634
|
+
// the pending call survives for a later resume with a fresh signal
|
|
1635
|
+
// (slc/link.md §Nested playbook bridge). Identity and validation
|
|
1636
|
+
// control errors above still win — they are the caller's defects.
|
|
1637
|
+
if (signal.aborted) {
|
|
1638
|
+
throw signal.reason;
|
|
1639
|
+
}
|
|
1640
|
+
const resumeAborts = createAbortReasonClassifier(active.aborts, signal);
|
|
1641
|
+
options.bindResumeSignal?.(signal, resumeAborts);
|
|
1642
|
+
await settlePending(active, validatedResult, undefined, resumeAborts);
|
|
1565
1643
|
},
|
|
1566
1644
|
abortPending,
|
|
1567
1645
|
async dispose() {
|