@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
@@ -70,11 +70,11 @@ const GOVERNED_JUDGE_FAILURE_REASONS = [
70
70
  'corrective semantic candidate is invalid',
71
71
  'semantic correction budget is unavailable',
72
72
  ];
73
- const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
73
+ export const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
74
74
  code: 'aborted',
75
75
  evidence: {},
76
76
  });
77
- function runtimeDefectCause(reason) {
77
+ export function runtimeDefectCause(reason) {
78
78
  return assertPlaybookFailureCause({
79
79
  code: 'runtime-defect',
80
80
  evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
@@ -98,7 +98,7 @@ function failureErrorCode(error) {
98
98
  return undefined;
99
99
  }
100
100
  }
101
- function playerFailureCause(input) {
101
+ export function playerFailureCause(input) {
102
102
  return assertPlaybookFailureCause({
103
103
  code: 'player-failed',
104
104
  evidence: {
@@ -114,6 +114,49 @@ function playerFailureCause(input) {
114
114
  },
115
115
  });
116
116
  }
117
+ /**
118
+ * DR-063 §1: the cause of an unresolved governed settlement. A reconciled
119
+ * mismatch supplies its own receipt-read cause; the reasons a runtime owns map
120
+ * to adjudication, abort, and runtime-defect codes. Shared with DECIDE's
121
+ * bespoke runtime, which decides the same reasons.
122
+ */
123
+ export function governedSettlementCause(reason, error, aborted, supplied) {
124
+ if (supplied !== undefined)
125
+ return supplied;
126
+ if (aborted || reason.includes('aborted'))
127
+ return ABORTED_FAILURE_CAUSE;
128
+ if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
129
+ const transport = failureErrorEvidence(error);
130
+ return assertPlaybookFailureCause({
131
+ code: 'judge-failed',
132
+ evidence: {
133
+ reason,
134
+ ...(transport === undefined ? {} : { error: transport }),
135
+ },
136
+ });
137
+ }
138
+ return runtimeDefectCause(reason);
139
+ }
140
+ const FAILURE_CAUSE_RETENTION_LIMIT = 8;
141
+ export function createFailureCauseRetention() {
142
+ const causes = new Map();
143
+ return {
144
+ retain(error, cause) {
145
+ if (error === undefined || error === null)
146
+ return;
147
+ causes.delete(error);
148
+ causes.set(error, cause);
149
+ while (causes.size > FAILURE_CAUSE_RETENTION_LIMIT) {
150
+ causes.delete(causes.keys().next().value);
151
+ }
152
+ },
153
+ causeOf(error) {
154
+ if (error === undefined || error === null)
155
+ return undefined;
156
+ return normalizeError(error).cause ?? causes.get(error);
157
+ },
158
+ };
159
+ }
117
160
  // ---------------------------------------------------------------------------
118
161
  // DR-028: both call boundaries treat an `ok` result whose `finalText` is
119
162
  // missing, empty, or whitespace-only under one empty predicate, and that
@@ -578,7 +621,7 @@ function preExistingChangesBlock(baseline) {
578
621
  * `one-descendant-commit` disposition may commit nothing, so it is told
579
622
  * nothing.
580
623
  */
581
- function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
624
+ export function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
582
625
  return effectBoundary.dispositions.includes('one-descendant-commit')
583
626
  ? preExistingChangesBlock(baseline)
584
627
  : undefined;
@@ -2186,15 +2229,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
2186
2229
  let deferInspectionEmissions = false;
2187
2230
  let deferredInspectionEmissions = [];
2188
2231
  let controlPlaneError;
2189
- // DR-063 §2: the cause decided at the site the failure was decided, kept
2190
- // for the failures whose Error the compiled artifact builds itself and the
2191
- // runtime therefore cannot decorate. It is cleared at each Boss turn start
2192
- // and at each successful player result, so it is never stale, and it is
2193
- // only ever read as the cause of an error that carries none of its own.
2194
- let lastFailureCause;
2195
- const rememberFailureCause = (cause) => {
2196
- lastFailureCause = cause;
2197
- };
2232
+ // DR-063 §2: the cause decided for a non-`ok` player result before the
2233
+ // compiled artifact builds the Error that carries it and hands it to
2234
+ // `markPlayerResultFailure`. It is cleared at each Boss turn start and at
2235
+ // each successful result, so it is never stale.
2236
+ let pendingResultFailureCause;
2237
+ // DR-063 §2: the causes decided for thrown values that cannot carry the
2238
+ // marker — a string, a frozen error — kept by identity for as long as the
2239
+ // failure stands, so every surface that reads the FSM's own `lastError`
2240
+ // publishes the cause decided for exactly that value, whatever turn reads
2241
+ // it.
2242
+ const failureCauses = createFailureCauseRetention();
2198
2243
  // Previous root-machine state for the inspect-driven telemetry /
2199
2244
  // status emitter. undefined before the first inspect firing.
2200
2245
  let priorState;
@@ -3102,33 +3147,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
3102
3147
  },
3103
3148
  });
3104
3149
  }
3105
- // DR-063 §1: the cause of an unresolved governed settlement. A reconciled
3106
- // mismatch supplies its own receipt-read cause; the reasons this function
3107
- // owns map to adjudication, abort, and runtime-defect codes.
3108
- function governedSettlementCause(reason, error, aborted, supplied) {
3109
- if (supplied !== undefined)
3110
- return supplied;
3111
- if (aborted || reason.includes('aborted'))
3112
- return ABORTED_FAILURE_CAUSE;
3113
- if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
3114
- const transport = failureErrorEvidence(error);
3115
- return assertPlaybookFailureCause({
3116
- code: 'judge-failed',
3117
- evidence: {
3118
- reason,
3119
- ...(transport === undefined ? {} : { error: transport }),
3120
- },
3121
- });
3122
- }
3123
- return runtimeDefectCause(reason);
3124
- }
3125
3150
  function unresolvedGovernedSettlement(reason, error, signal = activeSignal, cause) {
3126
3151
  const aborted = error !== undefined && signal?.aborted === true
3127
3152
  ? Object.is(error, signal.reason)
3128
3153
  : false;
3129
3154
  const decided = governedSettlementCause(reason, error, aborted, cause);
3130
- rememberFailureCause(decided);
3131
3155
  if (aborted) {
3156
+ failureCauses.retain(error, decided);
3132
3157
  return {
3133
3158
  status: 'unresolved',
3134
3159
  error: attachPlaybookFailureCause(error, decided),
@@ -3137,6 +3162,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3137
3162
  const failure = error instanceof Error
3138
3163
  ? error
3139
3164
  : new Error(`${label} governed outcome remains unresolved: ${reason}`);
3165
+ failureCauses.retain(failure, decided);
3140
3166
  return {
3141
3167
  status: 'unresolved',
3142
3168
  error: attachPlaybookFailureCause(markFsmResultFailure(failure), decided),
@@ -3766,18 +3792,18 @@ export function createXStatePlaybookRuntime(machine, spec) {
3766
3792
  // the player, and the reported error are known, and a successful
3767
3793
  // result clears it so no later failure inherits it.
3768
3794
  if (result.status === 'ok') {
3769
- lastFailureCause = undefined;
3795
+ pendingResultFailureCause = undefined;
3770
3796
  }
3771
3797
  else if (result.status === 'aborted') {
3772
- rememberFailureCause(ABORTED_FAILURE_CAUSE);
3798
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
3773
3799
  }
3774
3800
  else {
3775
- rememberFailureCause(playerFailureCause({
3801
+ pendingResultFailureCause = playerFailureCause({
3776
3802
  roleId,
3777
3803
  ...(playerId === undefined ? {} : { playerId }),
3778
3804
  error: result.error ??
3779
3805
  `callPlayer status ${JSON.stringify(result.status)}`,
3780
- }));
3806
+ });
3781
3807
  }
3782
3808
  return result;
3783
3809
  };
@@ -3850,9 +3876,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
3850
3876
  }
3851
3877
  },
3852
3878
  markPlayerResultFailure(error) {
3853
- return lastFailureCause === undefined
3854
- ? error
3855
- : attachPlaybookFailureCause(error, lastFailureCause);
3879
+ if (pendingResultFailureCause === undefined)
3880
+ return error;
3881
+ failureCauses.retain(error, pendingResultFailureCause);
3882
+ return attachPlaybookFailureCause(error, pendingResultFailureCause);
3856
3883
  },
3857
3884
  takeGovernedPlayerOutput(result) {
3858
3885
  const settlement = governedPlayerSettlements.get(result);
@@ -4381,7 +4408,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
4381
4408
  }
4382
4409
  return {
4383
4410
  ...normalized,
4384
- cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
4411
+ cause: failureCauses.causeOf(context.lastError) ??
4412
+ runtimeDefectCause(normalized.message),
4385
4413
  };
4386
4414
  }
4387
4415
  // DR-063 §2: a rejected player port or a result the runtime cannot read is
@@ -4394,7 +4422,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4394
4422
  ...(playerId === undefined ? {} : { playerId }),
4395
4423
  error,
4396
4424
  });
