@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.
Files changed (32) hide show
  1. package/README.md +2 -2
  2. package/docs/configuration.md +15 -9
  3. package/docs/embedding.md +7 -5
  4. package/package.json +3 -1
  5. package/reference/sdlc/code.md +1 -1
  6. package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
  7. package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
  8. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  9. package/reference/sdlc/code.playbook/bin/run.js +12 -8
  10. package/reference/sdlc/code.playbook/code.fsm.js +1 -1
  11. package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
  12. package/reference/sdlc/code.playbook/code.gears.md +2 -2
  13. package/reference/sdlc/decide.md +1 -1
  14. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  15. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  16. package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
  17. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +1 -9
  18. package/reference/sdlc/decide.playbook/decide.playbook.js +158 -32
  19. package/reference/sdlc/decide.playbook/decide.playbook.ts +229 -46
  20. package/reference/sdlc/review.md +1 -1
  21. package/reference/sdlc/review.playbook/review.fsm.js +1 -1
  22. package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
  23. package/reference/sdlc/review.playbook/review.gears.md +1 -1
  24. package/slc/prefix-prompts.mjs +190 -0
  25. package/slc/prefix.md +200 -0
  26. package/slc/slc.pin-inputs.json +1 -0
  27. package/src/xstate-playbook-runtime.d.ts +35 -1
  28. package/src/xstate-playbook-runtime.js +82 -47
  29. package/src/xstate-playbook-runtime.ts +107 -60
  30. package/src/xstate-runtime.d.ts +3 -1
  31. package/src/xstate-runtime.js +9 -2
  32. 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
- const freshPrompt = composeInvocationPrompt(input);
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 ? freshPrompt : composeInvocationPrompt(input, true);
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
- const unresolvedGovernedEvidence = (boundaryId, reason, error) => {
1472
- governedFailuresByBoundaryId.set(boundaryId, error instanceof Error
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
- cohortOperations.abort(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`));
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
- throw acknowledgementFailures.length === 1
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
- operation: async () => {
2022
- envelope = await runPlayerCall(input, signal, { callId });
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
- throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
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.lastError)
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 ? undefined : { lastError });
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 = normalizeErrorFull(context.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 = normalizeErrorFull(context.lastError);
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: normalizeErrorFull(effectiveError) ?? {
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: normalizeErrorFull(effectiveError) ?? {
3473
+ error: failedStateError({ lastError: effectiveError }) ?? {
3348
3474
  name: 'Error',
3349
3475
  message: String(effectiveError),
3350
3476
  },