@sublang/playbook 15.0.0 → 15.1.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 +1 -1
- package/docs/embedding.md +7 -5
- package/package.json +3 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -9
- package/reference/sdlc/decide.playbook/decide.playbook.js +158 -32
- package/reference/sdlc/decide.playbook/decide.playbook.ts +229 -46
- package/slc/prefix-prompts.mjs +190 -0
- package/slc/prefix.md +200 -0
- package/slc/slc.pin-inputs.json +1 -0
- package/src/xstate-playbook-runtime.d.ts +35 -1
- package/src/xstate-playbook-runtime.js +82 -47
- package/src/xstate-playbook-runtime.ts +107 -60
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
|
@@ -28,18 +28,27 @@ import {
|
|
|
28
28
|
} from '../../../src/accepted-outcome.js';
|
|
29
29
|
|
|
30
30
|
import {
|
|
31
|
+
ABORTED_FAILURE_CAUSE,
|
|
31
32
|
assertJsonSafe,
|
|
32
33
|
assertPlaybookEffectLedger,
|
|
33
34
|
assertPlaybookRuntimeSnapshot,
|
|
35
|
+
attachPlaybookFailureCause,
|
|
34
36
|
combineAbortSignals,
|
|
35
37
|
composePlayerContinuation,
|
|
38
|
+
createFailureCauseRetention,
|
|
36
39
|
createNestedPlaybookBridge,
|
|
37
40
|
detachPersistedMachineSnapshot,
|
|
41
|
+
effectAuthorizedPreExistingBlock,
|
|
42
|
+
governedSettlementCause,
|
|
38
43
|
normalizeError,
|
|
44
|
+
normalizeErrorCompact,
|
|
45
|
+
normalizeErrorFull,
|
|
39
46
|
normalizePlaybookSnapshot,
|
|
40
47
|
PlaybookSemanticCandidateStructureError,
|
|
48
|
+
playerFailureCause,
|
|
41
49
|
reconcilePlaybookSemanticEvidence,
|
|
42
50
|
renderGovernedOutcomeContract,
|
|
51
|
+
runtimeDefectCause,
|
|
43
52
|
snapshotJsonValue,
|
|
44
53
|
snapshotPlaybookSession,
|
|
45
54
|
terminalOutcomesFromMachine,
|
|
@@ -80,6 +89,7 @@ import type {
|
|
|
80
89
|
PlaybookEffectBoundaryStart,
|
|
81
90
|
PlaybookEffectLedger,
|
|
82
91
|
PlaybookEffectLedgerCapability,
|
|
92
|
+
PlaybookFailureCause,
|
|
83
93
|
PlaybookPendingBossQuestion,
|
|
84
94
|
PlaybookPendingCall,
|
|
85
95
|
PlaybookPorts,
|
|
@@ -699,20 +709,6 @@ function combineSignals(
|
|
|
699
709
|
return combineAbortSignals(a, b);
|
|
700
710
|
}
|
|
701
711
|
|
|
702
|
-
function normalizeErrorCompact(
|
|
703
|
-
err: unknown,
|
|
704
|
-
): { name: string; message: string } | undefined {
|
|
705
|
-
if (err === undefined || err === null) return undefined;
|
|
706
|
-
const normalized = normalizeError(err);
|
|
707
|
-
return { name: normalized.name, message: normalized.message };
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function normalizeErrorFull(
|
|
711
|
-
err: unknown,
|
|
712
|
-
): { name: string; message: string; stack?: string } | undefined {
|
|
713
|
-
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
712
|
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
717
713
|
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
718
714
|
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
@@ -1240,6 +1236,7 @@ function telemetryPayload(
|
|
|
1240
1236
|
event: unknown,
|
|
1241
1237
|
context: Record<string, unknown>,
|
|
1242
1238
|
hiddenQuestionId?: string,
|
|
1239
|
+
failedError?: NormalizedError,
|
|
1243
1240
|
): JsonValue {
|
|
1244
1241
|
const pendingBossQuestions = pendingQuestionsForState(
|
|
1245
1242
|
state,
|
|
@@ -1254,7 +1251,7 @@ function telemetryPayload(
|
|
|
1254
1251
|
state,
|
|
1255
1252
|
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
1256
1253
|
...(state.activeStateIds.includes('failed')
|
|
1257
|
-
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
1254
|
+
? { lastError: failedError ?? normalizeErrorFull(context.lastError) ?? null }
|
|
1258
1255
|
: {}),
|
|
1259
1256
|
} satisfies Record<string, unknown>;
|
|
1260
1257
|
assertJsonSafe(payload);
|
|
@@ -1278,6 +1275,7 @@ interface ActiveDeferredContinuation {
|
|
|
1278
1275
|
readonly result: DeferredValue<PlayerResult>;
|
|
1279
1276
|
readonly acknowledged: DeferredValue<PlayerResult>;
|
|
1280
1277
|
playerContinuation?: string | false;
|
|
1278
|
+
baseline?: PlaybookRepositoryReceipt['baseline'];
|
|
1281
1279
|
input?: PlayerInput;
|
|
1282
1280
|
playerId?: string;
|
|
1283
1281
|
}
|
|
@@ -1350,6 +1348,85 @@ function createDecidePlaybookRuntime(
|
|
|
1350
1348
|
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
1351
1349
|
const governedOutputsByBoundaryId = new Map<string, PlayerOutput>();
|
|
1352
1350
|
const governedFailuresByBoundaryId = new Map<string, Error>();
|
|
1351
|
+
// DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
1352
|
+
// marker — a string, a frozen error — kept by identity for as long as the
|
|
1353
|
+
// failure stands, so every surface that reads the FSM's own `lastError`
|
|
1354
|
+
// publishes the cause decided for exactly that value (mirrors the shared
|
|
1355
|
+
// factory).
|
|
1356
|
+
const failureCauses = createFailureCauseRetention();
|
|
1357
|
+
// DR-063 §2: each decided failure is one occurrence in decision order, kept
|
|
1358
|
+
// by the player call or governed boundary it was decided for, so a cohort
|
|
1359
|
+
// names the member that failed first whatever value its siblings threw.
|
|
1360
|
+
type FailureOccurrence = {
|
|
1361
|
+
readonly cause: PlaybookFailureCause;
|
|
1362
|
+
readonly sequence: number;
|
|
1363
|
+
};
|
|
1364
|
+
let failureSequence = 0;
|
|
1365
|
+
const playerCallFailures = new Map<string, FailureOccurrence>();
|
|
1366
|
+
const governedFailureOccurrences = new Map<string, FailureOccurrence>();
|
|
1367
|
+
const recordFailure = (
|
|
1368
|
+
occurrences: Map<string, FailureOccurrence>,
|
|
1369
|
+
key: string,
|
|
1370
|
+
cause: PlaybookFailureCause,
|
|
1371
|
+
): void => {
|
|
1372
|
+
occurrences.set(key, { cause, sequence: ++failureSequence });
|
|
1373
|
+
};
|
|
1374
|
+
const failedStateError = (
|
|
1375
|
+
context: Record<string, unknown>,
|
|
1376
|
+
): NormalizedError | undefined => {
|
|
1377
|
+
const normalized = normalizeErrorFull(context.lastError);
|
|
1378
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
1379
|
+
return normalized;
|
|
1380
|
+
}
|
|
1381
|
+
return {
|
|
1382
|
+
...normalized,
|
|
1383
|
+
cause:
|
|
1384
|
+
failureCauses.causeOf(context.lastError) ??
|
|
1385
|
+
runtimeDefectCause(normalized.message),
|
|
1386
|
+
};
|
|
1387
|
+
};
|
|
1388
|
+
const playerResultFailureCause = (
|
|
1389
|
+
roleId: RoleId,
|
|
1390
|
+
playerId: string | undefined,
|
|
1391
|
+
result: PlayerResult,
|
|
1392
|
+
): PlaybookFailureCause =>
|
|
1393
|
+
result.status === 'aborted'
|
|
1394
|
+
? ABORTED_FAILURE_CAUSE
|
|
1395
|
+
: playerFailureCause({
|
|
1396
|
+
roleId,
|
|
1397
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
1398
|
+
error:
|
|
1399
|
+
result.error ??
|
|
1400
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
1401
|
+
});
|
|
1402
|
+
// A rejected player port or a result the runtime cannot read is a
|
|
1403
|
+
// `player-failed` failure named by role; the turn's own abort is `aborted`.
|
|
1404
|
+
// A cancellation the runtime itself issued — a proposal cancelled because
|
|
1405
|
+
// its sibling failed — is not this player's failure and decides nothing, so
|
|
1406
|
+
// the originator's cause stands; every other failure is decided afresh and
|
|
1407
|
+
// recorded as this call's occurrence, whatever an earlier failure threw.
|
|
1408
|
+
const decidePlayerCallFailure = (
|
|
1409
|
+
error: unknown,
|
|
1410
|
+
roleId: RoleId,
|
|
1411
|
+
playerId: string | undefined,
|
|
1412
|
+
signal: AbortSignal,
|
|
1413
|
+
callId: string,
|
|
1414
|
+
): void => {
|
|
1415
|
+
const turn = currentSignal;
|
|
1416
|
+
const turnAborted = turn !== undefined && turn.aborted;
|
|
1417
|
+
if (!turnAborted && signal.aborted) return;
|
|
1418
|
+
const cause =
|
|
1419
|
+
turn !== undefined && turn.aborted && Object.is(error, turn.reason)
|
|
1420
|
+
? ABORTED_FAILURE_CAUSE
|
|
1421
|
+
: playerFailureCause({
|
|
1422
|
+
roleId,
|
|
1423
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
1424
|
+
error,
|
|
1425
|
+
});
|
|
1426
|
+
attachPlaybookFailureCause(error, cause);
|
|
1427
|
+
failureCauses.retain(error, cause);
|
|
1428
|
+
recordFailure(playerCallFailures, callId, cause);
|
|
1429
|
+
};
|
|
1353
1430
|
const governedEvidenceByBoundaryId = new Map<
|
|
1354
1431
|
string,
|
|
1355
1432
|
{ readonly finalText: string; readonly semanticCandidate?: JsonValue }
|
|
@@ -2154,6 +2231,7 @@ function createDecidePlaybookRuntime(
|
|
|
2154
2231
|
continuation?: {
|
|
2155
2232
|
readonly resume?: string | false;
|
|
2156
2233
|
readonly callId: string;
|
|
2234
|
+
readonly preExistingBlock?: string;
|
|
2157
2235
|
},
|
|
2158
2236
|
): Promise<DeferredContinuationEnvelope> => {
|
|
2159
2237
|
const aborts = abortReasonClassifier(signal);
|
|
@@ -2165,7 +2243,13 @@ function createDecidePlaybookRuntime(
|
|
|
2165
2243
|
const roleId = input.role;
|
|
2166
2244
|
const playerId = resolvedPlayerId(roleId);
|
|
2167
2245
|
const playerKey = continuationKey(roleId, playerId);
|
|
2168
|
-
|
|
2246
|
+
// DR-062 §4: composed from this call's own baseline observation, so the
|
|
2247
|
+
// prompt the trace records is the prompt the player is sent.
|
|
2248
|
+
const withPreExisting = (text: string): string =>
|
|
2249
|
+
continuation?.preExistingBlock === undefined
|
|
2250
|
+
? text
|
|
2251
|
+
: `${text}\n\n${continuation.preExistingBlock}`;
|
|
2252
|
+
const freshPrompt = withPreExisting(composeInvocationPrompt(input));
|
|
2169
2253
|
let resume: PlayerCallOptions['resume'];
|
|
2170
2254
|
try {
|
|
2171
2255
|
signal.throwIfAborted();
|
|
@@ -2178,7 +2262,10 @@ function createDecidePlaybookRuntime(
|
|
|
2178
2262
|
latchControlPlaneError(error, signal);
|
|
2179
2263
|
throw error;
|
|
2180
2264
|
}
|
|
2181
|
-
const prompt =
|
|
2265
|
+
const prompt =
|
|
2266
|
+
resume === false
|
|
2267
|
+
? freshPrompt
|
|
2268
|
+
: withPreExisting(composeInvocationPrompt(input, true));
|
|
2182
2269
|
const callId =
|
|
2183
2270
|
continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
2184
2271
|
const identity = {
|
|
@@ -2244,6 +2331,7 @@ function createDecidePlaybookRuntime(
|
|
|
2244
2331
|
// A rejected call produced no authoritative result, so the previous
|
|
2245
2332
|
// token remains untouched. This also covers a coder promise that
|
|
2246
2333
|
// resolves after its invocation signal was cancelled.
|
|
2334
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
2247
2335
|
latchControlPlaneError(error, signal);
|
|
2248
2336
|
try {
|
|
2249
2337
|
await emitFailure(error);
|
|
@@ -2257,6 +2345,7 @@ function createDecidePlaybookRuntime(
|
|
|
2257
2345
|
try {
|
|
2258
2346
|
result = validatePlayerResult(rawResult);
|
|
2259
2347
|
} catch (error) {
|
|
2348
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
2260
2349
|
latchControlPlaneError(error, signal);
|
|
2261
2350
|
try {
|
|
2262
2351
|
await emitFailure(error);
|
|
@@ -2348,17 +2437,29 @@ function createDecidePlaybookRuntime(
|
|
|
2348
2437
|
};
|
|
2349
2438
|
};
|
|
2350
2439
|
|
|
2440
|
+
// DR-063 §1/§2: the cause of an unresolved governed settlement is decided
|
|
2441
|
+
// here — a reconciled mismatch supplies its receipt-read cause, an abort or
|
|
2442
|
+
// a refused adjudication its own code, anything else a runtime defect — and
|
|
2443
|
+
// rides the failure the boundary throws.
|
|
2351
2444
|
const unresolvedGovernedEvidence = (
|
|
2352
2445
|
boundaryId: string,
|
|
2353
2446
|
reason: string,
|
|
2354
2447
|
error?: unknown,
|
|
2448
|
+
cause?: PlaybookFailureCause,
|
|
2355
2449
|
): void => {
|
|
2356
|
-
|
|
2357
|
-
|
|
2450
|
+
const aborted =
|
|
2451
|
+
error !== undefined &&
|
|
2452
|
+
currentSignal?.aborted === true &&
|
|
2453
|
+
Object.is(error, currentSignal.reason);
|
|
2454
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
2455
|
+
const failure =
|
|
2358
2456
|
error instanceof Error
|
|
2359
2457
|
? error
|
|
2360
|
-
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`)
|
|
2361
|
-
);
|
|
2458
|
+
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`);
|
|
2459
|
+
attachPlaybookFailureCause(failure, decided);
|
|
2460
|
+
failureCauses.retain(failure, decided);
|
|
2461
|
+
recordFailure(governedFailureOccurrences, boundaryId, decided);
|
|
2462
|
+
governedFailuresByBoundaryId.set(boundaryId, failure);
|
|
2362
2463
|
};
|
|
2363
2464
|
|
|
2364
2465
|
const spendSemanticCorrectionBudget = async (
|
|
@@ -2706,6 +2807,8 @@ function createDecidePlaybookRuntime(
|
|
|
2706
2807
|
unresolvedGovernedEvidence(
|
|
2707
2808
|
boundary.boundaryId,
|
|
2708
2809
|
reconciliation.reason,
|
|
2810
|
+
undefined,
|
|
2811
|
+
reconciliation.cause,
|
|
2709
2812
|
);
|
|
2710
2813
|
return { finalText, semanticCandidate, unresolved: true as const };
|
|
2711
2814
|
}
|
|
@@ -2952,11 +3055,23 @@ function createDecidePlaybookRuntime(
|
|
|
2952
3055
|
envelope.result.status !== 'ok' &&
|
|
2953
3056
|
!cohortOperations.signal.aborted
|
|
2954
3057
|
) {
|
|
2955
|
-
|
|
3058
|
+
// DR-063 §2: the cancellation carries the cause of the result
|
|
3059
|
+
// that caused it, recorded as this member's occurrence, so the
|
|
3060
|
+
// cohort fails for this member.
|
|
3061
|
+
const cause = playerResultFailureCause(
|
|
3062
|
+
roleId,
|
|
3063
|
+
resolvedPlayerId(roleId),
|
|
3064
|
+
envelope.result,
|
|
3065
|
+
);
|
|
3066
|
+
const cancellation = attachPlaybookFailureCause(
|
|
2956
3067
|
new Error(
|
|
2957
3068
|
`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`,
|
|
2958
3069
|
),
|
|
3070
|
+
cause,
|
|
2959
3071
|
);
|
|
3072
|
+
failureCauses.retain(cancellation, cause);
|
|
3073
|
+
recordFailure(playerCallFailures, member.callId, cause);
|
|
3074
|
+
cohortOperations.abort(cancellation);
|
|
2960
3075
|
}
|
|
2961
3076
|
return envelope.result;
|
|
2962
3077
|
} catch (error) {
|
|
@@ -3037,12 +3152,43 @@ function createDecidePlaybookRuntime(
|
|
|
3037
3152
|
for (const result of acknowledgedResults.values()) {
|
|
3038
3153
|
governedPlayerOutputs.delete(result);
|
|
3039
3154
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3155
|
+
const failure =
|
|
3156
|
+
acknowledgementFailures.length === 1
|
|
3157
|
+
? acknowledgementFailures[0]
|
|
3158
|
+
: new AggregateError(
|
|
3159
|
+
acknowledgementFailures,
|
|
3160
|
+
'DECIDE proposal cohort reconciliation failed',
|
|
3161
|
+
);
|
|
3162
|
+
// DR-063 §2: a cohort fails for the member whose failure was decided
|
|
3163
|
+
// first — a rejected or non-`ok` player call, a refused adjudication,
|
|
3164
|
+
// a mismatched receipt — in decision order, not in the order the
|
|
3165
|
+
// players finished or the values they threw. That cause rides every
|
|
3166
|
+
// error the failure can surface as: the cancellation, each member's
|
|
3167
|
+
// own rejection, and the aggregate of both, never an abort.
|
|
3168
|
+
const origin = cohortOperations.signal.aborted
|
|
3169
|
+
? cohortOperations.signal.reason
|
|
3170
|
+
: undefined;
|
|
3171
|
+
const first = ROLE_IDS.flatMap((roleId) => {
|
|
3172
|
+
const member = members[roleId];
|
|
3173
|
+
const occurrence =
|
|
3174
|
+
member === undefined
|
|
3175
|
+
? undefined
|
|
3176
|
+
: (playerCallFailures.get(member.callId) ??
|
|
3177
|
+
governedFailureOccurrences.get(member.effectBoundary.boundaryId));
|
|
3178
|
+
return occurrence === undefined ? [] : [occurrence];
|
|
3179
|
+
}).sort((left, right) => left.sequence - right.sequence)[0];
|
|
3180
|
+
const originCause =
|
|
3181
|
+
first?.cause ??
|
|
3182
|
+
failureCauses.causeOf(failure) ??
|
|
3183
|
+
(origin === undefined ? undefined : failureCauses.causeOf(origin));
|
|
3184
|
+
if (originCause !== undefined) {
|
|
3185
|
+
for (const candidate of [failure, origin, ...acknowledgementFailures]) {
|
|
3186
|
+
if (candidate === undefined) continue;
|
|
3187
|
+
attachPlaybookFailureCause(candidate, originCause);
|
|
3188
|
+
failureCauses.retain(candidate, originCause);
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
throw failure;
|
|
3046
3192
|
}
|
|
3047
3193
|
for (const roleId of releaseOrder) {
|
|
3048
3194
|
const member = members[roleId]!;
|
|
@@ -3120,6 +3266,10 @@ function createDecidePlaybookRuntime(
|
|
|
3120
3266
|
const envelope = await runPlayerCall(input, signal, {
|
|
3121
3267
|
callId: active.effectBoundary.callId,
|
|
3122
3268
|
resume: active.playerContinuation,
|
|
3269
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(
|
|
3270
|
+
active.effectBoundary,
|
|
3271
|
+
active.baseline,
|
|
3272
|
+
),
|
|
3123
3273
|
});
|
|
3124
3274
|
active.result.resolve(envelope.result);
|
|
3125
3275
|
const acknowledgedResult = await active.acknowledged.promise;
|
|
@@ -3145,8 +3295,16 @@ function createDecidePlaybookRuntime(
|
|
|
3145
3295
|
const settled = await deferredEffects.repository.runExclusive({
|
|
3146
3296
|
signal,
|
|
3147
3297
|
effectBoundary,
|
|
3148
|
-
|
|
3149
|
-
|
|
3298
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
3299
|
+
// captured it under this exclusive claim.
|
|
3300
|
+
operation: async (context) => {
|
|
3301
|
+
envelope = await runPlayerCall(input, signal, {
|
|
3302
|
+
callId,
|
|
3303
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(
|
|
3304
|
+
effectBoundary,
|
|
3305
|
+
context?.baseline,
|
|
3306
|
+
),
|
|
3307
|
+
});
|
|
3150
3308
|
return envelope.result;
|
|
3151
3309
|
},
|
|
3152
3310
|
completeEffectBoundary: completionEvidenceFor(
|
|
@@ -3214,12 +3372,18 @@ function createDecidePlaybookRuntime(
|
|
|
3214
3372
|
));
|
|
3215
3373
|
}
|
|
3216
3374
|
if (result.status !== 'ok') {
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3375
|
+
// DR-063 §2: the failure Error built for a non-`ok` result carries
|
|
3376
|
+
// the cause decided for that exact result.
|
|
3377
|
+
const cause = playerResultFailureCause(roleId, playerId, result);
|
|
3378
|
+
throw attachPlaybookFailureCause(
|
|
3379
|
+
new Error(
|
|
3380
|
+
`${roleLabel(roleId)}${
|
|
3381
|
+
playerId === undefined ? '' : ` (${playerId})`
|
|
3382
|
+
} returned status "${result.status}"${
|
|
3383
|
+
result.error ? `: ${result.error}` : ''
|
|
3384
|
+
}`,
|
|
3385
|
+
),
|
|
3386
|
+
cause,
|
|
3223
3387
|
);
|
|
3224
3388
|
}
|
|
3225
3389
|
const finalText = result.finalText ?? '';
|
|
@@ -3365,6 +3529,9 @@ function createDecidePlaybookRuntime(
|
|
|
3365
3529
|
hiddenDeferredOperationId === undefined
|
|
3366
3530
|
? undefined
|
|
3367
3531
|
: 'commitCoderProposal',
|
|
3532
|
+
state.activeStateIds.includes('failed')
|
|
3533
|
+
? failedStateError(context)
|
|
3534
|
+
: undefined,
|
|
3368
3535
|
);
|
|
3369
3536
|
const describedFsmPayload = snapshotJsonValue(
|
|
3370
3537
|
fsmPayload,
|
|
@@ -3444,7 +3611,7 @@ function createDecidePlaybookRuntime(
|
|
|
3444
3611
|
}
|
|
3445
3612
|
const lastError =
|
|
3446
3613
|
activeStateId === 'failed'
|
|
3447
|
-
? normalizeErrorCompact(context
|
|
3614
|
+
? normalizeErrorCompact(failedStateError(context))
|
|
3448
3615
|
: undefined;
|
|
3449
3616
|
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
3450
3617
|
if (description === undefined) continue;
|
|
@@ -3456,7 +3623,9 @@ function createDecidePlaybookRuntime(
|
|
|
3456
3623
|
? '◆ workflow failed; awaiting Boss recovery.'
|
|
3457
3624
|
: `⤷ ${roleLabel(roleState.role)}: ${description}`,
|
|
3458
3625
|
activeStateId,
|
|
3459
|
-
lastError === undefined
|
|
3626
|
+
lastError === undefined
|
|
3627
|
+
? undefined
|
|
3628
|
+
: snapshotJsonValue({ lastError }, 'DECIDE failed status data'),
|
|
3460
3629
|
);
|
|
3461
3630
|
}
|
|
3462
3631
|
} catch (error) {
|
|
@@ -3640,7 +3809,7 @@ function createDecidePlaybookRuntime(
|
|
|
3640
3809
|
signal,
|
|
3641
3810
|
operationId,
|
|
3642
3811
|
effectBoundary,
|
|
3643
|
-
operation: async ({ playerContinuation }) => {
|
|
3812
|
+
operation: async ({ playerContinuation, baseline }) => {
|
|
3644
3813
|
if (
|
|
3645
3814
|
playerContinuation !== false &&
|
|
3646
3815
|
(typeof playerContinuation !== 'string' ||
|
|
@@ -3651,6 +3820,8 @@ function createDecidePlaybookRuntime(
|
|
|
3651
3820
|
);
|
|
3652
3821
|
}
|
|
3653
3822
|
active.playerContinuation = playerContinuation;
|
|
3823
|
+
// DR-062 §4: the continuation's own baseline.
|
|
3824
|
+
active.baseline = baseline;
|
|
3654
3825
|
readiness.resolve({ status: 'ready' });
|
|
3655
3826
|
return active.result.promise;
|
|
3656
3827
|
},
|
|
@@ -3957,7 +4128,10 @@ function createDecidePlaybookRuntime(
|
|
|
3957
4128
|
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
3958
4129
|
...action,
|
|
3959
4130
|
}));
|
|
3960
|
-
const lastError =
|
|
4131
|
+
const lastError =
|
|
4132
|
+
state.stateId === 'failed'
|
|
4133
|
+
? failedStateError(context)
|
|
4134
|
+
: normalizeErrorFull(context.lastError);
|
|
3961
4135
|
const stateDescription =
|
|
3962
4136
|
unresolved || state.stateId === undefined
|
|
3963
4137
|
? undefined
|
|
@@ -4035,6 +4209,8 @@ function createDecidePlaybookRuntime(
|
|
|
4035
4209
|
const turnId = ++turnSequence;
|
|
4036
4210
|
const callId = `apply-${++applyCallSequence}`;
|
|
4037
4211
|
currentTurnId = turnId;
|
|
4212
|
+
playerCallFailures.clear();
|
|
4213
|
+
governedFailureOccurrences.clear();
|
|
4038
4214
|
currentSignal = signal;
|
|
4039
4215
|
currentAborts = abortReasonClassifier(signal);
|
|
4040
4216
|
controlPlaneError = undefined;
|
|
@@ -4306,7 +4482,7 @@ function createDecidePlaybookRuntime(
|
|
|
4306
4482
|
return abortedResult(signal);
|
|
4307
4483
|
}
|
|
4308
4484
|
if (state.activeStateIds.includes('failed')) {
|
|
4309
|
-
const error =
|
|
4485
|
+
const error = failedStateError(context);
|
|
4310
4486
|
return {
|
|
4311
4487
|
outcome: 'failed',
|
|
4312
4488
|
state,
|
|
@@ -4504,12 +4680,17 @@ function createDecidePlaybookRuntime(
|
|
|
4504
4680
|
}
|
|
4505
4681
|
const state = currentState();
|
|
4506
4682
|
if (state.status !== 'active' || !state.quiescent) return undefined;
|
|
4507
|
-
const machine = detachPersistedMachineSnapshot(
|
|
4508
|
-
actor.getPersistedSnapshot(),
|
|
4509
|
-
);
|
|
4510
4683
|
const context = (
|
|
4511
4684
|
actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
|
|
4512
4685
|
).context as unknown as Record<string, unknown>;
|
|
4686
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
4687
|
+
// surface publishes it, completed from the runtime's own cause slot.
|
|
4688
|
+
const machine = detachPersistedMachineSnapshot(
|
|
4689
|
+
actor.getPersistedSnapshot(),
|
|
4690
|
+
state.stateId === 'failed'
|
|
4691
|
+
? { lastError: failedStateError(context) }
|
|
4692
|
+
: {},
|
|
4693
|
+
);
|
|
4513
4694
|
if (deferredEffects !== undefined) {
|
|
4514
4695
|
synchronizeDeferredProjection(readEffectLedger());
|
|
4515
4696
|
}
|
|
@@ -4678,6 +4859,8 @@ function createDecidePlaybookRuntime(
|
|
|
4678
4859
|
|
|
4679
4860
|
const turnId = ++turnSequence;
|
|
4680
4861
|
currentTurnId = turnId;
|
|
4862
|
+
playerCallFailures.clear();
|
|
4863
|
+
governedFailureOccurrences.clear();
|
|
4681
4864
|
currentSignal = turn.signal;
|
|
4682
4865
|
currentAborts = abortReasonClassifier(turn.signal);
|
|
4683
4866
|
controlPlaneError = undefined;
|
|
@@ -4806,7 +4989,7 @@ function createDecidePlaybookRuntime(
|
|
|
4806
4989
|
: {
|
|
4807
4990
|
outcome: 'failed',
|
|
4808
4991
|
state,
|
|
4809
|
-
error:
|
|
4992
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
4810
4993
|
name: 'Error',
|
|
4811
4994
|
message: String(effectiveError),
|
|
4812
4995
|
},
|
|
@@ -4830,7 +5013,7 @@ function createDecidePlaybookRuntime(
|
|
|
4830
5013
|
result = {
|
|
4831
5014
|
outcome: drainAborted ? 'aborted' : 'failed',
|
|
4832
5015
|
state,
|
|
4833
|
-
error:
|
|
5016
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
4834
5017
|
name: 'Error',
|
|
4835
5018
|
message: String(effectiveError),
|
|
4836
5019
|
},
|