@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
|
@@ -114,20 +114,51 @@ function snapshotDecideRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
|
|
|
114
114
|
return Object.freeze({});
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
117
|
+
interface AuthoredStateConfig {
|
|
118
|
+
readonly meta?: {
|
|
119
|
+
readonly playbook?: {
|
|
120
|
+
readonly stateId?: unknown;
|
|
121
|
+
readonly description?: unknown;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
readonly states?: Readonly<Record<string, AuthoredStateConfig>>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function authoredStateDescriptions(
|
|
128
|
+
states: Readonly<Record<string, AuthoredStateConfig>> | undefined,
|
|
129
|
+
): Readonly<Record<string, string>> {
|
|
130
|
+
const descriptions: Record<string, string> = {};
|
|
131
|
+
const visit = (
|
|
132
|
+
children: Readonly<Record<string, AuthoredStateConfig>> | undefined,
|
|
133
|
+
): void => {
|
|
134
|
+
for (const state of Object.values(children ?? {})) {
|
|
135
|
+
const stateId = state.meta?.playbook?.stateId;
|
|
136
|
+
const description = state.meta?.playbook?.description;
|
|
137
|
+
if (
|
|
138
|
+
typeof stateId === 'string' &&
|
|
139
|
+
typeof description === 'string' &&
|
|
140
|
+
description.trim().length > 0
|
|
141
|
+
) {
|
|
142
|
+
const existing = descriptions[stateId];
|
|
143
|
+
if (existing !== undefined && existing !== description) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`DECIDE state ${stateId} declares conflicting descriptions`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
descriptions[stateId] = description;
|
|
149
|
+
}
|
|
150
|
+
visit(state.states);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
visit(states);
|
|
154
|
+
return Object.freeze(descriptions);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const STATE_DESCRIPTIONS = authoredStateDescriptions(
|
|
158
|
+
decideMachine.config.states as
|
|
159
|
+
| Readonly<Record<string, AuthoredStateConfig>>
|
|
160
|
+
| undefined,
|
|
161
|
+
);
|
|
131
162
|
|
|
132
163
|
const ROLE_STATES = [
|
|
133
164
|
{ stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
|
|
@@ -584,6 +615,27 @@ function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
|
584
615
|
return signal.aborted && Object.is(error, signal.reason);
|
|
585
616
|
}
|
|
586
617
|
|
|
618
|
+
interface AbortReasonClassifier {
|
|
619
|
+
isAbortReason(error: unknown): boolean;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function abortReasonClassifier(
|
|
623
|
+
...sources: readonly (AbortSignal | AbortReasonClassifier | undefined)[]
|
|
624
|
+
): AbortReasonClassifier {
|
|
625
|
+
const captured = sources.filter(
|
|
626
|
+
(source): source is AbortSignal | AbortReasonClassifier =>
|
|
627
|
+
source !== undefined,
|
|
628
|
+
);
|
|
629
|
+
return Object.freeze({
|
|
630
|
+
isAbortReason: (error: unknown): boolean =>
|
|
631
|
+
captured.some((source) =>
|
|
632
|
+
source instanceof AbortSignal
|
|
633
|
+
? isAbortFailure(error, source)
|
|
634
|
+
: source.isAbortReason(error),
|
|
635
|
+
),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
587
639
|
function pendingQuestionsFromContext(
|
|
588
640
|
context: Record<string, unknown>,
|
|
589
641
|
): PendingBossQuestion[] {
|
|
@@ -635,6 +687,33 @@ const STATUS_STATE_IDS: ReadonlySet<string> = new Set([
|
|
|
635
687
|
'failed',
|
|
636
688
|
]);
|
|
637
689
|
|
|
690
|
+
// PBRT-45: a question is pending only while its authored reply-wait state
|
|
691
|
+
// is active. The context retains an answered question through the resumed
|
|
692
|
+
// player call so the Q+A continuation prompt can quote it, and each branch
|
|
693
|
+
// keeps its own entry through the parallel region — so an unfiltered
|
|
694
|
+
// projection would report the answered question as still awaiting during
|
|
695
|
+
// the resume, and both branch questions after only one remains pending.
|
|
696
|
+
const RESUME_WAIT_STATE_IDS: Readonly<Record<string, string>> = {
|
|
697
|
+
...Object.fromEntries(
|
|
698
|
+
Object.entries(WAIT_STATE_RESUME_IDS).map(([waitStateId, resumeStateId]) => [
|
|
699
|
+
resumeStateId,
|
|
700
|
+
waitStateId,
|
|
701
|
+
]),
|
|
702
|
+
),
|
|
703
|
+
commitCoderProposal: 'awaitBossReply',
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
function pendingQuestionsForState(
|
|
707
|
+
state: PlaybookState,
|
|
708
|
+
context: Record<string, unknown>,
|
|
709
|
+
): PendingBossQuestion[] {
|
|
710
|
+
return pendingQuestionsFromContext(context).filter((pending) =>
|
|
711
|
+
state.activeStateIds.includes(
|
|
712
|
+
RESUME_WAIT_STATE_IDS[pending.resumeStateId] ?? '',
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
638
717
|
function questionForWaitState(
|
|
639
718
|
stateId: string,
|
|
640
719
|
pendingQuestions: readonly PendingBossQuestion[],
|
|
@@ -688,7 +767,7 @@ function telemetryPayload(
|
|
|
688
767
|
event: unknown,
|
|
689
768
|
context: Record<string, unknown>,
|
|
690
769
|
): JsonValue {
|
|
691
|
-
const pendingBossQuestions =
|
|
770
|
+
const pendingBossQuestions = pendingQuestionsForState(state, context);
|
|
692
771
|
const prior = previousState ?? state;
|
|
693
772
|
const payload = {
|
|
694
773
|
from: prior.value,
|
|
@@ -716,6 +795,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
716
795
|
let sessionIdentity: SessionIdentity | undefined;
|
|
717
796
|
let actor: ReturnType<typeof createActor> | undefined;
|
|
718
797
|
let currentSignal: AbortSignal | undefined;
|
|
798
|
+
let currentAborts: AbortReasonClassifier | undefined;
|
|
799
|
+
const actorSettlementAborts: AbortReasonClassifier[] = [];
|
|
800
|
+
let actorSettlementErrorAborts: AbortReasonClassifier | undefined;
|
|
719
801
|
let currentTurnId: number | undefined;
|
|
720
802
|
let previousState: PlaybookState | undefined;
|
|
721
803
|
let suppressInspectionEmissions = false;
|
|
@@ -754,26 +836,35 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
754
836
|
): void => {
|
|
755
837
|
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
756
838
|
};
|
|
757
|
-
const latchInspectionError = (
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
839
|
+
const latchInspectionError = (
|
|
840
|
+
error: unknown,
|
|
841
|
+
aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
842
|
+
): void => {
|
|
843
|
+
if (aborts?.isAbortReason(error)) return;
|
|
844
|
+
if (currentSignal !== undefined) controlPlaneError ??= error;
|
|
845
|
+
else collectFailure(emissionFailures, error);
|
|
763
846
|
};
|
|
764
|
-
const enqueue = (
|
|
847
|
+
const enqueue = (
|
|
848
|
+
fn: () => Promise<void>,
|
|
849
|
+
aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
850
|
+
): Promise<void> => {
|
|
851
|
+
const enqueueAborts = aborts;
|
|
765
852
|
const queued = emissionQueue.add(fn);
|
|
766
853
|
activeEmissionCalls.add(queued);
|
|
767
854
|
void queued.then(
|
|
768
855
|
() => activeEmissionCalls.delete(queued),
|
|
769
856
|
(error: unknown) => {
|
|
770
857
|
activeEmissionCalls.delete(queued);
|
|
771
|
-
|
|
858
|
+
if (!enqueueAborts?.isAbortReason(error)) {
|
|
859
|
+
collectFailure(emissionFailures, error);
|
|
860
|
+
}
|
|
772
861
|
},
|
|
773
862
|
);
|
|
774
863
|
return queued;
|
|
775
864
|
};
|
|
776
|
-
const flush = async (
|
|
865
|
+
const flush = async (
|
|
866
|
+
_aborts: AbortReasonClassifier | undefined = currentAborts,
|
|
867
|
+
): Promise<void> => {
|
|
777
868
|
while (true) {
|
|
778
869
|
const active = [...activeEmissionCalls];
|
|
779
870
|
if (active.length > 0) await Promise.allSettled(active);
|
|
@@ -789,8 +880,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
789
880
|
if (emissionFailures.length === 0) return;
|
|
790
881
|
const failures = emissionFailures;
|
|
791
882
|
emissionFailures = [];
|
|
792
|
-
|
|
793
|
-
|
|
883
|
+
const failure =
|
|
884
|
+
failures.length === 1
|
|
885
|
+
? failures[0]
|
|
886
|
+
: new AggregateError(failures, 'decide runtime emissions failed');
|
|
887
|
+
// Enqueue ownership already classified every stored failure as distinct.
|
|
888
|
+
// Preserve that classification if an unrelated public boundary drains
|
|
889
|
+
// it with a signal whose reason happens to be the same object.
|
|
890
|
+
if (currentSignal !== undefined) controlPlaneError ??= failure;
|
|
891
|
+
throw failure;
|
|
794
892
|
};
|
|
795
893
|
const drainBoundaryCallsAndEmissions = async (): Promise<void> => {
|
|
796
894
|
while (true) {
|
|
@@ -1012,6 +1110,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1012
1110
|
payload: unknown,
|
|
1013
1111
|
meta: { turnId?: number; callId?: string } = {},
|
|
1014
1112
|
describedEmission?: (runtimePorts: PlaybookPorts) => Promise<void>,
|
|
1113
|
+
aborts?: AbortReasonClassifier,
|
|
1015
1114
|
): Promise<void> => {
|
|
1016
1115
|
const runtimePorts = requirePorts();
|
|
1017
1116
|
const identity = requireSessionIdentity();
|
|
@@ -1035,16 +1134,20 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1035
1134
|
...(meta.callId !== undefined ? { callId: meta.callId } : {}),
|
|
1036
1135
|
payload: jsonPayload,
|
|
1037
1136
|
});
|
|
1038
|
-
return enqueue(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1137
|
+
return enqueue(
|
|
1138
|
+
async () => {
|
|
1139
|
+
await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
|
|
1140
|
+
await describedEmission?.(runtimePorts);
|
|
1141
|
+
},
|
|
1142
|
+
aborts,
|
|
1143
|
+
);
|
|
1042
1144
|
};
|
|
1043
1145
|
const emitTrace = (
|
|
1044
1146
|
type: PlaybookTraceType,
|
|
1045
1147
|
payload: unknown,
|
|
1046
1148
|
meta: { turnId?: number; callId?: string } = {},
|
|
1047
|
-
|
|
1149
|
+
aborts?: AbortReasonClassifier,
|
|
1150
|
+
): Promise<void> => enqueueTracedEmission(type, payload, meta, undefined, aborts);
|
|
1048
1151
|
const emitBoundaryStatus = async (
|
|
1049
1152
|
message: string,
|
|
1050
1153
|
state: PlaybookState,
|
|
@@ -1073,8 +1176,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1073
1176
|
meta: { turnId?: number; callId?: string },
|
|
1074
1177
|
signal: AbortSignal,
|
|
1075
1178
|
): Promise<void> => {
|
|
1179
|
+
const aborts = abortReasonClassifier(signal);
|
|
1076
1180
|
try {
|
|
1077
|
-
await emitTrace(startedType, identity, meta);
|
|
1181
|
+
await emitTrace(startedType, identity, meta, aborts);
|
|
1078
1182
|
} catch (error) {
|
|
1079
1183
|
latchControlPlaneError(error, signal);
|
|
1080
1184
|
try {
|
|
@@ -1082,13 +1186,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1082
1186
|
finishedType,
|
|
1083
1187
|
{
|
|
1084
1188
|
...identity,
|
|
1085
|
-
|
|
1189
|
+
// A started-trace sink rejection causally identical to the
|
|
1190
|
+
// boundary reason is the abort's own evidence: the pair
|
|
1191
|
+
// finishes 'aborted', not 'error' (DR-036 §4).
|
|
1192
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1086
1193
|
error: normalizeErrorFull(error) ?? {
|
|
1087
1194
|
name: 'Error',
|
|
1088
1195
|
message: String(error),
|
|
1089
1196
|
},
|
|
1090
1197
|
},
|
|
1091
1198
|
meta,
|
|
1199
|
+
aborts,
|
|
1092
1200
|
);
|
|
1093
1201
|
} catch {
|
|
1094
1202
|
// Preserve the start failure after one best-effort finish attempt.
|
|
@@ -1103,6 +1211,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1103
1211
|
purpose: 'boss-input-classification' | 'player-output-adjudication',
|
|
1104
1212
|
callStateId: string | undefined,
|
|
1105
1213
|
): Promise<string> => {
|
|
1214
|
+
const aborts = abortReasonClassifier(signal);
|
|
1106
1215
|
const identity = {
|
|
1107
1216
|
purpose,
|
|
1108
1217
|
...(callStateId !== undefined ? { stateId: callStateId } : {}),
|
|
@@ -1138,13 +1247,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1138
1247
|
'judge.call.finished',
|
|
1139
1248
|
{
|
|
1140
1249
|
...identity,
|
|
1141
|
-
|
|
1250
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
1251
|
+
// failure under an aborted signal stays an error
|
|
1252
|
+
// (slc/link.md §Abort).
|
|
1253
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1142
1254
|
error: normalizeErrorFull(error) ?? {
|
|
1143
1255
|
name: 'Error',
|
|
1144
1256
|
message: String(error),
|
|
1145
1257
|
},
|
|
1146
1258
|
},
|
|
1147
1259
|
{ turnId: currentTurnId, callId },
|
|
1260
|
+
aborts,
|
|
1148
1261
|
);
|
|
1149
1262
|
throw error;
|
|
1150
1263
|
}
|
|
@@ -1153,6 +1266,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1153
1266
|
'judge.call.finished',
|
|
1154
1267
|
{ ...identity, status: 'ok', reply: finalText },
|
|
1155
1268
|
{ turnId: currentTurnId, callId },
|
|
1269
|
+
aborts,
|
|
1156
1270
|
);
|
|
1157
1271
|
return finalText;
|
|
1158
1272
|
});
|
|
@@ -1178,6 +1292,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1178
1292
|
playerId?: string;
|
|
1179
1293
|
result: PlayerResult;
|
|
1180
1294
|
}> => {
|
|
1295
|
+
const aborts = abortReasonClassifier(signal);
|
|
1181
1296
|
if (!ROLE_ID_SET.has(input.role)) {
|
|
1182
1297
|
throw new TypeError(
|
|
1183
1298
|
`DECIDE player input role must name a declared local role`,
|
|
@@ -1208,13 +1323,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1208
1323
|
'player.call.finished',
|
|
1209
1324
|
{
|
|
1210
1325
|
...identity,
|
|
1211
|
-
|
|
1326
|
+
// Only the exact abort reason is cancellation; a distinct
|
|
1327
|
+
// failure under an aborted signal stays an error
|
|
1328
|
+
// (slc/link.md §Abort).
|
|
1329
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1212
1330
|
error: normalizeErrorFull(error) ?? {
|
|
1213
1331
|
name: 'Error',
|
|
1214
1332
|
message: String(error),
|
|
1215
1333
|
},
|
|
1216
1334
|
},
|
|
1217
1335
|
{ turnId: currentTurnId, callId },
|
|
1336
|
+
aborts,
|
|
1218
1337
|
);
|
|
1219
1338
|
if (inFlightPlayerKeys.has(playerKey)) {
|
|
1220
1339
|
const error = new Error(
|
|
@@ -1310,6 +1429,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1310
1429
|
: {}),
|
|
1311
1430
|
},
|
|
1312
1431
|
{ turnId: currentTurnId, callId },
|
|
1432
|
+
aborts,
|
|
1313
1433
|
);
|
|
1314
1434
|
return {
|
|
1315
1435
|
roleId,
|
|
@@ -1335,64 +1455,70 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1335
1455
|
const player = fromPromise<PlayerOutput, PlayerInput>(
|
|
1336
1456
|
async ({ input, signal }) => {
|
|
1337
1457
|
const combined = combineSignals(signal, currentSignal);
|
|
1338
|
-
|
|
1339
|
-
// Yield through the runtime emission queue before crossing the player
|
|
1340
|
-
// boundary so state trace/status always precede its call-start trace.
|
|
1458
|
+
const settlementAborts = abortReasonClassifier(combined);
|
|
1341
1459
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1353
|
-
// composed call repeated, traced by runPlayerCall as its own
|
|
1354
|
-
// player-call pair, with the resume selection re-read from the
|
|
1355
|
-
// token map the first result left (PBRT-38). An abort that lands
|
|
1356
|
-
// between the two calls ends the turn without the re-ask (aborts
|
|
1357
|
-
// are never retried), and a rejecting finish emission rejects
|
|
1358
|
-
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1460
|
+
// XState starts invoked actors while publishing the entering snapshot.
|
|
1461
|
+
// Yield through the runtime emission queue before crossing the player
|
|
1462
|
+
// boundary so state trace/status always precede its call-start trace.
|
|
1463
|
+
combined.throwIfAborted();
|
|
1464
|
+
try {
|
|
1465
|
+
await flush(settlementAborts);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
latchControlPlaneError(error, combined);
|
|
1468
|
+
throw error;
|
|
1469
|
+
}
|
|
1359
1470
|
combined.throwIfAborted();
|
|
1360
|
-
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
1361
|
-
}
|
|
1362
|
-
if (result.status !== 'ok') {
|
|
1363
|
-
throw new Error(
|
|
1364
|
-
`${roleLabel(roleId)}${
|
|
1365
|
-
playerId === undefined ? '' : ` (${playerId})`
|
|
1366
|
-
} returned status "${result.status}"${
|
|
1367
|
-
result.error ? `: ${result.error}` : ''
|
|
1368
|
-
}`,
|
|
1369
|
-
);
|
|
1370
|
-
}
|
|
1371
|
-
const finalText = result.finalText ?? '';
|
|
1372
|
-
if (isEmptyFinalText(finalText)) {
|
|
1373
|
-
throw new Error(
|
|
1374
|
-
`${roleLabel(roleId)}${
|
|
1375
|
-
playerId === undefined ? '' : ` (${playerId})`
|
|
1376
|
-
} returned status "ok" with no finalText`,
|
|
1377
|
-
);
|
|
1378
|
-
}
|
|
1379
|
-
combined.throwIfAborted();
|
|
1380
1471
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
),
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1472
|
+
let { roleId, playerId, result } = await callPlayer(input, combined);
|
|
1473
|
+
if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
|
|
1474
|
+
// DR-028: an `ok` result whose finalText is missing, empty, or
|
|
1475
|
+
// whitespace-only earns exactly one corrective re-ask — the same
|
|
1476
|
+
// composed call repeated, traced by runPlayerCall as its own
|
|
1477
|
+
// player-call pair, with the resume selection re-read from the
|
|
1478
|
+
// token map the first result left (PBRT-38). An abort that lands
|
|
1479
|
+
// between the two calls ends the turn without the re-ask (aborts
|
|
1480
|
+
// are never retried), and a rejecting finish emission rejects
|
|
1481
|
+
// `callPlayer` itself, so it never reaches this branch (PBRT-47).
|
|
1482
|
+
combined.throwIfAborted();
|
|
1483
|
+
({ roleId, playerId, result } = await callPlayer(input, combined));
|
|
1484
|
+
}
|
|
1485
|
+
if (result.status !== 'ok') {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`${roleLabel(roleId)}${
|
|
1488
|
+
playerId === undefined ? '' : ` (${playerId})`
|
|
1489
|
+
} returned status "${result.status}"${
|
|
1490
|
+
result.error ? `: ${result.error}` : ''
|
|
1491
|
+
}`,
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
const finalText = result.finalText ?? '';
|
|
1495
|
+
if (isEmptyFinalText(finalText)) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`${roleLabel(roleId)}${
|
|
1498
|
+
playerId === undefined ? '' : ` (${playerId})`
|
|
1499
|
+
} returned status "ok" with no finalText`,
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
combined.throwIfAborted();
|
|
1503
|
+
|
|
1504
|
+
try {
|
|
1505
|
+
const prompt = buildAdjudicatorPrompt(input, finalText);
|
|
1506
|
+
return parseAdjudication(
|
|
1507
|
+
await callJudge(
|
|
1508
|
+
prompt,
|
|
1509
|
+
combined,
|
|
1510
|
+
'player-output-adjudication',
|
|
1511
|
+
input.stateId,
|
|
1512
|
+
),
|
|
1513
|
+
input,
|
|
1514
|
+
finalText,
|
|
1515
|
+
);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
latchControlPlaneError(error, combined);
|
|
1518
|
+
throw error;
|
|
1519
|
+
}
|
|
1520
|
+
} finally {
|
|
1521
|
+
actorSettlementAborts.push(settlementAborts);
|
|
1396
1522
|
}
|
|
1397
1523
|
},
|
|
1398
1524
|
);
|
|
@@ -1404,7 +1530,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1404
1530
|
trackBoundaryCall(
|
|
1405
1531
|
Promise.resolve(requirePorts().callPlaybook(request, signal)),
|
|
1406
1532
|
),
|
|
1407
|
-
emitStarted: async (event) => {
|
|
1533
|
+
emitStarted: async (event, aborts) => {
|
|
1408
1534
|
playbookCallTurnIds.set(event.callId, currentTurnId);
|
|
1409
1535
|
await emitTrace(
|
|
1410
1536
|
'playbook.call.started',
|
|
@@ -1417,9 +1543,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1417
1543
|
...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
|
|
1418
1544
|
callId: event.callId,
|
|
1419
1545
|
},
|
|
1546
|
+
aborts,
|
|
1420
1547
|
);
|
|
1421
1548
|
},
|
|
1422
|
-
emitFinished: async (event) => {
|
|
1549
|
+
emitFinished: async (event, aborts) => {
|
|
1423
1550
|
const turnId = playbookCallTurnIds.get(event.callId);
|
|
1424
1551
|
try {
|
|
1425
1552
|
await emitTrace(
|
|
@@ -1434,23 +1561,32 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1434
1561
|
...(turnId === undefined ? {} : { turnId }),
|
|
1435
1562
|
callId: event.callId,
|
|
1436
1563
|
},
|
|
1564
|
+
aborts,
|
|
1437
1565
|
);
|
|
1438
1566
|
} finally {
|
|
1439
1567
|
playbookCallTurnIds.delete(event.callId);
|
|
1440
1568
|
}
|
|
1441
1569
|
},
|
|
1442
1570
|
drain: flush,
|
|
1443
|
-
bindResumeSignal: (signal) => {
|
|
1571
|
+
bindResumeSignal: (signal, aborts) => {
|
|
1444
1572
|
currentSignal = signal;
|
|
1573
|
+
currentAborts = aborts ?? abortReasonClassifier(signal);
|
|
1574
|
+
},
|
|
1575
|
+
bindActorSettlement: (aborts) => {
|
|
1576
|
+
actorSettlementAborts.push(aborts);
|
|
1445
1577
|
},
|
|
1446
|
-
onControlPlaneError: (error) => {
|
|
1447
|
-
|
|
1448
|
-
|
|
1578
|
+
onControlPlaneError: (error, aborts) => {
|
|
1579
|
+
if (
|
|
1580
|
+
!aborts?.isAbortReason(error) &&
|
|
1581
|
+
!currentAborts?.isAbortReason(error)
|
|
1582
|
+
) {
|
|
1449
1583
|
controlPlaneError ??= error;
|
|
1450
1584
|
}
|
|
1451
1585
|
},
|
|
1452
|
-
onBackgroundError: (error) => {
|
|
1453
|
-
|
|
1586
|
+
onBackgroundError: (error, aborts) => {
|
|
1587
|
+
if (!aborts?.isAbortReason(error)) {
|
|
1588
|
+
collectFailure(emissionFailures, error);
|
|
1589
|
+
}
|
|
1454
1590
|
},
|
|
1455
1591
|
});
|
|
1456
1592
|
|
|
@@ -1458,10 +1594,27 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1458
1594
|
actors: { player, playbook: nestedBridge.actorLogic },
|
|
1459
1595
|
});
|
|
1460
1596
|
|
|
1597
|
+
const consumeActorSettlementAborts = (
|
|
1598
|
+
forSnapshot = false,
|
|
1599
|
+
): AbortReasonClassifier | undefined => {
|
|
1600
|
+
const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
|
|
1601
|
+
actorSettlementErrorAborts = undefined;
|
|
1602
|
+
if (forSnapshot && aborts !== undefined) {
|
|
1603
|
+
actorSettlementErrorAborts = aborts;
|
|
1604
|
+
queueMicrotask(() => {
|
|
1605
|
+
if (actorSettlementErrorAborts === aborts) {
|
|
1606
|
+
actorSettlementErrorAborts = undefined;
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
return aborts;
|
|
1611
|
+
};
|
|
1612
|
+
|
|
1461
1613
|
const inspect = (event: InspectionEvent): void => {
|
|
1462
1614
|
if (event.type !== '@xstate.snapshot') return;
|
|
1463
1615
|
if (actor === undefined || event.actorRef !== actor) return;
|
|
1464
1616
|
if (suppressInspectionEmissions) return;
|
|
1617
|
+
const settlementAborts = consumeActorSettlementAborts(true);
|
|
1465
1618
|
try {
|
|
1466
1619
|
const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
|
|
1467
1620
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
@@ -1482,6 +1635,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1482
1635
|
topic: TELEMETRY_TOPIC,
|
|
1483
1636
|
payload: describedFsmPayload,
|
|
1484
1637
|
}),
|
|
1638
|
+
settlementAborts,
|
|
1485
1639
|
).catch(() => undefined);
|
|
1486
1640
|
|
|
1487
1641
|
const priorIds = new Set(previousState?.activeStateIds ?? []);
|
|
@@ -1507,6 +1661,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1507
1661
|
tracePayload,
|
|
1508
1662
|
{ turnId: currentTurnId },
|
|
1509
1663
|
(emissionPorts) => emissionPorts.emitStatus(message, data),
|
|
1664
|
+
settlementAborts,
|
|
1510
1665
|
).catch(() => undefined);
|
|
1511
1666
|
};
|
|
1512
1667
|
|
|
@@ -1552,7 +1707,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1552
1707
|
);
|
|
1553
1708
|
}
|
|
1554
1709
|
} catch (error) {
|
|
1555
|
-
latchInspectionError(error);
|
|
1710
|
+
latchInspectionError(error, settlementAborts);
|
|
1556
1711
|
}
|
|
1557
1712
|
};
|
|
1558
1713
|
|
|
@@ -1571,6 +1726,16 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1571
1726
|
}),
|
|
1572
1727
|
inspect,
|
|
1573
1728
|
});
|
|
1729
|
+
// A synchronous FSM action throw errors the actor without any pending
|
|
1730
|
+
// boundary await to observe it; unobserved, XState would surface it via
|
|
1731
|
+
// reportUnhandledError as an uncaughtException. Observe it here: latch
|
|
1732
|
+
// it as a control error while a turn signal is active (unless it is
|
|
1733
|
+
// the abort reason itself), otherwise collect it with the emission
|
|
1734
|
+
// failures (slc/link.md §Abort).
|
|
1735
|
+
actor.subscribe({
|
|
1736
|
+
error: (error) =>
|
|
1737
|
+
latchInspectionError(error, consumeActorSettlementAborts()),
|
|
1738
|
+
});
|
|
1574
1739
|
};
|
|
1575
1740
|
|
|
1576
1741
|
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
@@ -1610,7 +1775,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1610
1775
|
const state = normalizePlaybookSnapshot(snapshot, {
|
|
1611
1776
|
pendingCall: nestedBridge.getPendingCall(),
|
|
1612
1777
|
});
|
|
1613
|
-
const pendingQuestions =
|
|
1778
|
+
const pendingQuestions = pendingQuestionsForState(state, context);
|
|
1614
1779
|
if (
|
|
1615
1780
|
pendingQuestions.length === 0 &&
|
|
1616
1781
|
(snapshot.status === 'done' ||
|
|
@@ -1644,34 +1809,58 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1644
1809
|
const pendingCall = nestedBridge.getPendingCall();
|
|
1645
1810
|
const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
|
|
1646
1811
|
const context = snapshot.context as unknown as Record<string, unknown>;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1812
|
+
const abortedResult = (abortSignal: AbortSignal): PlaybookRunResult => ({
|
|
1813
|
+
outcome: 'aborted',
|
|
1814
|
+
state,
|
|
1815
|
+
...(abortSignal.reason === undefined
|
|
1816
|
+
? {}
|
|
1817
|
+
: {
|
|
1818
|
+
error: normalizeErrorFull(abortSignal.reason) ?? {
|
|
1819
|
+
name: 'AbortError',
|
|
1820
|
+
message: String(abortSignal.reason),
|
|
1821
|
+
},
|
|
1822
|
+
}),
|
|
1823
|
+
});
|
|
1824
|
+
if (snapshot.status === 'error') {
|
|
1825
|
+
// An errored actor outranks a coincident abort unless the actor's
|
|
1826
|
+
// error is the abort reason itself (slc/link.md §Abort).
|
|
1827
|
+
const actorError = (snapshot as { error?: unknown }).error;
|
|
1828
|
+
if (
|
|
1829
|
+
actorError !== undefined &&
|
|
1830
|
+
signal !== undefined &&
|
|
1831
|
+
isAbortFailure(actorError, signal)
|
|
1832
|
+
) {
|
|
1833
|
+
return abortedResult(signal);
|
|
1834
|
+
}
|
|
1835
|
+
throw (
|
|
1836
|
+
actorError ?? new Error('decide runtime actor entered error status')
|
|
1837
|
+
);
|
|
1660
1838
|
}
|
|
1839
|
+
// Terminal completion outranks a coincident abort (DR-036 §3): reporting
|
|
1840
|
+
// 'aborted' over a completed machine would hide a terminal state that the
|
|
1841
|
+
// next turn silently restarts, duplicating the workflow's side effects.
|
|
1661
1842
|
if (snapshot.status === 'done') {
|
|
1662
1843
|
const output = (snapshot as { output?: unknown }).output;
|
|
1663
1844
|
if (output !== undefined) assertJsonSafe(output, 'terminal output');
|
|
1845
|
+
const stateDescription = state.activeStateIds.includes('done')
|
|
1846
|
+
? STATE_DESCRIPTIONS.done
|
|
1847
|
+
: state.activeStateIds.includes('reportedReviewFailure')
|
|
1848
|
+
? STATE_DESCRIPTIONS.reportedReviewFailure
|
|
1849
|
+
: undefined;
|
|
1850
|
+
if (stateDescription === undefined) {
|
|
1851
|
+
throw new Error(
|
|
1852
|
+
'decide runtime: completed actor has no authored final-state description',
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1664
1855
|
return {
|
|
1665
1856
|
outcome: 'terminal',
|
|
1666
1857
|
state,
|
|
1858
|
+
stateDescription,
|
|
1667
1859
|
...(output === undefined ? {} : { output }),
|
|
1668
1860
|
};
|
|
1669
1861
|
}
|
|
1670
|
-
if (
|
|
1671
|
-
|
|
1672
|
-
(snapshot as { error?: unknown }).error ??
|
|
1673
|
-
new Error('decide runtime actor entered error status')
|
|
1674
|
-
);
|
|
1862
|
+
if (signal?.aborted) {
|
|
1863
|
+
return abortedResult(signal);
|
|
1675
1864
|
}
|
|
1676
1865
|
if (state.activeStateIds.includes('failed')) {
|
|
1677
1866
|
const error = normalizeErrorFull(context.lastError);
|
|
@@ -1741,6 +1930,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1741
1930
|
judgeQueue.clear();
|
|
1742
1931
|
actor = undefined;
|
|
1743
1932
|
currentSignal = undefined;
|
|
1933
|
+
currentAborts = undefined;
|
|
1934
|
+
actorSettlementAborts.length = 0;
|
|
1935
|
+
actorSettlementErrorAborts = undefined;
|
|
1744
1936
|
currentTurnId = undefined;
|
|
1745
1937
|
ports = undefined;
|
|
1746
1938
|
sessionIdentity = undefined;
|
|
@@ -1866,7 +2058,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1866
2058
|
playbookCall: playbookCallSequence,
|
|
1867
2059
|
},
|
|
1868
2060
|
state,
|
|
1869
|
-
pendingBossQuestions:
|
|
2061
|
+
pendingBossQuestions: pendingQuestionsForState(state, context).map(
|
|
1870
2062
|
(pending) => ({
|
|
1871
2063
|
questionId: pending.questionId,
|
|
1872
2064
|
asker: pending.asker,
|
|
@@ -1993,12 +2185,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
1993
2185
|
const turnId = ++turnSequence;
|
|
1994
2186
|
currentTurnId = turnId;
|
|
1995
2187
|
currentSignal = turn.signal;
|
|
2188
|
+
currentAborts = abortReasonClassifier(turn.signal);
|
|
1996
2189
|
controlPlaneError = undefined;
|
|
1997
2190
|
let result: PlaybookRunResult = resultForSnapshot(turn.signal);
|
|
1998
2191
|
let settlement: unknown = result;
|
|
1999
2192
|
const failures: unknown[] = [];
|
|
2000
2193
|
try {
|
|
2001
2194
|
await emitTrace('boss.input.received', { text: turn.text }, { turnId });
|
|
2195
|
+
// A boundary entered aborted records the attempted input, then refuses
|
|
2196
|
+
// delivery before deterministic mapping or the classifier can perform
|
|
2197
|
+
// any host-visible work (DR-036 §5).
|
|
2198
|
+
turn.signal.throwIfAborted();
|
|
2002
2199
|
if (turn.text.trim().length === 0) {
|
|
2003
2200
|
const state = currentState();
|
|
2004
2201
|
result = { outcome: 'no-action', state };
|
|
@@ -2031,15 +2228,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2031
2228
|
};
|
|
2032
2229
|
} catch (error) {
|
|
2033
2230
|
const primaryError = controlPlaneError;
|
|
2231
|
+
// Only a rejection that is the exact abort reason settles as the
|
|
2232
|
+
// cancellation; a distinct failure observed while the signal is
|
|
2233
|
+
// aborted remains a control error (slc/link.md §Abort).
|
|
2034
2234
|
if (primaryError !== undefined) {
|
|
2035
2235
|
collectFailure(failures, primaryError);
|
|
2036
|
-
} else if (!turn.signal
|
|
2236
|
+
} else if (!isAbortFailure(error, turn.signal)) {
|
|
2037
2237
|
collectFailure(failures, error);
|
|
2038
2238
|
}
|
|
2039
2239
|
const state = currentState();
|
|
2040
2240
|
const effectiveError = primaryError ?? error;
|
|
2041
2241
|
result =
|
|
2042
|
-
turn.signal
|
|
2242
|
+
isAbortFailure(error, turn.signal) && primaryError === undefined
|
|
2043
2243
|
? resultForSnapshot(turn.signal)
|
|
2044
2244
|
: {
|
|
2045
2245
|
outcome: 'failed',
|
|
@@ -2060,13 +2260,13 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2060
2260
|
} catch (error) {
|
|
2061
2261
|
const primaryError = controlPlaneError;
|
|
2062
2262
|
const effectiveError = primaryError ?? error;
|
|
2063
|
-
|
|
2263
|
+
// A drain rejection that is the exact abort reason evidences the
|
|
2264
|
+
// cancellation, not a control-plane failure (slc/link.md §Abort).
|
|
2265
|
+
const drainAborted = isAbortFailure(effectiveError, turn.signal);
|
|
2266
|
+
if (!drainAborted) collectFailure(failures, effectiveError);
|
|
2064
2267
|
const state = currentState();
|
|
2065
2268
|
result = {
|
|
2066
|
-
outcome:
|
|
2067
|
-
turn.signal.aborted && primaryError === undefined
|
|
2068
|
-
? 'aborted'
|
|
2069
|
-
: 'failed',
|
|
2269
|
+
outcome: drainAborted ? 'aborted' : 'failed',
|
|
2070
2270
|
state,
|
|
2071
2271
|
error: normalizeErrorFull(effectiveError) ?? {
|
|
2072
2272
|
name: 'Error',
|
|
@@ -2075,18 +2275,28 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2075
2275
|
};
|
|
2076
2276
|
settlement = { ...result, ...stateIdentity(state) };
|
|
2077
2277
|
}
|
|
2078
|
-
currentSignal = undefined;
|
|
2079
2278
|
try {
|
|
2080
2279
|
await emitTrace('boss.input.settled', settlement, { turnId });
|
|
2081
2280
|
} catch (error) {
|
|
2082
|
-
|
|
2281
|
+
// A settlement-trace rejection that is the exact abort reason also
|
|
2282
|
+
// evidences the cancellation (slc/link.md §Abort).
|
|
2283
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
2284
|
+
collectFailure(failures, error);
|
|
2285
|
+
}
|
|
2083
2286
|
}
|
|
2084
2287
|
try {
|
|
2085
2288
|
await flush();
|
|
2086
2289
|
} catch (error) {
|
|
2087
|
-
|
|
2290
|
+
// A late flush rejection that is the exact abort reason likewise
|
|
2291
|
+
// evidences the cancellation; the settled result already labels
|
|
2292
|
+
// the turn aborted then (slc/link.md §Abort).
|
|
2293
|
+
if (!isAbortFailure(error, turn.signal)) {
|
|
2294
|
+
collectFailure(failures, error);
|
|
2295
|
+
}
|
|
2088
2296
|
} finally {
|
|
2089
2297
|
const primaryError = controlPlaneError;
|
|
2298
|
+
currentSignal = undefined;
|
|
2299
|
+
currentAborts = undefined;
|
|
2090
2300
|
currentTurnId = undefined;
|
|
2091
2301
|
controlPlaneError = undefined;
|
|
2092
2302
|
if (primaryError !== undefined) throw primaryError;
|
|
@@ -2122,6 +2332,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2122
2332
|
}
|
|
2123
2333
|
currentTurnId = playbookCallTurnIds.get(callId);
|
|
2124
2334
|
currentSignal = signal;
|
|
2335
|
+
currentAborts = abortReasonClassifier(signal);
|
|
2125
2336
|
controlPlaneError = undefined;
|
|
2126
2337
|
let runResult: PlaybookRunResult | undefined;
|
|
2127
2338
|
let operationError: unknown;
|
|
@@ -2148,13 +2359,56 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2148
2359
|
} catch (error) {
|
|
2149
2360
|
drainError = error;
|
|
2150
2361
|
}
|
|
2151
|
-
const
|
|
2362
|
+
const aborts = currentAborts ?? abortReasonClassifier(signal);
|
|
2363
|
+
// The control latch has already classified its failure as distinct
|
|
2364
|
+
// under the operation that owned it. Only still-unclassified drain and
|
|
2365
|
+
// operation candidates may be cancellation evidence for this resume.
|
|
2366
|
+
const controlFailure = controlPlaneError;
|
|
2367
|
+
const drainAbort =
|
|
2368
|
+
controlFailure === undefined &&
|
|
2369
|
+
drainError !== undefined &&
|
|
2370
|
+
aborts.isAbortReason(drainError);
|
|
2371
|
+
const operationAbort =
|
|
2372
|
+
controlFailure === undefined &&
|
|
2373
|
+
operationError !== undefined &&
|
|
2374
|
+
aborts.isAbortReason(operationError);
|
|
2375
|
+
const abortEvidence =
|
|
2376
|
+
(drainAbort ? drainError : undefined) ??
|
|
2377
|
+
(operationAbort ? operationError : undefined);
|
|
2378
|
+
const failure =
|
|
2379
|
+
controlFailure ??
|
|
2380
|
+
(drainAbort ? undefined : drainError) ??
|
|
2381
|
+
(operationAbort ? undefined : operationError);
|
|
2152
2382
|
currentSignal = undefined;
|
|
2383
|
+
currentAborts = undefined;
|
|
2153
2384
|
currentTurnId = undefined;
|
|
2154
2385
|
controlPlaneError = undefined;
|
|
2155
2386
|
if (failure !== undefined) throw failure;
|
|
2387
|
+
if (
|
|
2388
|
+
abortEvidence !== undefined &&
|
|
2389
|
+
runResult?.outcome !== 'terminal' &&
|
|
2390
|
+
runResult?.outcome !== 'suspended'
|
|
2391
|
+
) {
|
|
2392
|
+
const state = currentState();
|
|
2393
|
+
runResult = {
|
|
2394
|
+
outcome: 'aborted',
|
|
2395
|
+
state,
|
|
2396
|
+
error: normalizeErrorFull(abortEvidence) ?? {
|
|
2397
|
+
name: 'AbortError',
|
|
2398
|
+
message: String(abortEvidence),
|
|
2399
|
+
},
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2156
2402
|
if (runResult === undefined) {
|
|
2157
|
-
|
|
2403
|
+
if (signal.aborted) {
|
|
2404
|
+
// Every candidate was the abort's own evidence: settle on the
|
|
2405
|
+
// machine's state under the aborted boundary signal (DR-036 §4).
|
|
2406
|
+
runResult = resultForSnapshot(signal);
|
|
2407
|
+
} else {
|
|
2408
|
+
throw new Error(
|
|
2409
|
+
'decide runtime: playbook resume produced no result',
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2158
2412
|
}
|
|
2159
2413
|
return runResult;
|
|
2160
2414
|
},
|
|
@@ -2210,6 +2464,9 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2210
2464
|
judgeQueue.clear();
|
|
2211
2465
|
actor = undefined;
|
|
2212
2466
|
currentSignal = undefined;
|
|
2467
|
+
currentAborts = undefined;
|
|
2468
|
+
actorSettlementAborts.length = 0;
|
|
2469
|
+
actorSettlementErrorAborts = undefined;
|
|
2213
2470
|
currentTurnId = undefined;
|
|
2214
2471
|
ports = undefined;
|
|
2215
2472
|
sessionIdentity = undefined;
|
|
@@ -2224,6 +2481,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
|
|
|
2224
2481
|
})();
|
|
2225
2482
|
return disposalPromise;
|
|
2226
2483
|
},
|
|
2484
|
+
|
|
2485
|
+
// @internal — test-only parity with the shared factory's bridge escape
|
|
2486
|
+
// hatch. This is hidden by the PlaybookRuntime return type.
|
|
2487
|
+
_getNestedBridge() {
|
|
2488
|
+
return nestedBridge;
|
|
2489
|
+
},
|
|
2227
2490
|
};
|
|
2228
2491
|
};
|
|
2229
2492
|
|
|
@@ -2237,6 +2500,7 @@ export const _internal = {
|
|
|
2237
2500
|
parseAdjudication,
|
|
2238
2501
|
combineSignals,
|
|
2239
2502
|
pendingQuestionsFromContext,
|
|
2503
|
+
pendingQuestionsForState,
|
|
2240
2504
|
normalizeErrorCompact,
|
|
2241
2505
|
normalizeErrorFull,
|
|
2242
2506
|
STATE_DESCRIPTIONS,
|