4397
- rememberFailureCause(cause);
4425
+ failureCauses.retain(error, cause);
4398
4426
  return attachPlaybookFailureCause(error, cause);
4399
4427
  }
4400
4428
  function completeFailedStatuses(statuses, state, context) {
@@ -5691,9 +5719,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
5691
5719
  const state = currentState();
5692
5720
  if (state.status !== 'active' || !state.quiescent)
5693
5721
  return undefined;
5694
- const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
5695
5722
  const context = actor.getSnapshot()
5696
5723
  .context;
5724
+ // DR-063 §2: the failed state's `lastError` is persisted as every live
5725
+ // surface publishes it, completed from the runtime's own cause slot, so
5726
+ // a thrown value that refused the cause still explains itself after a
5727
+ // restart.
5728
+ const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
5729
+ ? { lastError: failedStateError(context ?? {}) }
5730
+ : {});
5697
5731
  effectLedgerMirror = currentEffectLedger();
5698
5732
  refreshRetainedEffectReconciliation(effectLedgerMirror);
5699
5733
  syncDeferredReconciliationOverlay();
@@ -6128,9 +6162,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
6128
6162
  activeAborts = abortReasonClassifier(signal);
6129
6163
  activeAbortEmission = undefined;
6130
6164
  controlPlaneError = undefined;
