@sublang/playbook 14.1.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 +2 -2
- package/docs/configuration.md +15 -9
- package/docs/embedding.md +7 -5
- package/package.json +3 -1
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
- package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/code.playbook/bin/run.js +12 -8
- package/reference/sdlc/code.playbook/code.fsm.js +1 -1
- package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
- package/reference/sdlc/code.playbook/code.gears.md +2 -2
- package/reference/sdlc/decide.md +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
- 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/reference/sdlc/review.md +1 -1
- package/reference/sdlc/review.playbook/review.fsm.js +1 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
- package/reference/sdlc/review.playbook/review.gears.md +1 -1
- 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
|
@@ -19,7 +19,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
19
19
|
import PQueue from 'p-queue';
|
|
20
20
|
import { createActor, fromPromise } from 'xstate';
|
|
21
21
|
import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
|
|
22
|
-
import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, composePlayerContinuation, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
22
|
+
import { ABORTED_FAILURE_CAUSE, assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, attachPlaybookFailureCause, combineAbortSignals, composePlayerContinuation, createFailureCauseRetention, createNestedPlaybookBridge, detachPersistedMachineSnapshot, effectAuthorizedPreExistingBlock, governedSettlementCause, normalizeError, normalizeErrorCompact, normalizeErrorFull, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, playerFailureCause, reconcilePlaybookSemanticEvidence, renderGovernedOutcomeContract, runtimeDefectCause, snapshotJsonValue, snapshotPlaybookSession, terminalOutcomesFromMachine, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
|
|
23
23
|
import decideMachine from './decide.fsm.js';
|
|
24
24
|
function snapshotDecideRuntimeOptions(value) {
|
|
25
25
|
const captured = snapshotJsonValue(value, 'DECIDE runtime options');
|
|
@@ -453,15 +453,6 @@ function parseGovernedSemanticCandidate(raw) {
|
|
|
453
453
|
function combineSignals(a, b) {
|
|
454
454
|
return combineAbortSignals(a, b);
|
|
455
455
|
}
|
|
456
|
-
function normalizeErrorCompact(err) {
|
|
457
|
-
if (err === undefined || err === null)
|
|
458
|
-
return undefined;
|
|
459
|
-
const normalized = normalizeError(err);
|
|
460
|
-
return { name: normalized.name, message: normalized.message };
|
|
461
|
-
}
|
|
462
|
-
function normalizeErrorFull(err) {
|
|
463
|
-
return err === undefined || err === null ? undefined : normalizeError(err);
|
|
464
|
-
}
|
|
465
456
|
// DR-028's unified empty predicate: a missing, empty, or whitespace-only
|
|
466
457
|
// `finalText` on an `ok` player result is the one shape that earns exactly
|
|
467
458
|
// one corrective re-ask before the existing failure path applies. Mirrors
|
|
@@ -691,7 +682,7 @@ function normalizedTransitionEvent(event) {
|
|
|
691
682
|
}
|
|
692
683
|
return snapshotJsonValue(descriptor, 'FSM event');
|
|
693
684
|
}
|
|
694
|
-
function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
|
|
685
|
+
function telemetryPayload(previousState, state, event, context, hiddenQuestionId, failedError) {
|
|
695
686
|
const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
|
|
696
687
|
const prior = previousState ?? state;
|
|
697
688
|
const payload = {
|
|
@@ -702,7 +693,7 @@ function telemetryPayload(previousState, state, event, context, hiddenQuestionId
|
|
|
702
693
|
state,
|
|
703
694
|
...(pendingBossQuestions.length > 0 ? { pendingBossQuestions } : {}),
|
|
704
695
|
...(state.activeStateIds.includes('failed')
|
|
705
|
-
? { lastError: normalizeErrorFull(context.lastError) ?? null }
|
|
696
|
+
? { lastError: failedError ?? normalizeErrorFull(context.lastError) ?? null }
|
|
706
697
|
: {}),
|
|
707
698
|
};
|
|
708
699
|
assertJsonSafe(payload);
|
|
@@ -751,6 +742,59 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
751
742
|
const semanticCompletionQueue = new PQueue({ concurrency: 1 });
|
|
752
743
|
const governedOutputsByBoundaryId = new Map();
|
|
753
744
|
const governedFailuresByBoundaryId = new Map();
|
|
745
|
+
// DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
746
|
+
// marker — a string, a frozen error — kept by identity for as long as the
|
|
747
|
+
// failure stands, so every surface that reads the FSM's own `lastError`
|
|
748
|
+
// publishes the cause decided for exactly that value (mirrors the shared
|
|
749
|
+
// factory).
|
|
750
|
+
const failureCauses = createFailureCauseRetention();
|
|
751
|
+
let failureSequence = 0;
|
|
752
|
+
const playerCallFailures = new Map();
|
|
753
|
+
const governedFailureOccurrences = new Map();
|
|
754
|
+
const recordFailure = (occurrences, key, cause) => {
|
|
755
|
+
occurrences.set(key, { cause, sequence: ++failureSequence });
|
|
756
|
+
};
|
|
757
|
+
const failedStateError = (context) => {
|
|
758
|
+
const normalized = normalizeErrorFull(context.lastError);
|
|
759
|
+
if (normalized === undefined || normalized.cause !== undefined) {
|
|
760
|
+
return normalized;
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
...normalized,
|
|
764
|
+
cause: failureCauses.causeOf(context.lastError) ??
|
|
765
|
+
runtimeDefectCause(normalized.message),
|
|
766
|
+
};
|
|
767
|
+
};
|
|
768
|
+
const playerResultFailureCause = (roleId, playerId, result) => result.status === 'aborted'
|
|
769
|
+
? ABORTED_FAILURE_CAUSE
|
|
770
|
+
: playerFailureCause({
|
|
771
|
+
roleId,
|
|
772
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
773
|
+
error: result.error ??
|
|
774
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
775
|
+
});
|
|
776
|
+
// A rejected player port or a result the runtime cannot read is a
|
|
777
|
+
// `player-failed` failure named by role; the turn's own abort is `aborted`.
|
|
778
|
+
// A cancellation the runtime itself issued — a proposal cancelled because
|
|
779
|
+
// its sibling failed — is not this player's failure and decides nothing, so
|
|
780
|
+
// the originator's cause stands; every other failure is decided afresh and
|
|
781
|
+
// recorded as this call's occurrence, whatever an earlier failure threw.
|
|
782
|
+
const decidePlayerCallFailure = (error, roleId, playerId, signal, callId) => {
|
|
783
|
+
const turn = currentSignal;
|
|
784
|
+
const turnAborted = turn !== undefined && turn.aborted;
|
|
785
|
+
if (!turnAborted && signal.aborted)
|
|
786
|
+
return;
|
|
787
|
+
const cause = turn !== undefined && turn.aborted && Object.is(error, turn.reason)
|
|
788
|
+
? ABORTED_FAILURE_CAUSE
|
|
789
|
+
: playerFailureCause({
|
|
790
|
+
roleId,
|
|
791
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
792
|
+
error,
|
|
793
|
+
});
|
|
794
|
+
attachPlaybookFailureCause(error, cause);
|
|
795
|
+
failureCauses.retain(error, cause);
|
|
796
|
+
recordFailure(playerCallFailures, callId, cause);
|
|
797
|
+
};
|
|
754
798
|
const governedEvidenceByBoundaryId = new Map();
|
|
755
799
|
const governedReceiptsByBoundaryId = new Map();
|
|
756
800
|
const governedPlayerOutputs = new WeakMap();
|
|
@@ -1325,7 +1369,12 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1325
1369
|
const roleId = input.role;
|
|
1326
1370
|
const playerId = resolvedPlayerId(roleId);
|
|
1327
1371
|
const playerKey = continuationKey(roleId, playerId);
|
|
1328
|
-
|
|
1372
|
+
// DR-062 §4: composed from this call's own baseline observation, so the
|
|
1373
|
+
// prompt the trace records is the prompt the player is sent.
|
|
1374
|
+
const withPreExisting = (text) => continuation?.preExistingBlock === undefined
|
|
1375
|
+
? text
|
|
1376
|
+
: `${text}\n\n${continuation.preExistingBlock}`;
|
|
1377
|
+
const freshPrompt = withPreExisting(composeInvocationPrompt(input));
|
|
1329
1378
|
let resume;
|
|
1330
1379
|
try {
|
|
1331
1380
|
signal.throwIfAborted();
|
|
@@ -1339,7 +1388,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1339
1388
|
latchControlPlaneError(error, signal);
|
|
1340
1389
|
throw error;
|
|
1341
1390
|
}
|
|
1342
|
-
const prompt = resume === false
|
|
1391
|
+
const prompt = resume === false
|
|
1392
|
+
? freshPrompt
|
|
1393
|
+
: withPreExisting(composeInvocationPrompt(input, true));
|
|
1343
1394
|
const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
|
|
1344
1395
|
const identity = {
|
|
1345
1396
|
stateId: input.stateId,
|
|
@@ -1382,6 +1433,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1382
1433
|
// A rejected call produced no authoritative result, so the previous
|
|
1383
1434
|
// token remains untouched. This also covers a coder promise that
|
|
1384
1435
|
// resolves after its invocation signal was cancelled.
|
|
1436
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
1385
1437
|
latchControlPlaneError(error, signal);
|
|
1386
1438
|
try {
|
|
1387
1439
|
await emitFailure(error);
|
|
@@ -1396,6 +1448,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1396
1448
|
result = validatePlayerResult(rawResult);
|
|
1397
1449
|
}
|
|
1398
1450
|
catch (error) {
|
|
1451
|
+
decidePlayerCallFailure(error, roleId, playerId, signal, callId);
|
|
1399
1452
|
latchControlPlaneError(error, signal);
|
|
1400
1453
|
try {
|
|
1401
1454
|
await emitFailure(error);
|
|
@@ -1468,10 +1521,22 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1468
1521
|
correctionBudget: { limit: 1, spent: false },
|
|
1469
1522
|
};
|
|
1470
1523
|
};
|
|
1471
|
-
|
|
1472
|
-
|
|
1524
|
+
// DR-063 §1/§2: the cause of an unresolved governed settlement is decided
|
|
1525
|
+
// here — a reconciled mismatch supplies its receipt-read cause, an abort or
|
|
1526
|
+
// a refused adjudication its own code, anything else a runtime defect — and
|
|
1527
|
+
// rides the failure the boundary throws.
|
|
1528
|
+
const unresolvedGovernedEvidence = (boundaryId, reason, error, cause) => {
|
|
1529
|
+
const aborted = error !== undefined &&
|
|
1530
|
+
currentSignal?.aborted === true &&
|
|
1531
|
+
Object.is(error, currentSignal.reason);
|
|
1532
|
+
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
1533
|
+
const failure = error instanceof Error
|
|
1473
1534
|
? error
|
|
1474
|
-
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`)
|
|
1535
|
+
: new Error(`DECIDE governed outcome remains unresolved: ${reason}`);
|
|
1536
|
+
attachPlaybookFailureCause(failure, decided);
|
|
1537
|
+
failureCauses.retain(failure, decided);
|
|
1538
|
+
recordFailure(governedFailureOccurrences, boundaryId, decided);
|
|
1539
|
+
governedFailuresByBoundaryId.set(boundaryId, failure);
|
|
1475
1540
|
};
|
|
1476
1541
|
const spendSemanticCorrectionBudget = async (boundary, receipt, finalText, semanticCandidate) => {
|
|
1477
1542
|
const ledger = readEffectLedger();
|
|
@@ -1700,7 +1765,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1700
1765
|
semanticCandidate,
|
|
1701
1766
|
});
|
|
1702
1767
|
if (reconciliation.status === 'unresolved') {
|
|
1703
|
-
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason);
|
|
1768
|
+
unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason, undefined, reconciliation.cause);
|
|
1704
1769
|
return { finalText, semanticCandidate, unresolved: true };
|
|
1705
1770
|
}
|
|
1706
1771
|
const output = reconciliation.output;
|
|
@@ -1861,7 +1926,14 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1861
1926
|
member.envelope = envelope;
|
|
1862
1927
|
if (envelope.result.status !== 'ok' &&
|
|
1863
1928
|
!cohortOperations.signal.aborted) {
|
|
1864
|
-
|
|
1929
|
+
// DR-063 §2: the cancellation carries the cause of the result
|
|
1930
|
+
// that caused it, recorded as this member's occurrence, so the
|
|
1931
|
+
// cohort fails for this member.
|
|
1932
|
+
const cause = playerResultFailureCause(roleId, resolvedPlayerId(roleId), envelope.result);
|
|
1933
|
+
const cancellation = attachPlaybookFailureCause(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`), cause);
|
|
1934
|
+
failureCauses.retain(cancellation, cause);
|
|
1935
|
+
recordFailure(playerCallFailures, member.callId, cause);
|
|
1936
|
+
cohortOperations.abort(cancellation);
|
|
1865
1937
|
}
|
|
1866
1938
|
return envelope.result;
|
|
1867
1939
|
}
|
|
@@ -1928,9 +2000,38 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1928
2000
|
for (const result of acknowledgedResults.values()) {
|
|
1929
2001
|
governedPlayerOutputs.delete(result);
|
|
1930
2002
|
}
|
|
1931
|
-
|
|
2003
|
+
const failure = acknowledgementFailures.length === 1
|
|
1932
2004
|
? acknowledgementFailures[0]
|
|
1933
2005
|
: new AggregateError(acknowledgementFailures, 'DECIDE proposal cohort reconciliation failed');
|
|
2006
|
+
// DR-063 §2: a cohort fails for the member whose failure was decided
|
|
2007
|
+
// first — a rejected or non-`ok` player call, a refused adjudication,
|
|
2008
|
+
// a mismatched receipt — in decision order, not in the order the
|
|
2009
|
+
// players finished or the values they threw. That cause rides every
|
|
2010
|
+
// error the failure can surface as: the cancellation, each member's
|
|
2011
|
+
// own rejection, and the aggregate of both, never an abort.
|
|
2012
|
+
const origin = cohortOperations.signal.aborted
|
|
2013
|
+
? cohortOperations.signal.reason
|
|
2014
|
+
: undefined;
|
|
2015
|
+
const first = ROLE_IDS.flatMap((roleId) => {
|
|
2016
|
+
const member = members[roleId];
|
|
2017
|
+
const occurrence = member === undefined
|
|
2018
|
+
? undefined
|
|
2019
|
+
: (playerCallFailures.get(member.callId) ??
|
|
2020
|
+
governedFailureOccurrences.get(member.effectBoundary.boundaryId));
|
|
2021
|
+
return occurrence === undefined ? [] : [occurrence];
|
|
2022
|
+
}).sort((left, right) => left.sequence - right.sequence)[0];
|
|
2023
|
+
const originCause = first?.cause ??
|
|
2024
|
+
failureCauses.causeOf(failure) ??
|
|
2025
|
+
(origin === undefined ? undefined : failureCauses.causeOf(origin));
|
|
2026
|
+
if (originCause !== undefined) {
|
|
2027
|
+
for (const candidate of [failure, origin, ...acknowledgementFailures]) {
|
|
2028
|
+
if (candidate === undefined)
|
|
2029
|
+
continue;
|
|
2030
|
+
attachPlaybookFailureCause(candidate, originCause);
|
|
2031
|
+
failureCauses.retain(candidate, originCause);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
throw failure;
|
|
1934
2035
|
}
|
|
1935
2036
|
for (const roleId of releaseOrder) {
|
|
1936
2037
|
const member = members[roleId];
|
|
@@ -1993,6 +2094,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
1993
2094
|
const envelope = await runPlayerCall(input, signal, {
|
|
1994
2095
|
callId: active.effectBoundary.callId,
|
|
1995
2096
|
resume: active.playerContinuation,
|
|
2097
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(active.effectBoundary, active.baseline),
|
|
1996
2098
|
});
|
|
1997
2099
|
active.result.resolve(envelope.result);
|
|
1998
2100
|
const acknowledgedResult = await active.acknowledged.promise;
|
|
@@ -2018,8 +2120,13 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2018
2120
|
const settled = await deferredEffects.repository.runExclusive({
|
|
2019
2121
|
signal,
|
|
2020
2122
|
effectBoundary,
|
|
2021
|
-
|
|
2022
|
-
|
|
2123
|
+
// DR-062 §4: the call's own baseline, supplied by the host that
|
|
2124
|
+
// captured it under this exclusive claim.
|
|
2125
|
+
operation: async (context) => {
|
|
2126
|
+
envelope = await runPlayerCall(input, signal, {
|
|
2127
|
+
callId,
|
|
2128
|
+
preExistingBlock: effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline),
|
|
2129
|
+
});
|
|
2023
2130
|
return envelope.result;
|
|
2024
2131
|
},
|
|
2025
2132
|
completeEffectBoundary: completionEvidenceFor(input, input.role, resolvedPlayerId(input.role), signal),
|
|
@@ -2064,7 +2171,10 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2064
2171
|
({ roleId, playerId, callId, result } = await callPlayer(input, combined));
|
|
2065
2172
|
}
|
|
2066
2173
|
if (result.status !== 'ok') {
|
|
2067
|
-
|
|
2174
|
+
// DR-063 §2: the failure Error built for a non-`ok` result carries
|
|
2175
|
+
// the cause decided for that exact result.
|
|
2176
|
+
const cause = playerResultFailureCause(roleId, playerId, result);
|
|
2177
|
+
throw attachPlaybookFailureCause(new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`), cause);
|
|
2068
2178
|
}
|
|
2069
2179
|
const finalText = result.finalText ?? '';
|
|
2070
2180
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -2181,7 +2291,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2181
2291
|
const context = snapshot.context;
|
|
2182
2292
|
const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
|
|
2183
2293
|
? undefined
|
|
2184
|
-
: 'commitCoderProposal')
|
|
2294
|
+
: 'commitCoderProposal', state.activeStateIds.includes('failed')
|
|
2295
|
+
? failedStateError(context)
|
|
2296
|
+
: undefined);
|
|
2185
2297
|
const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
|
|
2186
2298
|
void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
|
|
2187
2299
|
topic: TELEMETRY_TOPIC,
|
|
@@ -2218,7 +2330,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2218
2330
|
continue;
|
|
2219
2331
|
}
|
|
2220
2332
|
const lastError = activeStateId === 'failed'
|
|
2221
|
-
? normalizeErrorCompact(context
|
|
2333
|
+
? normalizeErrorCompact(failedStateError(context))
|
|
2222
2334
|
: undefined;
|
|
2223
2335
|
const description = STATE_DESCRIPTIONS[activeStateId];
|
|
2224
2336
|
if (description === undefined)
|
|
@@ -2226,7 +2338,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2226
2338
|
const roleState = ROLE_STATES.find((candidate) => candidate.stateId === activeStateId);
|
|
2227
2339
|
scheduleStatus(roleState === undefined
|
|
2228
2340
|
? '◆ workflow failed; awaiting Boss recovery.'
|
|
2229
|
-
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined
|
|
2341
|
+
: `⤷ ${roleLabel(roleState.role)}: ${description}`, activeStateId, lastError === undefined
|
|
2342
|
+
? undefined
|
|
2343
|
+
: snapshotJsonValue({ lastError }, 'DECIDE failed status data'));
|
|
2230
2344
|
}
|
|
2231
2345
|
}
|
|
2232
2346
|
catch (error) {
|
|
@@ -2357,13 +2471,15 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2357
2471
|
signal,
|
|
2358
2472
|
operationId,
|
|
2359
2473
|
effectBoundary,
|
|
2360
|
-
operation: async ({ playerContinuation }) => {
|
|
2474
|
+
operation: async ({ playerContinuation, baseline }) => {
|
|
2361
2475
|
if (playerContinuation !== false &&
|
|
2362
2476
|
(typeof playerContinuation !== 'string' ||
|
|
2363
2477
|
playerContinuation.trim() === '')) {
|
|
2364
2478
|
throw new TypeError('DECIDE bound deferred player continuation is invalid');
|
|
2365
2479
|
}
|
|
2366
2480
|
active.playerContinuation = playerContinuation;
|
|
2481
|
+
// DR-062 §4: the continuation's own baseline.
|
|
2482
|
+
active.baseline = baseline;
|
|
2367
2483
|
readiness.resolve({ status: 'ready' });
|
|
2368
2484
|
return active.result.promise;
|
|
2369
2485
|
},
|
|
@@ -2566,7 +2682,9 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2566
2682
|
const actions = deriveControlCandidates().map(({ action }) => ({
|
|
2567
2683
|
...action,
|
|
2568
2684
|
}));
|
|
2569
|
-
const lastError =
|
|
2685
|
+
const lastError = state.stateId === 'failed'
|
|
2686
|
+
? failedStateError(context)
|
|
2687
|
+
: normalizeErrorFull(context.lastError);
|
|
2570
2688
|
const stateDescription = unresolved || state.stateId === undefined
|
|
2571
2689
|
? undefined
|
|
2572
2690
|
: STATE_DESCRIPTIONS[state.stateId];
|
|
@@ -2614,6 +2732,8 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2614
2732
|
const turnId = ++turnSequence;
|
|
2615
2733
|
const callId = `apply-${++applyCallSequence}`;
|
|
2616
2734
|
currentTurnId = turnId;
|
|
2735
|
+
playerCallFailures.clear();
|
|
2736
|
+
governedFailureOccurrences.clear();
|
|
2617
2737
|
currentSignal = signal;
|
|
2618
2738
|
currentAborts = abortReasonClassifier(signal);
|
|
2619
2739
|
controlPlaneError = undefined;
|
|
@@ -2862,7 +2982,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
2862
2982
|
return abortedResult(signal);
|
|
2863
2983
|
}
|
|
2864
2984
|
if (state.activeStateIds.includes('failed')) {
|
|
2865
|
-
const error =
|
|
2985
|
+
const error = failedStateError(context);
|
|
2866
2986
|
return {
|
|
2867
2987
|
outcome: 'failed',
|
|
2868
2988
|
state,
|
|
@@ -3052,8 +3172,12 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3052
3172
|
const state = currentState();
|
|
3053
3173
|
if (state.status !== 'active' || !state.quiescent)
|
|
3054
3174
|
return undefined;
|
|
3055
|
-
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
3056
3175
|
const context = actor.getSnapshot().context;
|
|
3176
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
3177
|
+
// surface publishes it, completed from the runtime's own cause slot.
|
|
3178
|
+
const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
|
|
3179
|
+
? { lastError: failedStateError(context) }
|
|
3180
|
+
: {});
|
|
3057
3181
|
if (deferredEffects !== undefined) {
|
|
3058
3182
|
synchronizeDeferredProjection(readEffectLedger());
|
|
3059
3183
|
}
|
|
@@ -3189,6 +3313,8 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3189
3313
|
}
|
|
3190
3314
|
const turnId = ++turnSequence;
|
|
3191
3315
|
currentTurnId = turnId;
|
|
3316
|
+
playerCallFailures.clear();
|
|
3317
|
+
governedFailureOccurrences.clear();
|
|
3192
3318
|
currentSignal = turn.signal;
|
|
3193
3319
|
currentAborts = abortReasonClassifier(turn.signal);
|
|
3194
3320
|
controlPlaneError = undefined;
|
|
@@ -3319,7 +3445,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3319
3445
|
: {
|
|
3320
3446
|
outcome: 'failed',
|
|
3321
3447
|
state,
|
|
3322
|
-
error:
|
|
3448
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
3323
3449
|
name: 'Error',
|
|
3324
3450
|
message: String(effectiveError),
|
|
3325
3451
|
},
|
|
@@ -3344,7 +3470,7 @@ function createDecidePlaybookRuntime(options, deferredEffects) {
|
|
|
3344
3470
|
result = {
|
|
3345
3471
|
outcome: drainAborted ? 'aborted' : 'failed',
|
|
3346
3472
|
state,
|
|
3347
|
-
error:
|
|
3473
|
+
error: failedStateError({ lastError: effectiveError }) ?? {
|
|
3348
3474
|
name: 'Error',
|
|
3349
3475
|
message: String(effectiveError),
|
|
3350
3476
|
},
|