6131
- // DR-063 §2: each turn decides its own failures, so the slot never
6132
- // carries a previous turn's cause into this one.
6133
- lastFailureCause = undefined;
6165
+ // DR-063 §2: each turn decides its own failures, so no pending result
6166
+ // cause carries into this one; a cause decided for a parked failure
6167
+ // stays bound to that failure.
6168
+ pendingResultFailureCause = undefined;
6134
6169
  let result;
6135
6170
  let operationError;
6136
6171
  // The boundary sentinel releases on every exit: a settlement defect
@@ -284,12 +284,12 @@ const GOVERNED_JUDGE_FAILURE_REASONS: readonly string[] = [
284
284
  'semantic correction budget is unavailable',
285
285
  ];
286
286
 
287
- const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
287
+ export const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
288
288
  code: 'aborted',
289
289
  evidence: {},
290
290
  });
291
291
 
292
- function runtimeDefectCause(reason: string): PlaybookFailureCause {
292
+ export function runtimeDefectCause(reason: string): PlaybookFailureCause {
293
293
  return assertPlaybookFailureCause({
294
294
  code: 'runtime-defect',
295
295
  evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
@@ -315,7 +315,7 @@ function failureErrorCode(error: unknown): string | undefined {
315
315
  }
316
316
  }
317
317
 
318
- function playerFailureCause(input: {
318
+ export function playerFailureCause(input: {
319
319
  readonly roleId: string;
320
320
  readonly playerId?: string;
321
321
  readonly error: unknown;
@@ -336,6 +336,66 @@ function playerFailureCause(input: {
336
336
  });
337
337
  }
338
338
 
339
+ /**
340
+ * DR-063 §1: the cause of an unresolved governed settlement. A reconciled
341
+ * mismatch supplies its own receipt-read cause; the reasons a runtime owns map
342
+ * to adjudication, abort, and runtime-defect codes. Shared with DECIDE's
343
+ * bespoke runtime, which decides the same reasons.
344
+ */
345
+ export function governedSettlementCause(
346
+ reason: string,
347
+ error: unknown,
348
+ aborted: boolean,
349
+ supplied: PlaybookFailureCause | undefined,
350
+ ): PlaybookFailureCause {
351
+ if (supplied !== undefined) return supplied;
352
+ if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
353
+ if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
354
+ const transport = failureErrorEvidence(error);
355
+ return assertPlaybookFailureCause({
356
+ code: 'judge-failed',
357
+ evidence: {
358
+ reason,
359
+ ...(transport === undefined ? {} : { error: transport }),
360
+ },
361
+ });
362
+ }
363
+ return runtimeDefectCause(reason);
364
+ }
365
+
366
+ /**
367
+ * DR-063 §2: the causes decided for thrown values that cannot carry the
368
+ * marker — a string, a frozen error — kept by identity for as long as the
369
+ * failure stands, so every surface that reads the FSM's own `lastError`
370
+ * publishes the cause decided for exactly that value, whatever turn reads it.
371
+ * A value that carries the marker answers from it; the map is bounded, since
372
+ * only values that refuse the marker need an entry.
373
+ */
374
+ export interface PlaybookFailureCauseRetention {
375
+ retain(error: unknown, cause: PlaybookFailureCause): void;
376
+ causeOf(error: unknown): PlaybookFailureCause | undefined;
377
+ }
378
+
379
+ const FAILURE_CAUSE_RETENTION_LIMIT = 8;
380
+
381
+ export function createFailureCauseRetention(): PlaybookFailureCauseRetention {
382
+ const causes = new Map<unknown, PlaybookFailureCause>();
383
+ return {
384
+ retain(error, cause) {
385
+ if (error === undefined || error === null) return;
386
+ causes.delete(error);
387
+ causes.set(error, cause);
388
+ while (causes.size > FAILURE_CAUSE_RETENTION_LIMIT) {
389
+ causes.delete(causes.keys().next().value);
390
+ }
391
+ },
392
+ causeOf(error) {
393
+ if (error === undefined || error === null) return undefined;
394
+ return normalizeError(error).cause ?? causes.get(error);
395
+ },
396
+ };
397
+ }
398
+
339
399
 
340
400
  // ---------------------------------------------------------------------------
341
401
  // DR-028: both call boundaries treat an `ok` result whose `finalText` is
@@ -1269,8 +1329,8 @@ function preExistingChangesBlock(
1269
1329
  * `one-descendant-commit` disposition may commit nothing, so it is told
1270
1330
  * nothing.
1271
1331
  */
1272
- function effectAuthorizedPreExistingBlock(
1273
- effectBoundary: Pick<XStateEffectBoundarySeed, 'dispositions'>,
1332
+ export function effectAuthorizedPreExistingBlock(
1333
+ effectBoundary: Pick<PlaybookEffectBoundaryStart, 'dispositions'>,
1274
1334
  baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
1275
1335
  ): string | undefined {
1276
1336
  return effectBoundary.dispositions.includes('one-descendant-commit')
@@ -3577,15 +3637,17 @@ export function createXStatePlaybookRuntime<
3577
3637
  let deferInspectionEmissions = false;
3578
3638
  let deferredInspectionEmissions: Array<() => void> = [];
3579
3639
  let controlPlaneError: unknown;
3580
- // DR-063 §2: the cause decided at the site the failure was decided, kept
3581
- // for the failures whose Error the compiled artifact builds itself and the
3582
- // runtime therefore cannot decorate. It is cleared at each Boss turn start
3583
- // and at each successful player result, so it is never stale, and it is
3584
- // only ever read as the cause of an error that carries none of its own.
3585
- let lastFailureCause: PlaybookFailureCause | undefined;
3586
- const rememberFailureCause = (cause: PlaybookFailureCause): void => {
3587
- lastFailureCause = cause;
3588
- };
3640
+ // DR-063 §2: the cause decided for a non-`ok` player result before the
3641
+ // compiled artifact builds the Error that carries it and hands it to
3642
+ // `markPlayerResultFailure`. It is cleared at each Boss turn start and at
3643
+ // each successful result, so it is never stale.
3644
+ let pendingResultFailureCause: PlaybookFailureCause | undefined;
3645
+ // DR-063 §2: the causes decided for thrown values that cannot carry the
3646
+ // marker — a string, a frozen error — kept by identity for as long as the
3647
+ // failure stands, so every surface that reads the FSM's own `lastError`
3648
+ // publishes the cause decided for exactly that value, whatever turn reads
3649
+ // it.
3650
+ const failureCauses = createFailureCauseRetention();
3589
3651
  // Previous root-machine state for the inspect-driven telemetry /
3590
3652
  // status emitter. undefined before the first inspect firing.
3591
3653
  let priorState: PlaybookState | undefined;
@@ -4865,30 +4927,6 @@ export function createXStatePlaybookRuntime<
4865
4927
  });
4866
4928
  }
4867
4929
 
4868
- // DR-063 §1: the cause of an unresolved governed settlement. A reconciled
4869
- // mismatch supplies its own receipt-read cause; the reasons this function
4870
- // owns map to adjudication, abort, and runtime-defect codes.
4871
- function governedSettlementCause(
4872
- reason: string,
4873
- error: unknown,
4874
- aborted: boolean,
4875
- supplied: PlaybookFailureCause | undefined,
4876
- ): PlaybookFailureCause {
4877
- if (supplied !== undefined) return supplied;
4878
- if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
4879
- if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
4880
- const transport = failureErrorEvidence(error);
4881
- return assertPlaybookFailureCause({
4882
- code: 'judge-failed',
4883
- evidence: {
4884
- reason,
4885
- ...(transport === undefined ? {} : { error: transport }),
4886
- },
4887
- });
4888
- }
4889
- return runtimeDefectCause(reason);
4890
- }
4891
-
4892
4930
  function unresolvedGovernedSettlement(
4893
4931
  reason: string,
4894
4932
  error?: unknown,
@@ -4900,8 +4938,8 @@ export function createXStatePlaybookRuntime<
4900
4938
  ? Object.is(error, signal.reason)
4901
4939
  : false;
4902
4940
  const decided = governedSettlementCause(reason, error, aborted, cause);
4903
- rememberFailureCause(decided);
4904
4941
  if (aborted) {
4942
+ failureCauses.retain(error, decided);
4905
4943
  return {
4906
4944
  status: 'unresolved',
4907
4945
  error: attachPlaybookFailureCause(error, decided),
@@ -4911,6 +4949,7 @@ export function createXStatePlaybookRuntime<
4911
4949
  error instanceof Error
4912
4950
  ? error
4913
4951
  : new Error(`${label} governed outcome remains unresolved: ${reason}`);
4952
+ failureCauses.retain(failure, decided);
4914
4953
  return {
4915
4954
  status: 'unresolved',
4916
4955
  error: attachPlaybookFailureCause(
@@ -5835,19 +5874,17 @@ export function createXStatePlaybookRuntime<
5835
5874
  // the player, and the reported error are known, and a successful
5836
5875
  // result clears it so no later failure inherits it.
5837
5876
  if (result.status === 'ok') {
5838
- lastFailureCause = undefined;
5877
+ pendingResultFailureCause = undefined;
5839
5878
  } else if (result.status === 'aborted') {
5840
- rememberFailureCause(ABORTED_FAILURE_CAUSE);
5879
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
5841
5880
  } else {
5842
- rememberFailureCause(
5843
- playerFailureCause({
5844
- roleId,
5845
- ...(playerId === undefined ? {} : { playerId }),
5846
- error:
5847
- result.error ??
5848
- `callPlayer status ${JSON.stringify(result.status)}`,
5849
- }),
5850
- );
5881
+ pendingResultFailureCause = playerFailureCause({
5882
+ roleId,
5883
+ ...(playerId === undefined ? {} : { playerId }),
5884
+ error:
5885
+ result.error ??
5886
+ `callPlayer status ${JSON.stringify(result.status)}`,
5887
+ });
5851
5888
  }
5852
5889
  return result;
5853
5890
  };
@@ -5957,9 +5994,9 @@ export function createXStatePlaybookRuntime<
5957
5994
  },
5958
5995
 
5959
5996
  markPlayerResultFailure(error): Error {
5960
- return lastFailureCause === undefined
5961
- ? error
5962
- : attachPlaybookFailureCause(error, lastFailureCause);
5997
+ if (pendingResultFailureCause === undefined) return error;
5998
+ failureCauses.retain(error, pendingResultFailureCause);
5999
+ return attachPlaybookFailureCause(error, pendingResultFailureCause);
5963
6000
  },
5964
6001
 
5965
6002
  takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
@@ -6619,7 +6656,9 @@ export function createXStatePlaybookRuntime<
6619
6656
  }
6620
6657
  return {
6621
6658
  ...normalized,
6622
- cause: lastFailureCause ?? runtimeDefectCause(normalized.message),
6659
+ cause:
6660
+ failureCauses.causeOf(context.lastError) ??
6661
+ runtimeDefectCause(normalized.message),
6623
6662
  };
6624
6663
  }
6625
6664
 
@@ -6638,7 +6677,7 @@ export function createXStatePlaybookRuntime<
6638
6677
  ...(playerId === undefined ? {} : { playerId }),
6639
6678
  error,
6640
6679
  });
6641
- rememberFailureCause(cause);
6680
+ failureCauses.retain(error, cause);
6642
6681
  return attachPlaybookFailureCause(error, cause);
6643
6682
  }
6644
6683
 
@@ -8243,11 +8282,18 @@ export function createXStatePlaybookRuntime<
8243
8282
  }
8244
8283
  const state = currentState();
8245
8284
  if (state.status !== 'active' || !state.quiescent) return undefined;
8285
+ const context = (actor.getSnapshot() as { context?: unknown })
8286
+ .context as Record<string, unknown>;
8287
+ // DR-063 §2: the failed state's `lastError` is persisted as every live
8288
+ // surface publishes it, completed from the runtime's own cause slot, so
8289
+ // a thrown value that refused the cause still explains itself after a
8290
+ // restart.
8246
8291
  const machineSnapshot = detachPersistedMachineSnapshot(
8247
8292
  actor.getPersistedSnapshot(),
8293
+ state.stateId === 'failed'
8294
+ ? { lastError: failedStateError(context ?? {}) }
8295
+ : {},
8248
8296
  );
8249
- const context = (actor.getSnapshot() as { context?: unknown })
8250
- .context as Record<string, unknown>;
8251
8297
  effectLedgerMirror = currentEffectLedger();
8252
8298
  refreshRetainedEffectReconciliation(effectLedgerMirror);
8253
8299
  syncDeferredReconciliationOverlay();
@@ -8763,9 +8809,10 @@ export function createXStatePlaybookRuntime<
8763
8809
  activeAborts = abortReasonClassifier(signal);
8764
8810
  activeAbortEmission = undefined;
8765
8811
  controlPlaneError = undefined;
8766
- // DR-063 §2: each turn decides its own failures, so the slot never
8767
- // carries a previous turn's cause into this one.
8768
- lastFailureCause = undefined;
8812
+ // DR-063 §2: each turn decides its own failures, so no pending result
8813
+ // cause carries into this one; a cause decided for a parked failure
8814
+ // stays bound to that failure.
8815
+ pendingResultFailureCause = undefined;
8769
8816
  let result: PlaybookRunResult | undefined;
8770
8817
  let operationError: unknown;
8771
8818
  // The boundary sentinel releases on every exit: a settlement defect
@@ -44,7 +44,9 @@ export interface SnapshotNormalizationOptions {
44
44
  pendingCall?: PlaybookPendingCall;
45
45
  }
46
46
  export declare function normalizePlaybookSnapshot(snapshot: unknown, options?: SnapshotNormalizationOptions): PlaybookState;
47
- export declare function detachPersistedMachineSnapshot(persisted: unknown): JsonValue;
47
+ export declare function detachPersistedMachineSnapshot(persisted: unknown, completion?: {
48
+ readonly lastError?: NormalizedError;
49
+ }): JsonValue;
48
50
  /** Authority for one governed delegated-player output field (DR-040 §1). */
49
51
  export type PlaybookSemanticFieldAuthority = 'presentation' | 'semantic' | 'effect' | 'runtime';
50
52
  /** One already-validated state-local governed outcome declaration. */
@@ -607,8 +607,15 @@ export function normalizePlaybookSnapshot(snapshot, options = {}) {
607
607
  // DR-014 §1: deep-detach an XState persisted actor snapshot into strict
608
608
  // JSON for a PlaybookRuntimeSnapshot, normalizing any raw Error value
609
609
  // (for example FSM context `lastError`) instead of rejecting it.
610
- export function detachPersistedMachineSnapshot(persisted) {
611
- return snapshotJsonValue(withErrorsNormalized(persisted, new Set()), 'persisted machine snapshot');
610
+ // DR-063 §2: a runtime parked in its failure state supplies the `lastError`
611
+ // it publishes live — completed with the cause it decided — and that record
612
+ // replaces the context's own, which a thrown value may have refused to carry.
613
+ export function detachPersistedMachineSnapshot(persisted, completion = {}) {
614
+ const detached = withErrorsNormalized(persisted, new Set());
615
+ const lastError = completion.lastError;
616
+ return snapshotJsonValue(lastError !== undefined && isRecord(detached) && isRecord(detached.context)
617
+ ? { ...detached, context: { ...detached.context, lastError } }
618
+ : detached, 'persisted machine snapshot');
612
619
  }
613
620
  function withErrorsNormalized(value, ancestors) {
614
621
  if (value instanceof Error)
@@ -902,9 +902,19 @@ export function normalizePlaybookSnapshot(
902
902
  // DR-014 §1: deep-detach an XState persisted actor snapshot into strict
903
903
  // JSON for a PlaybookRuntimeSnapshot, normalizing any raw Error value
904
904
  // (for example FSM context `lastError`) instead of rejecting it.
905
- export function detachPersistedMachineSnapshot(persisted: unknown): JsonValue {
905
+ // DR-063 §2: a runtime parked in its failure state supplies the `lastError`
906
+ // it publishes live — completed with the cause it decided — and that record
907
+ // replaces the context's own, which a thrown value may have refused to carry.
908
+ export function detachPersistedMachineSnapshot(
909
+ persisted: unknown,
910
+ completion: { readonly lastError?: NormalizedError } = {},
911
+ ): JsonValue {
912
+ const detached = withErrorsNormalized(persisted, new Set());
913
+ const lastError = completion.lastError;
906
914
  return snapshotJsonValue(
907
- withErrorsNormalized(persisted, new Set()),
915
+ lastError !== undefined && isRecord(detached) && isRecord(detached.context)
916
+ ? { ...detached, context: { ...detached.context, lastError } }
917
+ : detached,
908
918
  'persisted machine snapshot',
909
919
  );
910
920
  }