@sublang/playbook 15.1.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -66,6 +66,8 @@ import type {
66
66
  PlaybookEffectLogicalOperation,
67
67
  PlaybookFailureCause,
68
68
  PlaybookPendingBossQuestion,
69
+ PlaybookPendingCall,
70
+ PlaybookStepRecord,
69
71
  PlaybookPorts,
70
72
  PlaybookRepositoryReceipt,
71
73
  PlaybookRunResult,
@@ -158,7 +160,7 @@ export interface RuntimeBoundaryCalls {
158
160
  * with the cause the boundary decided at the call itself, where the role,
159
161
  * the resolved player, and the reported error are known.
160
162
  */
161
- markPlayerResultFailure?(error: Error): Error;
163
+ markPlayerResultFailure?(error: Error, result?: PlayerResult): Error;
162
164
  callJudge(
163
165
  purpose: JudgePurpose,
164
166
  stateId: string | undefined,
@@ -339,8 +341,8 @@ export function playerFailureCause(input: {
339
341
  /**
340
342
  * DR-063 §1: the cause of an unresolved governed settlement. A reconciled
341
343
  * 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
+ * to adjudication, abort, and runtime-defect codes. Exported so linked
345
+ * machinery of an artifact's own decides the same reasons.
344
346
  */
345
347
  export function governedSettlementCause(
346
348
  reason: string,
@@ -365,11 +367,12 @@ export function governedSettlementCause(
365
367
 
366
368
  /**
367
369
  * 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.
370
+ * marker — a string, a frozen error — and for the record a machine keeps in
371
+ * place of the thrown value, kept by identity for as long as the failure
372
+ * stands, so every surface that reads the FSM's own `lastError` publishes the
373
+ * cause decided for exactly that failure, whatever turn reads it. A value
374
+ * that carries the marker answers from it; the map is bounded, since only
375
+ * values that lack the marker need an entry.
373
376
  */
374
377
  export interface PlaybookFailureCauseRetention {
375
378
  retain(error: unknown, cause: PlaybookFailureCause): void;
@@ -396,6 +399,23 @@ export function createFailureCauseRetention(): PlaybookFailureCauseRetention {
396
399
  };
397
400
  }
398
401
 
402
+ /**
403
+ * DR-063 §2: the value an invoked actor's rejection carried into the machine,
404
+ * read from the XState error event that delivered it, or `undefined` for any
405
+ * other event.
406
+ */
407
+ function actorErrorOf(event: unknown): unknown {
408
+ if (typeof event !== 'object' || event === null) return undefined;
409
+ try {
410
+ const { type, error } = event as { type?: unknown; error?: unknown };
411
+ return typeof type === 'string' && type.startsWith('xstate.error.actor.')
412
+ ? error
413
+ : undefined;
414
+ } catch {
415
+ return undefined;
416
+ }
417
+ }
418
+
399
419
 
400
420
  // ---------------------------------------------------------------------------
401
421
  // DR-028: both call boundaries treat an `ok` result whose `finalText` is
@@ -508,7 +528,29 @@ type XStateEffectBoundarySeed = Omit<
508
528
  'playbookId' | 'canonicalWorktree' | 'baseline' | 'cohortId'
509
529
  >;
510
530
 
531
+ /** One member's completion inside a repository cohort (DR-067 §1). */
532
+ type XStateRepositoryCohortCompletion<T> =
533
+ XStateRepositoryExclusiveCompletion<T> & { readonly roleId: string };
534
+
535
+ interface XStateRepositoryCohortResult<T> {
536
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
537
+ readonly invocationId: string;
538
+ readonly operations: Readonly<
539
+ Record<
540
+ string,
541
+ XStateRepositoryOperationSettlement<T> | XStateRepositoryOperationRejection
542
+ >
543
+ >;
544
+ readonly receipts: Readonly<Record<string, PlaybookRepositoryReceipt>>;
545
+ readonly effectLedger: PlaybookEffectLedger;
546
+ }
547
+
511
548
  export interface XStateRepositoryCapability {
549
+ observe?(): Promise<PlaybookRepositoryReceipt['baseline']>;
550
+ acquire?(options: { readonly signal: AbortSignal }): Promise<{
551
+ assertOwner(): Promise<void>;
552
+ release(): Promise<void>;
553
+ }>;
512
554
  runExclusive<T>(options: {
513
555
  readonly signal: AbortSignal;
514
556
  readonly effectBoundary: XStateEffectBoundarySeed;
@@ -542,6 +584,34 @@ export interface XStateRepositoryCapability {
542
584
  readonly signal: AbortSignal;
543
585
  readonly operationId: string;
544
586
  }): Promise<XStateRepositoryDeferredParked | XStateRepositoryDeferredRestoreResult>;
587
+ /**
588
+ * DR-067 §1 / PBRT-73: one repository claim over the working leaves of a
589
+ * parallel state entered together — every member an `unchanged` boundary
590
+ * of one cohort, the operations run concurrently under that claim, and
591
+ * one completion callback per member. Required for an artifact that
592
+ * declares a parallel state.
593
+ */
594
+ runCohort?<T>(options: {
595
+ readonly signal: AbortSignal;
596
+ readonly invocationId: string;
597
+ readonly roleIds: readonly string[];
598
+ readonly dispositionsByRole: Readonly<Record<string, readonly ['unchanged']>>;
599
+ readonly effectBoundaries: Readonly<Record<string, XStateEffectBoundarySeed>>;
600
+ readonly operations: Readonly<
601
+ Record<
602
+ string,
603
+ (context: {
604
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
605
+ readonly identity: unknown;
606
+ readonly invocationId: string;
607
+ readonly roleId: string;
608
+ }) => Promise<T>
609
+ >
610
+ >;
611
+ readonly completeEffectBoundary: (
612
+ completion: XStateRepositoryCohortCompletion<T>,
613
+ ) => XStateRepositoryCompletionEvidence | Promise<XStateRepositoryCompletionEvidence>;
614
+ }): Promise<XStateRepositoryCohortResult<T>>;
545
615
  }
546
616
 
547
617
  function assertNoConfiguredHostCapabilities(value: unknown, label: string): void {
@@ -643,6 +713,7 @@ function configuredOptionsFromFactoryInput(
643
713
  function repositoryCapabilityFromHostCapabilities(
644
714
  hostCapabilities: object | undefined,
645
715
  label: string,
716
+ requireCohort = false,
646
717
  ): XStateRepositoryCapability {
647
718
  const descriptor =
648
719
  hostCapabilities === undefined
@@ -659,10 +730,15 @@ function repositoryCapabilityFromHostCapabilities(
659
730
  Array.isArray(repository) ||
660
731
  typeof (repository as { runExclusive?: unknown }).runExclusive !==
661
732
  'function' ||
662
- typeof (repository as { runDeferred?: unknown }).runDeferred !== 'function'
733
+ typeof (repository as { runDeferred?: unknown }).runDeferred !==
734
+ 'function' ||
735
+ (requireCohort &&
736
+ typeof (repository as { runCohort?: unknown }).runCohort !== 'function')
663
737
  ) {
664
738
  throw new TypeError(
665
- `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`,
739
+ requireCohort
740
+ ? `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive, runCohort, and runDeferred`
741
+ : `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`,
666
742
  );
667
743
  }
668
744
  return repository as XStateRepositoryCapability;
@@ -881,14 +957,7 @@ interface XStatePlaybookRuntimeSpecBase<TOptions> {
881
957
  entryEvent?: {
882
958
  type: string;
883
959
  textField: string;
884
- /**
885
- * DR-034: the FSM context member this machine's entry action copies the
886
- * exact Boss text into. Where it is named, the failure-state retry
887
- * builds its payload from that member of the live snapshot instead of
888
- * from the process-local recorded event, so the action derives the same
889
- * before and after `restore`. Absent: the recorded event stays the
890
- * source and the action lives only as long as the process.
891
- */
960
+ /** @deprecated Step checkpoints now carry the accepted input directly. */
892
961
  contextField?: string;
893
962
  };
894
963
  /**
@@ -1118,7 +1187,7 @@ export function normalizeErrorFull(
1118
1187
  // slc/link.md §Abort: cancellation is causal identity with the applicable
1119
1188
  // signal's reason — never an `AbortError` name, never bare signal state. A
1120
1189
  // distinct failure observed while the signal is aborted stays a non-abort
1121
- // control error and takes precedence (mirrors DECIDE's bespoke reference).
1190
+ // control error and takes precedence.
1122
1191
  function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
1123
1192
  return signal.aborted && Object.is(error, signal.reason);
1124
1193
  }
@@ -1169,7 +1238,38 @@ function pendingBossQuestionForState(
1169
1238
  export function pendingBossQuestionFromContext(
1170
1239
  context: Record<string, unknown>,
1171
1240
  ): PlaybookPendingBossQuestionContext | undefined {
1172
- const pending = context.pendingBossQuestion;
1241
+ return pendingBossQuestionRecord(context.pendingBossQuestion);
1242
+ }
1243
+
1244
+ /**
1245
+ * DR-067 §1: the keyed form of gears2fsm's Boss-reply suspension. Each
1246
+ * well-formed `context.pendingBossQuestions[stateId]` record whose own
1247
+ * `questionId` equals its key counts only while a wait state that resumes
1248
+ * its `resumeStateId` is active, so an answered question the context
1249
+ * retains for the resumed prompt never reads as pending. Ordered by
1250
+ * question id.
1251
+ */
1252
+ function keyedPendingBossQuestions(
1253
+ context: Record<string, unknown>,
1254
+ resumesFromActiveWait: (resumeStateId: string) => boolean,
1255
+ ): PlaybookPendingBossQuestionContext[] {
1256
+ const keyed = context.pendingBossQuestions;
1257
+ if (!isPlainObject(keyed)) return [];
1258
+ const pending: PlaybookPendingBossQuestionContext[] = [];
1259
+ for (const [key, value] of Object.entries(keyed)) {
1260
+ const record = pendingBossQuestionRecord(value);
1261
+ if (record === undefined || record.questionId !== key) continue;
1262
+ if (!resumesFromActiveWait(record.resumeStateId)) continue;
1263
+ pending.push(record);
1264
+ }
1265
+ return pending.sort((left, right) =>
1266
+ left.questionId.localeCompare(right.questionId),
1267
+ );
1268
+ }
1269
+
1270
+ function pendingBossQuestionRecord(
1271
+ pending: unknown,
1272
+ ): PlaybookPendingBossQuestionContext | undefined {
1173
1273
  if (
1174
1274
  pending === undefined ||
1175
1275
  pending === null ||
@@ -1647,8 +1747,8 @@ export function defaultBuildJudgePrompt(
1647
1747
  * so the judge omits it. Rendering the clause verbatim asked the judge for
1648
1748
  * `question`, `planningResult`, or `evaluatedRevision`, which the reconciler
1649
1749
  * rejects as a structural error, spending the single correction on a
1650
- * self-inflicted defect. Exported so a bespoke linked runtime (DECIDE's
1651
- * parallel machinery) renders the identical contract instead of restating it.
1750
+ * self-inflicted defect. Exported so linked machinery of an artifact's own
1751
+ * renders the identical contract instead of restating it.
1652
1752
  */
1653
1753
  export function renderGovernedOutcomeContract(
1654
1754
  guard: string,
@@ -1731,6 +1831,7 @@ function buildGovernedJudgePrompt(
1731
1831
  '```',
1732
1832
  '',
1733
1833
  'Pick exactly one declared `guard` and reply with exactly that outcome\'s reply shape below: `guard` plus its semantic-owned fields and nothing else.',
1834
+ 'If no declared outcome matches the player output, instead reply exactly {"blocked":"explain the missing outcome or contradictory requirements"}. Never force a match or invent a guard. A blocked reply pauses the playbook for Boss; it is not completion.',
1734
1835
  'A field listed as runtime-supplied is owned by presentation, effect, or runtime evidence; the runtime fills it itself, and a reply that includes one is structurally invalid.',
1735
1836
  '',
1736
1837
  ];
@@ -1879,6 +1980,7 @@ interface PlayerBridgeSpec {
1879
1980
  adjudication: PlayerAdjudicationSpec;
1880
1981
  resumableStateIds: ReadonlySet<string>;
1881
1982
  allowsCorrectiveReplay?: (result: PlayerResult) => boolean;
1983
+ run?: (input: PlaybookPlayerInput, signal: AbortSignal, execute: () => Promise<PlaybookActorOutput>) => Promise<PlaybookActorOutput>;
1882
1984
  }
1883
1985
 
1884
1986
  export function createPlayerBridge(
@@ -1888,8 +1990,7 @@ export function createPlayerBridge(
1888
1990
  boundary?: RuntimeBoundaryCalls,
1889
1991
  onControlPlaneError?: (error: unknown) => void,
1890
1992
  ): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput> {
1891
- return fromPromise<PlaybookActorOutput, PlaybookPlayerInput>(
1892
- async ({ input, signal }) => {
1993
+ const execute = async ({ input, signal }: { input: PlaybookPlayerInput; signal: AbortSignal }): Promise<PlaybookActorOutput> => {
1893
1994
  const activeSignal = combineAbortSignals(signal, getActiveSignal?.());
1894
1995
  let roleId: string;
1895
1996
  let prompt: string;
@@ -1943,7 +2044,7 @@ export function createPlayerBridge(
1943
2044
  const failure = new Error(
1944
2045
  result.error ?? `captainBridge: callPlayer status "${result.status}"`,
1945
2046
  );
1946
- throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
2047
+ throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
1947
2048
  }
1948
2049
  const finalText = result.finalText ?? '';
1949
2050
  if (isEmptyFinalText(finalText)) {
@@ -1979,8 +2080,9 @@ export function createPlayerBridge(
1979
2080
  }
1980
2081
  throw error;
1981
2082
  }
1982
- },
1983
- );
2083
+ };
2084
+ return fromPromise<PlaybookActorOutput, PlaybookPlayerInput>((args) => spec.run
2085
+ ? spec.run(args.input, args.signal, () => execute(args)) : execute(args));
1984
2086
  }
1985
2087
 
1986
2088
  // ---------------------------------------------------------------------------
@@ -2170,7 +2272,81 @@ function transitionTargets(transition: unknown): string[] {
2170
2272
  return targets;
2171
2273
  }
2172
2274
 
2173
- /** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
2275
+ function transitionArmTargets(transition: unknown): string[] {
2276
+ const arms = Array.isArray(transition) ? transition : [transition];
2277
+ const targets: string[] = [];
2278
+ for (const arm of arms) {
2279
+ if (typeof arm === 'string') {
2280
+ targets.push(arm);
2281
+ } else if (isPlainObject(arm) && typeof arm.target === 'string') {
2282
+ targets.push(arm.target);
2283
+ }
2284
+ }
2285
+ return targets;
2286
+ }
2287
+
2288
+ function playbookStateIdOf(stateDef: unknown): string | undefined {
2289
+ if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
2290
+ return undefined;
2291
+ }
2292
+ const playbook = stateDef.meta.playbook;
2293
+ return isPlainObject(playbook) && typeof playbook.stateId === 'string'
2294
+ ? playbook.stateId
2295
+ : undefined;
2296
+ }
2297
+
2298
+ function stateTagsOf(stateDef: Record<string, unknown>): readonly string[] {
2299
+ const tags = stateDef.tags;
2300
+ if (typeof tags === 'string') return [tags];
2301
+ return Array.isArray(tags)
2302
+ ? tags.filter((tag): tag is string => typeof tag === 'string')
2303
+ : [];
2304
+ }
2305
+
2306
+ /** Every explicit node `id` in the config, mapped to its playbook state id. */
2307
+ function playbookStateIdsByNodeId(config: unknown): ReadonlyMap<string, string> {
2308
+ const byNodeId = new Map<string, string>();
2309
+ const visit = (stateDef: unknown): void => {
2310
+ if (!isPlainObject(stateDef)) return;
2311
+ const stateId = playbookStateIdOf(stateDef);
2312
+ if (typeof stateDef.id === 'string' && stateId !== undefined) {
2313
+ byNodeId.set(stateDef.id, stateId);
2314
+ }
2315
+ if (isPlainObject(stateDef.states)) {
2316
+ for (const child of Object.values(stateDef.states)) visit(child);
2317
+ }
2318
+ };
2319
+ if (isPlainObject(config) && isPlainObject(config.states)) {
2320
+ for (const child of Object.values(config.states)) visit(child);
2321
+ }
2322
+ return byNodeId;
2323
+ }
2324
+
2325
+ /**
2326
+ * One transition target written inside a parallel region, resolved to the
2327
+ * playbook state id it names: an `#id` reference through the node ids, and a
2328
+ * bare sibling key through the region's own leaves.
2329
+ */
2330
+ function resolveRegionTarget(
2331
+ target: string,
2332
+ regionStates: Record<string, unknown>,
2333
+ byNodeId: ReadonlyMap<string, string>,
2334
+ ): string {
2335
+ if (target.startsWith('#')) {
2336
+ const nodeId = target.slice(1);
2337
+ return byNodeId.get(nodeId) ?? nodeId;
2338
+ }
2339
+ if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
2340
+ return playbookStateIdOf(regionStates[target]) ?? target;
2341
+ }
2342
+ return target;
2343
+ }
2344
+
2345
+ /**
2346
+ * Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
2347
+ * the parallel shape gears2fsm compiles (DR-067) — the targets of each
2348
+ * region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
2349
+ */
2174
2350
  export function resumableStateIdsFromMachine(
2175
2351
  machine: AnyStateMachine,
2176
2352
  ): ReadonlySet<string> {
@@ -2178,13 +2354,321 @@ export function resumableStateIdsFromMachine(
2178
2354
  if (!isPlainObject(config) || !isPlainObject(config.states)) {
2179
2355
  return new Set();
2180
2356
  }
2357
+ const resumable = new Set<string>();
2181
2358
  const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
2182
- if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
2183
- return new Set();
2359
+ if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
2360
+ const bossReply = awaitState.on.BOSS_REPLY;
2361
+ if (bossReply !== undefined) {
2362
+ for (const target of transitionTargets(bossReply)) resumable.add(target);
2363
+ }
2364
+ }
2365
+ const byNodeId = playbookStateIdsByNodeId(config);
2366
+ for (const stateDef of Object.values(config.states)) {
2367
+ if (
2368
+ !isPlainObject(stateDef) ||
2369
+ stateDef.type !== 'parallel' ||
2370
+ !isPlainObject(stateDef.states)
2371
+ ) {
2372
+ continue;
2373
+ }
2374
+ for (const region of Object.values(stateDef.states)) {
2375
+ if (!isPlainObject(region) || !isPlainObject(region.states)) continue;
2376
+ const regionStates = region.states;
2377
+ for (const leaf of Object.values(regionStates)) {
2378
+ if (!isPlainObject(leaf) || !isPlainObject(leaf.on)) continue;
2379
+ const bossReply = leaf.on.BOSS_REPLY;
2380
+ if (bossReply === undefined) continue;
2381
+ for (const target of transitionArmTargets(bossReply)) {
2382
+ resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
2383
+ }
2384
+ }
2385
+ }
2386
+ }
2387
+ return resumable;
2388
+ }
2389
+
2390
+ /** One region of a compiled parallel state (DR-067 §1). */
2391
+ interface XStateParallelRegion {
2392
+ readonly key: string;
2393
+ readonly roleId: string;
2394
+ readonly workingStateId: string;
2395
+ readonly waitStateId: string;
2396
+ readonly finalStateId: string;
2397
+ }
2398
+
2399
+ /** One root `type: 'parallel'` state of the compiled shape. */
2400
+ interface XStateParallelState {
2401
+ readonly stateId: string;
2402
+ readonly regions: readonly XStateParallelRegion[];
2403
+ }
2404
+
2405
+ /**
2406
+ * The parallel profile derived from a machine that declares a parallel
2407
+ * state (DR-067 §1): its parallel states, each wait state's resume targets
2408
+ * — region waits and the root reply wait alike — and, per state id, the
2409
+ * config nodes whose `on` maps an active state contributes.
2410
+ */
2411
+ interface XStateParallelProfile {
2412
+ readonly states: readonly XStateParallelState[];
2413
+ readonly parallelStateByWorkingStateId: ReadonlyMap<
2414
+ string,
2415
+ XStateParallelState
2416
+ >;
2417
+ readonly waitResumeTargets: ReadonlyMap<string, ReadonlySet<string>>;
2418
+ readonly transitionNodesByStateId: ReadonlyMap<
2419
+ string,
2420
+ readonly Record<string, unknown>[]
2421
+ >;
2422
+ }
2423
+
2424
+ /** One registered cohort member awaiting its cohort's settlement. */
2425
+ interface XStateCohortMember {
2426
+ readonly region: XStateParallelRegion;
2427
+ readonly input: PlaybookPlayerInput;
2428
+ readonly roleId: string;
2429
+ readonly playerId: string | undefined;
2430
+ readonly playerKey: string;
2431
+ readonly signal: AbortSignal;
2432
+ readonly effectBoundary: XStateEffectBoundarySeed;
2433
+ readonly run: (
2434
+ callSignal: AbortSignal,
2435
+ cohortCancellation: AbortSignal,
2436
+ preExistingBlock: string | undefined,
2437
+ ) => Promise<PlayerResult>;
2438
+ readonly reportCollision: (callSignal: AbortSignal) => Promise<never>;
2439
+ readonly settled: DeferredValue<PlayerResult>;
2440
+ }
2441
+
2442
+ /** The cohort of one parallel state within one public boundary. */
2443
+ interface PendingCohort {
2444
+ readonly boundarySequence: number;
2445
+ readonly members: Map<string, XStateCohortMember>;
2446
+ started: boolean;
2447
+ }
2448
+
2449
+ function malformedParallelState(
2450
+ label: string,
2451
+ key: string,
2452
+ reason: string,
2453
+ ): Error {
2454
+ return new Error(
2455
+ `${label} parallel state ${key} is not the compiled parallel shape: ${reason}`,
2456
+ );
2457
+ }
2458
+
2459
+ /**
2460
+ * DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
2461
+ * `type: 'parallel'` states of the one shape gears2fsm compiles — each
2462
+ * region a compound state with exactly one player-invoking `playbook.busy`
2463
+ * working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
2464
+ * arms resume that working leaf, and exactly one final leaf, every leaf
2465
+ * carrying a stable `meta.playbook.stateId` distinct from every other
2466
+ * state's, and the parent's `onDone` the join. Any other compound state
2467
+ * keeps the flat-domain rejection. Returns `undefined` for a flat machine.
2468
+ */
2469
+ function parallelProfileFromMachine(
2470
+ machine: AnyStateMachine,
2471
+ label: string,
2472
+ ): XStateParallelProfile | undefined {
2473
+ const config = (machine as unknown as { config?: unknown }).config;
2474
+ if (!isPlainObject(config) || !isPlainObject(config.states)) {
2475
+ return undefined;
2476
+ }
2477
+ const rootStates = config.states;
2478
+ const parallelKeys = Object.entries(rootStates)
2479
+ .filter(
2480
+ ([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel',
2481
+ )
2482
+ .map(([key]) => key);
2483
+ for (const stateDef of Object.values(rootStates)) {
2484
+ if (
2485
+ isPlainObject(stateDef) &&
2486
+ stateDef.type !== 'parallel' &&
2487
+ isPlainObject(stateDef.states) &&
2488
+ Object.keys(stateDef.states).length > 0
2489
+ ) {
2490
+ throw new Error(
2491
+ `${label} declares a compound state; the shared runtime supports only flat single-region FSMs`,
2492
+ );
2493
+ }
2494
+ }
2495
+ if (parallelKeys.length === 0) return undefined;
2496
+
2497
+ const byNodeId = playbookStateIdsByNodeId(config);
2498
+ const seenStateIds = new Set<string>();
2499
+ for (const stateDef of Object.values(rootStates)) {
2500
+ const stateId = playbookStateIdOf(stateDef);
2501
+ if (stateId !== undefined) seenStateIds.add(stateId);
2502
+ }
2503
+ const states: XStateParallelState[] = [];
2504
+ const parallelStateByWorkingStateId = new Map<string, XStateParallelState>();
2505
+ const waitResumeTargets = new Map<string, ReadonlySet<string>>();
2506
+ const transitionNodesByStateId = new Map<
2507
+ string,
2508
+ readonly Record<string, unknown>[]
2509
+ >();
2510
+ for (const key of parallelKeys) {
2511
+ const parent = rootStates[key] as Record<string, unknown>;
2512
+ if (parent.onDone === undefined) {
2513
+ throw malformedParallelState(label, key, 'it declares no onDone join');
2514
+ }
2515
+ if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
2516
+ throw malformedParallelState(
2517
+ label,
2518
+ key,
2519
+ 'it must declare at least two regions',
2520
+ );
2521
+ }
2522
+ const regions: XStateParallelRegion[] = [];
2523
+ const roleIds = new Set<string>();
2524
+ for (const [regionKey, region] of Object.entries(parent.states)) {
2525
+ const regionReason = (reason: string) =>
2526
+ malformedParallelState(label, key, `region ${regionKey} ${reason}`);
2527
+ if (
2528
+ !isPlainObject(region) ||
2529
+ region.type === 'parallel' ||
2530
+ region.type === 'final' ||
2531
+ region.type === 'history' ||
2532
+ !isPlainObject(region.states)
2533
+ ) {
2534
+ throw regionReason('must be a compound state of leaves');
2535
+ }
2536
+ const regionStates = region.states;
2537
+ let working: [string, Record<string, unknown>] | undefined;
2538
+ let wait: [string, Record<string, unknown>] | undefined;
2539
+ let final: [string, Record<string, unknown>] | undefined;
2540
+ for (const [leafKey, leaf] of Object.entries(regionStates)) {
2541
+ if (!isPlainObject(leaf)) {
2542
+ throw regionReason(`leaf ${leafKey} must be a state object`);
2543
+ }
2544
+ if (
2545
+ (isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
2546
+ leaf.type === 'parallel' ||
2547
+ leaf.type === 'history'
2548
+ ) {
2549
+ throw regionReason(`leaf ${leafKey} must be an atomic state`);
2550
+ }
2551
+ const leafStateId = playbookStateIdOf(leaf);
2552
+ if (leafStateId === undefined || leafStateId.trim().length === 0) {
2553
+ throw regionReason(
2554
+ `leaf ${leafKey} must carry a string meta.playbook.stateId`,
2555
+ );
2556
+ }
2557
+ if (seenStateIds.has(leafStateId)) {
2558
+ throw regionReason(
2559
+ `leaf ${leafKey} reuses playbook state id ${leafStateId}`,
2560
+ );
2561
+ }
2562
+ seenStateIds.add(leafStateId);
2563
+ const invoke = leaf.invoke;
2564
+ const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
2565
+ const tags = stateTagsOf(leaf);
2566
+ if (leaf.type === 'final') {
2567
+ if (final !== undefined) throw regionReason('declares two final leaves');
2568
+ final = [leafKey, leaf];
2569
+ } else if (
2570
+ invokes.length === 1 &&
2571
+ isPlainObject(invokes[0]) &&
2572
+ invokes[0].src === 'player' &&
2573
+ tags.includes('playbook.busy')
2574
+ ) {
2575
+ if (working !== undefined) {
2576
+ throw regionReason('declares two working leaves');
2577
+ }
2578
+ working = [leafKey, leaf];
2579
+ } else if (
2580
+ invokes.length === 0 &&
2581
+ tags.includes('playbook.parked') &&
2582
+ isPlainObject(leaf.on) &&
2583
+ leaf.on.BOSS_REPLY !== undefined
2584
+ ) {
2585
+ if (wait !== undefined) throw regionReason('declares two wait leaves');
2586
+ wait = [leafKey, leaf];
2587
+ } else {
2588
+ throw regionReason(
2589
+ `leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`,
2590
+ );
2591
+ }
2592
+ }
2593
+ if (working === undefined || wait === undefined || final === undefined) {
2594
+ throw regionReason(
2595
+ 'must hold exactly one working leaf, one wait leaf, and one final leaf',
2596
+ );
2597
+ }
2598
+ if (region.initial !== working[0]) {
2599
+ throw regionReason('must start in its working leaf');
2600
+ }
2601
+ const workingStateId = playbookStateIdOf(working[1])!;
2602
+ const waitStateId = playbookStateIdOf(wait[1])!;
2603
+ const finalStateId = playbookStateIdOf(final[1])!;
2604
+ const replyTargets = transitionArmTargets(
2605
+ (wait[1].on as Record<string, unknown>).BOSS_REPLY,
2606
+ ).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
2607
+ const regionStateIds = new Set([waitStateId, finalStateId]);
2608
+ if (
2609
+ !replyTargets.includes(workingStateId) ||
2610
+ replyTargets.some((target) => regionStateIds.has(target))
2611
+ ) {
2612
+ throw regionReason(
2613
+ 'wait leaf BOSS_REPLY arms must resume its working leaf',
2614
+ );
2615
+ }
2616
+ const workingMeta = (working[1].meta as Record<string, unknown>)
2617
+ .playbook as Record<string, unknown>;
2618
+ const roleId = workingMeta.role;
2619
+ if (typeof roleId !== 'string' || roleId.trim().length === 0) {
2620
+ throw regionReason('working leaf must declare meta.playbook.role');
2621
+ }
2622
+ if (roleIds.has(roleId)) {
2623
+ throw malformedParallelState(
2624
+ label,
2625
+ key,
2626
+ `role ${roleId} works in more than one region`,
2627
+ );
2628
+ }
2629
+ roleIds.add(roleId);
2630
+ regions.push({
2631
+ key: regionKey,
2632
+ roleId,
2633
+ workingStateId,
2634
+ waitStateId,
2635
+ finalStateId,
2636
+ });
2637
+ waitResumeTargets.set(waitStateId, new Set([workingStateId]));
2638
+ for (const [, leaf] of [working, wait, final]) {
2639
+ transitionNodesByStateId.set(playbookStateIdOf(leaf)!, [region, leaf]);
2640
+ }
2641
+ }
2642
+ const parallelState: XStateParallelState = {
2643
+ stateId: key,
2644
+ regions: Object.freeze(regions),
2645
+ };
2646
+ states.push(parallelState);
2647
+ for (const region of regions) {
2648
+ parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
2649
+ }
2650
+ transitionNodesByStateId.set(key, [parent]);
2651
+ }
2652
+ for (const [key, stateDef] of Object.entries(rootStates)) {
2653
+ if (!isPlainObject(stateDef) || parallelKeys.includes(key)) continue;
2654
+ transitionNodesByStateId.set(key, [stateDef]);
2655
+ }
2656
+ const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
2657
+ if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
2658
+ const bossReply = rootWait.on.BOSS_REPLY;
2659
+ if (bossReply !== undefined) {
2660
+ waitResumeTargets.set(
2661
+ BOSS_REPLY_WAIT_STATE_ID,
2662
+ new Set(transitionTargets(bossReply)),
2663
+ );
2664
+ }
2184
2665
  }
2185
- const bossReply = awaitState.on.BOSS_REPLY;
2186
- if (bossReply === undefined) return new Set();
2187
- return new Set(transitionTargets(bossReply));
2666
+ return Object.freeze({
2667
+ states: Object.freeze(states),
2668
+ parallelStateByWorkingStateId,
2669
+ waitResumeTargets,
2670
+ transitionNodesByStateId,
2671
+ });
2188
2672
  }
2189
2673
 
2190
2674
  // ---------------------------------------------------------------------------
@@ -2742,11 +3226,12 @@ function makeDefaultStatusesForState(
2742
3226
  }
2743
3227
  return [
2744
3228
  ...statuses,
2745
- { message: `${askerLabel(pending.asker)} asks: ${pending.question}` },
3229
+ { message: `${askerLabel(pending.asker)} asks: ${pending.question}`, data: { kind: 'boss-question' } },
2746
3230
  {
2747
3231
  message:
2748
3232
  `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
2749
3233
  `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
3234
+ data: { kind: 'boss-question' },
2750
3235
  },
2751
3236
  ];
2752
3237
  }
@@ -2997,10 +3482,63 @@ function eventContractPrompt(contract: XStateBossEventSpec): string {
2997
3482
  }`;
2998
3483
  }
2999
3484
 
3485
+ /**
3486
+ * DR-067 §1: the questions pending in a parallel-declaring machine — the
3487
+ * keyed records whose resume state an active wait state (a region wait leaf
3488
+ * or the root reply wait) resumes.
3489
+ */
3490
+ function parallelPendingBossQuestions(
3491
+ profile: XStateParallelProfile,
3492
+ activeStateIds: readonly string[],
3493
+ context: Record<string, unknown>,
3494
+ ): PlaybookPendingBossQuestionContext[] {
3495
+ return keyedPendingBossQuestions(context, (resumeStateId) =>
3496
+ activeStateIds.some(
3497
+ (stateId) =>
3498
+ profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true,
3499
+ ),
3500
+ );
3501
+ }
3502
+
3503
+ /** Event types the root and every active state node of a parallel machine configure. */
3504
+ function parallelConfiguredEventTypes(
3505
+ machine: AnyStateMachine,
3506
+ profile: XStateParallelProfile,
3507
+ activeStateIds: readonly string[],
3508
+ ): ReadonlySet<string> {
3509
+ const configured = new Set<string>();
3510
+ const config = (machine as unknown as { config?: unknown }).config;
3511
+ if (!isPlainObject(config)) return configured;
3512
+ if (isPlainObject(config.on)) {
3513
+ for (const type of Object.keys(config.on)) configured.add(type);
3514
+ }
3515
+ for (const stateId of activeStateIds) {
3516
+ for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
3517
+ if (!isPlainObject(node.on)) continue;
3518
+ for (const type of Object.keys(node.on)) configured.add(type);
3519
+ }
3520
+ }
3521
+ return configured;
3522
+ }
3523
+
3524
+ function activeStateIdsOf(snapshotOrState: unknown): readonly string[] {
3525
+ const listed = (snapshotOrState as { activeStateIds?: unknown } | null)
3526
+ ?.activeStateIds;
3527
+ if (Array.isArray(listed)) {
3528
+ return listed.filter((stateId): stateId is string => typeof stateId === 'string');
3529
+ }
3530
+ try {
3531
+ return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
3532
+ } catch {
3533
+ return [];
3534
+ }
3535
+ }
3536
+
3000
3537
  function makeDefaultClassifyBossText(
3001
3538
  machine: AnyStateMachine,
3002
3539
  entryEvent: { type: string; textField: string } | undefined,
3003
3540
  bossEvents: readonly XStateBossEventSpec[],
3541
+ parallelProfile?: XStateParallelProfile,
3004
3542
  ): (
3005
3543
  text: string,
3006
3544
  ports: PlaybookPorts,
@@ -3020,16 +3558,54 @@ function makeDefaultClassifyBossText(
3020
3558
  // is answered history, so the prompt must not present it as pending —
3021
3559
  // a judge told a question awaits at the failure state is steered toward
3022
3560
  // a reply it cannot select or toward no action at all.
3023
- const pending =
3024
- stateId === BOSS_REPLY_WAIT_STATE_ID
3025
- ? pendingBossQuestionFromContext(state.context)
3026
- : undefined;
3027
- const configuredTypes = configuredEventTypesForState(machine, stateId);
3028
- const applicable = [...contracts.values()].filter(
3029
- (contract) =>
3030
- configuredTypes.has(contract.type) &&
3031
- (contract.type !== 'BOSS_REPLY' || pending !== undefined),
3032
- );
3561
+ let pendingQuestions: readonly PlaybookPendingBossQuestionContext[];
3562
+ let configuredTypes: ReadonlySet<string>;
3563
+ if (parallelProfile === undefined) {
3564
+ const pending =
3565
+ stateId === BOSS_REPLY_WAIT_STATE_ID
3566
+ ? pendingBossQuestionFromContext(state.context)
3567
+ : undefined;
3568
+ pendingQuestions = pending === undefined ? [] : [pending];
3569
+ configuredTypes = configuredEventTypesForState(machine, stateId);
3570
+ } else {
3571
+ // DR-067 §1: every question pending in any active wait is offered, and
3572
+ // the events of every active state node apply.
3573
+ const activeStateIds = activeStateIdsOf(snapshotOrState);
3574
+ pendingQuestions = parallelPendingBossQuestions(
3575
+ parallelProfile,
3576
+ activeStateIds,
3577
+ state.context,
3578
+ );
3579
+ configuredTypes = parallelConfiguredEventTypes(
3580
+ machine,
3581
+ parallelProfile,
3582
+ activeStateIds,
3583
+ );
3584
+ }
3585
+ // Several pending questions: a reply must name the one it answers, and
3586
+ // the classifier never guesses among them (gears2fsm §Parallel groups).
3587
+ const severalPending = pendingQuestions.length > 1;
3588
+ const applicable = [...contracts.values()]
3589
+ .filter(
3590
+ (contract) =>
3591
+ configuredTypes.has(contract.type) &&
3592
+ (contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0),
3593
+ )
3594
+ .map((contract): XStateBossEventSpec =>
3595
+ contract.type === 'BOSS_REPLY' && severalPending
3596
+ ? {
3597
+ type: 'BOSS_REPLY',
3598
+ fields: {
3599
+ questionId: {
3600
+ source: 'judge',
3601
+ required: true,
3602
+ values: pendingQuestions.map(({ questionId }) => questionId),
3603
+ },
3604
+ answer: { source: 'text', required: true },
3605
+ },
3606
+ }
3607
+ : contract,
3608
+ );
3033
3609
 
3034
3610
  const lines = [
3035
3611
  'Classify the following Boss message into exactly one event.',
@@ -3038,13 +3614,18 @@ function makeDefaultClassifyBossText(
3038
3614
  '',
3039
3615
  `Current state: ${currentState}`,
3040
3616
  ];
3041
- if (pending !== undefined) {
3617
+ for (const pending of pendingQuestions) {
3042
3618
  lines.push(
3043
3619
  `Pending question id: ${pending.questionId}`,
3044
3620
  `Pending asker: ${askerLabel(pending.asker)}`,
3045
3621
  `Pending Boss question: ${pending.question}`,
3046
3622
  );
3047
3623
  }
3624
+ if (severalPending) {
3625
+ lines.push(
3626
+ 'Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.',
3627
+ );
3628
+ }
3048
3629
  lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
3049
3630
  for (const contract of applicable) {
3050
3631
  lines.push(`- ${eventContractPrompt(contract)}`);
@@ -3137,20 +3718,23 @@ function makeDefaultClassifyBossText(
3137
3718
  }
3138
3719
 
3139
3720
  if (eventType === 'BOSS_REPLY') {
3140
- if (pending === undefined) {
3721
+ if (pendingQuestions.length === 0) {
3141
3722
  await ports.emitStatus(
3142
3723
  'Classifier returned BOSS_REPLY without a pending question',
3143
3724
  );
3144
3725
  return undefined;
3145
3726
  }
3146
- const questionId = event.questionId;
3147
- if (questionId !== undefined && questionId !== pending.questionId) {
3148
- await ports.emitStatus(
3149
- `Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`,
3150
- );
3151
- return undefined;
3727
+ if (!severalPending) {
3728
+ const pending = pendingQuestions[0]!;
3729
+ const questionId = event.questionId;
3730
+ if (questionId !== undefined && questionId !== pending.questionId) {
3731
+ await ports.emitStatus(
3732
+ `Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`,
3733
+ );
3734
+ return undefined;
3735
+ }
3736
+ event.questionId = pending.questionId;
3152
3737
  }
3153
- event.questionId = pending.questionId;
3154
3738
  }
3155
3739
  const can = (snapshotOrState as { can?: unknown } | null)?.can;
3156
3740
  if (
@@ -3187,34 +3771,6 @@ interface TracePosition {
3187
3771
  callId?: string;
3188
3772
  }
3189
3773
 
3190
- function machineDeclaresParallelState(machine: AnyStateMachine): boolean {
3191
- const visit = (stateDef: unknown): boolean => {
3192
- if (!isPlainObject(stateDef)) return false;
3193
- if (stateDef.type === 'parallel') return true;
3194
- if (!isPlainObject(stateDef.states)) return false;
3195
- return Object.values(stateDef.states).some(visit);
3196
- };
3197
- return visit((machine as unknown as { config?: unknown }).config);
3198
- }
3199
-
3200
- // PBRT-52: the factory's domain is FLAT single-region machines — every
3201
- // state a direct child of the root, so each snapshot exposes exactly one
3202
- // playbook state id and every state-keyed lookup (deterministic entries,
3203
- // retry, reply-wait pendingness, configured events, descriptions) indexes
3204
- // one unambiguous identity. A compound child would be accepted and then
3205
- // silently misbehave on all of those gates, so it is rejected up front
3206
- // exactly like a parallel region.
3207
- function machineDeclaresNestedState(machine: AnyStateMachine): boolean {
3208
- const config = (machine as unknown as { config?: unknown }).config;
3209
- if (!isPlainObject(config) || !isPlainObject(config.states)) return false;
3210
- return Object.values(config.states).some(
3211
- (stateDef) =>
3212
- isPlainObject(stateDef) &&
3213
- isPlainObject(stateDef.states) &&
3214
- Object.keys(stateDef.states).length > 0,
3215
- );
3216
- }
3217
-
3218
3774
  // PBRT-52: the factory's lookups index states by their root key, and the
3219
3775
  // published playbook identity is `meta.playbook.stateId` — the two must
3220
3776
  // coincide or a machine can advertise a pending question or retry under an
@@ -3309,10 +3865,12 @@ function assertUnfinishedFinalStateIds(
3309
3865
  * the optional parked-session snapshot capability (DR-014) and the retained-
3310
3866
  * snapshot adoption capability (DR-038).
3311
3867
  *
3312
- * Scope: flat single-region machines — no parallel state, no compound
3313
- * child states, and every root state's `meta.playbook.stateId` equal to its
3314
- * state key — so each snapshot exposes exactly one playbook state id.
3315
- * Parallel-region FSMs keep their own linked runtimes.
3868
+ * Scope: flat single-region machines — every root state's
3869
+ * `meta.playbook.stateId` equal to its state key, so each snapshot exposes
3870
+ * exactly one playbook state id — plus the parallel profile of DR-067: root
3871
+ * `type: 'parallel'` states of the shape gears2fsm compiles, whose regions
3872
+ * run as one all-`unchanged` repository cohort with keyed pending questions.
3873
+ * A machine declaring a parallel state omits `adopt`.
3316
3874
  */
3317
3875
  export function createXStatePlaybookRuntime<
3318
3876
  TOptions,
@@ -3348,16 +3906,9 @@ export function createXStatePlaybookRuntime<
3348
3906
  `${label} artifacts must not derive concrete player bindings`,
3349
3907
  );
3350
3908
  }
3351
- if (machineDeclaresParallelState(machine)) {
3352
- throw new Error(
3353
- `${label} uses a parallel state; the shared runtime supports only single-region FSMs`,
3354
- );
3355
- }
3356
- if (machineDeclaresNestedState(machine)) {
3357
- throw new Error(
3358
- `${label} declares a compound state; the shared runtime supports only flat single-region FSMs`,
3359
- );
3360
- }
3909
+ // DR-067 §1: a flat machine, or one whose only compound states are root
3910
+ // parallel states of the compiled shape; everything else is rejected.
3911
+ const parallelProfile = parallelProfileFromMachine(machine, label);
3361
3912
  assertFlatStateIdentity(machine, label);
3362
3913
  assertUnfinishedFinalStateIds(
3363
3914
  spec.unfinishedFinalStateIds,
@@ -3398,6 +3949,18 @@ export function createXStatePlaybookRuntime<
3398
3949
  const declaredRoleIds = Object.freeze([
3399
3950
  ...new Set([...roleStates.values()].map(({ role }) => role)),
3400
3951
  ]);
3952
+ // DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
3953
+ // each player state, each wait state, and the failure state. A status is
3954
+ // scheduled for each one newly entered, and a status trace names a
3955
+ // `stateId` only while exactly one is active.
3956
+ const bossRelevantStateIds: ReadonlySet<string> | undefined =
3957
+ parallelProfile === undefined
3958
+ ? undefined
3959
+ : new Set([
3960
+ ...roleStates.keys(),
3961
+ ...parallelProfile.waitResumeTargets.keys(),
3962
+ 'failed',
3963
+ ]);
3401
3964
  // PBRT-52: the artifact's own ControlView context projection. Nothing is
3402
3965
  // exported by default, so an FSM context member — including one added
3403
3966
  // after this artifact was linked — is private until named here. The two
@@ -3462,6 +4025,7 @@ export function createXStatePlaybookRuntime<
3462
4025
  machine,
3463
4026
  spec.entryEvent,
3464
4027
  spec.bossEvents ?? [],
4028
+ parallelProfile,
3465
4029
  );
3466
4030
  const classifyBossText: NonNullable<
3467
4031
  XStatePlaybookRuntimeSpec<TOptions>['classifyBossText']
@@ -3485,20 +4049,140 @@ export function createXStatePlaybookRuntime<
3485
4049
  return typeof cwd === 'string' ? cwd : undefined;
3486
4050
  });
3487
4051
 
3488
- const createPlaybookRuntime = function createPlaybookRuntime(
3489
- factoryOptions: unknown,
3490
- ): PlaybookRuntime {
3491
- const construction = configuredOptionsFromFactoryInput(
3492
- factoryOptions,
3493
- label,
4052
+ // DR-067 §1: the questions pending in a state — the singular reply-wait
4053
+ // question of a flat machine, or a parallel machine's keyed questions
4054
+ // whose own wait state is active.
4055
+ const pendingBossQuestionsFor = (
4056
+ state: PlaybookState,
4057
+ context: Record<string, unknown>,
4058
+ ): readonly PlaybookPendingBossQuestionContext[] => {
4059
+ if (parallelProfile === undefined) {
4060
+ const pending = pendingBossQuestionForState(state, context);
4061
+ return pending === undefined ? [] : [pending];
4062
+ }
4063
+ return parallelPendingBossQuestions(
4064
+ parallelProfile,
4065
+ state.activeStateIds,
4066
+ context,
3494
4067
  );
3495
- const configuredOptions = construction.configuredOptions as TOptions;
3496
- const effectLedgerCapability = construction.effectLedger;
3497
- const hasGovernedPlayerStates =
3498
- Object.keys(outcomeAuthority.governedPlayerStates).length > 0;
3499
- const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer(
3500
- (source, acceptedOutcome) => {
3501
- const governedPlayerStates = outcomeAuthority.governedPlayerStates;
4068
+ };
4069
+ const singlePendingBossQuestion = (
4070
+ state: PlaybookState,
4071
+ context: Record<string, unknown>,
4072
+ ): PlaybookPendingBossQuestionContext | undefined => {
4073
+ const pending = pendingBossQuestionsFor(state, context);
4074
+ return pending.length === 1 ? pending[0] : undefined;
4075
+ };
4076
+ // DR-067 §1: a status trace names a `stateId` only while exactly one
4077
+ // Boss-relevant state is active; a flat machine's is its one state.
4078
+ const statusTraceStateId = (state: PlaybookState): string | undefined => {
4079
+ if (bossRelevantStateIds === undefined) return state.stateId;
4080
+ const relevant = state.activeStateIds.filter((stateId) =>
4081
+ bossRelevantStateIds.has(stateId),
4082
+ );
4083
+ return relevant.length === 1 ? relevant[0] : undefined;
4084
+ };
4085
+ // DR-067 §1: a parallel machine schedules statuses for each newly entered
4086
+ // Boss-relevant state — a player state's role line, a wait state's
4087
+ // question lines, and the failure line — diffed against the previous
4088
+ // snapshot's active state ids, so a sibling still running is not
4089
+ // announced again.
4090
+ const parallelStatusesFor = (
4091
+ previousState: PlaybookState | undefined,
4092
+ state: PlaybookState,
4093
+ context: Record<string, unknown>,
4094
+ ): ScheduledStatus[] => {
4095
+ const prior = new Set(previousState?.activeStateIds ?? []);
4096
+ const statuses: ScheduledStatus[] = [];
4097
+ for (const stateId of state.activeStateIds) {
4098
+ if (prior.has(stateId) || !bossRelevantStateIds!.has(stateId)) continue;
4099
+ const resumeTargets = parallelProfile!.waitResumeTargets.get(stateId);
4100
+ if (resumeTargets !== undefined) {
4101
+ const questions = keyedPendingBossQuestions(context, (resumeStateId) =>
4102
+ resumeTargets.has(resumeStateId),
4103
+ );
4104
+ if (questions.length === 0) {
4105
+ statuses.push({ message: 'Awaiting Boss reply.' });
4106
+ continue;
4107
+ }
4108
+ for (const pending of questions) {
4109
+ statuses.push(
4110
+ {
4111
+ message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
4112
+ data: { kind: 'boss-question' },
4113
+ },
4114
+ {
4115
+ message:
4116
+ `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
4117
+ `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
4118
+ data: { kind: 'boss-question' },
4119
+ },
4120
+ );
4121
+ }
4122
+ continue;
4123
+ }
4124
+ if (stateId === 'failed') {
4125
+ const lastError = normalizeErrorCompact(context.lastError);
4126
+ statuses.push({
4127
+ message: '◆ workflow failed; awaiting Boss recovery.',
4128
+ ...(lastError === undefined
4129
+ ? {}
4130
+ : {
4131
+ data: snapshotJsonValue({ lastError }, 'failed status data'),
4132
+ }),
4133
+ });
4134
+ continue;
4135
+ }
4136
+ const roleState = roleStates.get(stateId);
4137
+ if (roleState !== undefined) {
4138
+ statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
4139
+ }
4140
+ }
4141
+ return statuses;
4142
+ };
4143
+ // DR-067 §1: each parallel state's region roles, in region order, are one
4144
+ // concurrent role set the FSM exports; a host that forwards those sets in
4145
+ // its schema-3 authority is held to them before any agent call.
4146
+ const assertDeclaredCohorts = (hostCapabilities: object): void => {
4147
+ const authority = Object.getOwnPropertyDescriptor(
4148
+ hostCapabilities,
4149
+ 'authority',
4150
+ )?.value;
4151
+ if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
4152
+ return;
4153
+ }
4154
+ const declared = authority.concurrentRoleSets as readonly unknown[];
4155
+ for (const parallelState of parallelProfile!.states) {
4156
+ const roles = parallelState.regions.map(({ roleId }) => roleId);
4157
+ if (
4158
+ !declared.some(
4159
+ (candidate) =>
4160
+ Array.isArray(candidate) &&
4161
+ candidate.length === roles.length &&
4162
+ candidate.every((roleId, index) => roleId === roles[index]),
4163
+ )
4164
+ ) {
4165
+ throw new TypeError(
4166
+ `${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`,
4167
+ );
4168
+ }
4169
+ }
4170
+ };
4171
+
4172
+ const createPlaybookRuntime = function createPlaybookRuntime(
4173
+ factoryOptions: unknown,
4174
+ ): PlaybookRuntime {
4175
+ const construction = configuredOptionsFromFactoryInput(
4176
+ factoryOptions,
4177
+ label,
4178
+ );
4179
+ const configuredOptions = construction.configuredOptions as TOptions;
4180
+ const effectLedgerCapability = construction.effectLedger;
4181
+ const hasGovernedPlayerStates =
4182
+ Object.keys(outcomeAuthority.governedPlayerStates).length > 0;
4183
+ const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer(
4184
+ (source, acceptedOutcome) => {
4185
+ const governedPlayerStates = outcomeAuthority.governedPlayerStates;
3502
4186
  if (
3503
4187
  !Object.prototype.hasOwnProperty.call(governedPlayerStates, source)
3504
4188
  ) {
@@ -3519,14 +4203,19 @@ export function createXStatePlaybookRuntime<
3519
4203
  ? repositoryCapabilityFromHostCapabilities(
3520
4204
  construction.hostCapabilities,
3521
4205
  label,
4206
+ parallelProfile !== undefined,
3522
4207
  )
3523
4208
  : undefined;
4209
+ if (parallelProfile !== undefined) {
4210
+ assertDeclaredCohorts(construction.hostCapabilities);
4211
+ }
3524
4212
  const currentEffectLedger = (): PlaybookEffectLedger =>
3525
4213
  assertPlaybookEffectLedger(
3526
4214
  effectLedgerCapability.snapshot(),
3527
4215
  `${label} current host effect ledger`,
3528
4216
  );
3529
4217
  let effectLedgerMirror = currentEffectLedger();
4218
+ let recoveryCheckpoint: PlaybookRuntimeSnapshot['recoveryCheckpoint'];
3530
4219
  let retainedEffectSourceSessionId: string | undefined;
3531
4220
  let retainedEffectReconciliation:
3532
4221
  | PlaybookRuntimeSnapshot['retainedEffectReconciliation']
@@ -3591,9 +4280,11 @@ export function createXStatePlaybookRuntime<
3591
4280
  // mutable `activeSignal` alone cannot classify a late invocation reason.
3592
4281
  let activeAborts: AbortReasonClassifier | undefined;
3593
4282
  // The bridge binds the provenance of a child result immediately before
3594
- // its promise actor settles. The next root snapshot/error consumes this
3595
- // one-shot so background settlement emissions retain their owner.
3596
- let actorSettlementAborts: AbortReasonClassifier | undefined;
4283
+ // its promise actor settles. The next root snapshot/error consumes it so
4284
+ // background settlement emissions retain their owner. A flat machine has
4285
+ // one settling actor at a time, so a later binding replaces an unconsumed
4286
+ // one; a parallel machine's settlements queue in order (DR-067 §1).
4287
+ const actorSettlementAborts: (AbortReasonClassifier | undefined)[] = [];
3597
4288
  let actorSettlementErrorAborts: AbortReasonClassifier | undefined;
3598
4289
  // Exact cancellation observed by an emission owned by the active
3599
4290
  // boundary. Ordinary runs settle from their signal/state; apply also
@@ -3643,11 +4334,29 @@ export function createXStatePlaybookRuntime<
3643
4334
  // each successful result, so it is never stale.
3644
4335
  let pendingResultFailureCause: PlaybookFailureCause | undefined;
3645
4336
  // 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
4337
+ // marker — a string, a frozen error — and for the record a machine keeps
4338
+ // in place of the thrown value, kept by identity for as long as the
3647
4339
  // 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.
4340
+ // publishes the cause decided for exactly that failure, whatever turn
4341
+ // reads it.
3650
4342
  const failureCauses = createFailureCauseRetention();
4343
+ // DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
4344
+ // result, keyed by the result itself, so concurrent region calls never
4345
+ // lend one another the single slot above.
4346
+ const resultFailureCauses = new WeakMap<object, PlaybookFailureCause>();
4347
+ // DR-067 §1: the parallel profile's cohort bookkeeping. Every public
4348
+ // boundary advances the sequence; a parallel state's working leaves run
4349
+ // as one cohort at most once per boundary, and each cohort's semantic
4350
+ // completions — which the host settles concurrently — reconcile one at a
4351
+ // time so their durable correction-budget writes cannot race.
4352
+ let publicBoundarySequence = 0;
4353
+ const consumedCohortBoundaries = new Map<string, number>();
4354
+ const pendingCohorts = new Map<string, PendingCohort>();
4355
+ const cohortCompletionQueue = new PQueue({ concurrency: 1 });
4356
+ // DR-067 §1: every player, judge, and nested call a parallel machine
4357
+ // starts, so a boundary's drain waits for a cancelled sibling whose port
4358
+ // ignores its signal before it settles.
4359
+ const activeBoundaryCalls = new Set<Promise<unknown>>();
3651
4360
  // Previous root-machine state for the inspect-driven telemetry /
3652
4361
  // status emitter. undefined before the first inspect firing.
3653
4362
  let priorState: PlaybookState | undefined;
@@ -3660,12 +4369,6 @@ export function createXStatePlaybookRuntime<
3660
4369
  let playbookCallSequence = 0;
3661
4370
  let captainCallSequence = 0;
3662
4371
  let applyCallSequence = 0;
3663
- // DR-029: the last event a public Boss boundary sent into the
3664
- // machine — classified, deterministic entry, or Boss reply — kept with
3665
- // its recorded payload so a failure-state retry action can replay the
3666
- // event that drove the run into `failed`. Process-local: the durable
3667
- // runtime snapshot does not persist it (PBRT-50).
3668
- let lastBossEvent: EventObject | undefined;
3669
4372
  // DR-029: process-local at-most-once `apply` execution — the accepted receipt
3670
4373
  // recorded for each idempotency key, returned verbatim on a repeated
3671
4374
  // key. A key whose call settled `rejected` or threw before reaching
@@ -3902,7 +4605,7 @@ export function createXStatePlaybookRuntime<
3902
4605
  function boundaryExcludesEffect(boundary: PlaybookEffectBoundary): boolean {
3903
4606
  return (
3904
4607
  boundary.logicalOperationId === undefined &&
3905
- boundary.physicalReceipt?.classification === 'unchanged'
4608
+ (boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined)
3906
4609
  );
3907
4610
  }
3908
4611
 
@@ -3974,7 +4677,7 @@ export function createXStatePlaybookRuntime<
3974
4677
  ({ sequence }) =>
3975
4678
  sequence > reconstructedGovernedPrefixSequence!,
3976
4679
  );
3977
- if (candidate === undefined || candidate.sourceStateId !== state.stateId) {
4680
+ if (candidate === undefined || candidate.restored !== undefined || candidate.sourceStateId !== state.stateId) {
3978
4681
  return;
3979
4682
  }
3980
4683
  const persisted = persistedBoundaryReconciliation(candidate, ledger);
@@ -4716,7 +5419,7 @@ export function createXStatePlaybookRuntime<
4716
5419
  ...(descriptor !== undefined
4717
5420
  ? {
4718
5421
  state: descriptor,
4719
- ...stateIdentity(descriptor.stateId),
5422
+ ...stateIdentity(statusTraceStateId(descriptor)),
4720
5423
  }
4721
5424
  : {}),
4722
5425
  },
@@ -4767,9 +5470,12 @@ export function createXStatePlaybookRuntime<
4767
5470
  // start-only fields, so it passes its own canonical pre-acceptance
4768
5471
  // base (slc/link.md §Playbook trace).
4769
5472
  finishIdentity: Record<string, unknown> = identity,
5473
+ // DR-067 §1: the per-call abort classification a parallel machine's
5474
+ // player pairs enqueue under; absent, the active boundary's.
5475
+ aborts?: AbortReasonClassifier,
4770
5476
  ): Promise<void> {
4771
5477
  try {
4772
- await emitTrace(startedType, identity, position);
5478
+ await emitTrace(startedType, identity, position, aborts);
4773
5479
  } catch (error) {
4774
5480
  if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
4775
5481
  try {
@@ -4781,6 +5487,7 @@ export function createXStatePlaybookRuntime<
4781
5487
  error: normalizeError(error),
4782
5488
  },
4783
5489
  position,
5490
+ aborts,
4784
5491
  );
4785
5492
  } catch {
4786
5493
  // Preserve the start failure after one best-effort finish attempt.
@@ -5010,12 +5717,46 @@ export function createXStatePlaybookRuntime<
5010
5717
  ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
5011
5718
  correctionBudget: { limit: 1, spent: true },
5012
5719
  };
5720
+ // DR-067 §1 / PBRT-69: a cohort's shared receipt becomes visible for
5721
+ // every member in one ledger revision — publishing only this member's
5722
+ // spend would leave the invalid half-complete cohort the ledger admits
5723
+ // nowhere.
5724
+ const cohortMembers =
5725
+ current.cohortId === undefined
5726
+ ? [current]
5727
+ : currentLedger.boundaries.filter(
5728
+ ({ cohortId }) => cohortId === current.cohortId,
5729
+ );
5730
+ const replacements = cohortMembers.map((member) => {
5731
+ if (member.boundaryId === current.boundaryId) {
5732
+ return { expected: current, next };
5733
+ }
5734
+ if (
5735
+ member.physicalReceipt !== undefined &&
5736
+ !isDeepStrictEqual(member.physicalReceipt, receipt)
5737
+ ) {
5738
+ throw new TypeError(
5739
+ `${label} correction budget cohort conflicts with its shared repository receipt`,
5740
+ );
5741
+ }
5742
+ return {
5743
+ expected: member,
5744
+ next: {
5745
+ ...member,
5746
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
5747
+ physicalReceipt: receipt,
5748
+ },
5749
+ };
5750
+ }) as [
5751
+ { readonly expected: PlaybookEffectBoundary; readonly next: PlaybookEffectBoundary },
5752
+ ...{
5753
+ readonly expected: PlaybookEffectBoundary;
5754
+ readonly next: PlaybookEffectBoundary;
5755
+ }[],
5756
+ ];
5013
5757
  const acknowledged = assertPlaybookEffectLedger(
5014
5758
  await effectLedgerCapability.writeAhead([
5015
- {
5016
- kind: 'replace-boundaries',
5017
- replacements: [{ expected: current, next }],
5018
- },
5759
+ { kind: 'replace-boundaries', replacements },
5019
5760
  ]),
5020
5761
  `${label} semantic correction budget acknowledgement`,
5021
5762
  );
@@ -5401,7 +6142,7 @@ export function createXStatePlaybookRuntime<
5401
6142
  function acknowledgeGovernedPlayerResult(
5402
6143
  value: unknown,
5403
6144
  boundaryId: string,
5404
- source: 'runExclusive' | 'runDeferred' = 'runExclusive',
6145
+ source: 'runExclusive' | 'runDeferred' | 'runCohort' = 'runExclusive',
5405
6146
  ): PlayerResult {
5406
6147
  if (!isPlainObject(value) || !isPlainObject(value.operation)) {
5407
6148
  throw new TypeError(
@@ -5512,7 +6253,7 @@ export function createXStatePlaybookRuntime<
5512
6253
  );
5513
6254
  governedOutput = undefined;
5514
6255
  }
5515
- } else if (source === 'runExclusive' && value.deferredStatus !== undefined) {
6256
+ } else if (source !== 'runDeferred' && value.deferredStatus !== undefined) {
5516
6257
  throw new TypeError(
5517
6258
  `${label} non-deferred settlement returned a deferred binding status`,
5518
6259
  );
@@ -5600,6 +6341,401 @@ export function createXStatePlaybookRuntime<
5600
6341
  );
5601
6342
  }
5602
6343
 
6344
+ let acceptedStepResult: PlaybookRuntimeSnapshot['recoveryCheckpoint'];
6345
+ let requiredRecoveryBaseline: PlaybookRepositoryReceipt['baseline'] | undefined;
6346
+
6347
+ function captureRecoveryCheckpoint(stateId: string, prompt: string, boundaryPrefix?: number): void {
6348
+ if (requiredRecoveryBaseline !== undefined) return;
6349
+ // Capture after the entry emissions drain: the invocation is now a
6350
+ // real active child in XState's persisted snapshot, before host work.
6351
+ const id = recoveryCheckpoint?.stateId === stateId ? recoveryCheckpoint.id : undefined;
6352
+ recoveryCheckpoint = undefined;
6353
+ if (!actor || currentState().stateId !== stateId) return;
6354
+ try {
6355
+ const current = currentEffectLedger();
6356
+ recoveryCheckpoint = deepFreeze({
6357
+ stateId,
6358
+ prompt,
6359
+ ...(id === undefined ? {} : { id }),
6360
+ machine: detachPersistedMachineSnapshot(actor.getPersistedSnapshot()),
6361
+ boundaryPrefix: boundaryPrefix ?? current.boundaries.length,
6362
+ });
6363
+ } catch {
6364
+ // An in-memory-only context is still executable; it simply cannot
6365
+ // promise a durable step retry. Keep the legacy control behavior.
6366
+ }
6367
+ }
6368
+
6369
+ function stepReplayIsSafe(): boolean {
6370
+ if (!recoveryCheckpoint || hasUnresolvedReconciliation()) return false;
6371
+ const current = currentEffectLedger();
6372
+ return current.boundaries
6373
+ .slice(recoveryCheckpoint.boundaryPrefix)
6374
+ .every(
6375
+ (boundary) =>
6376
+ runtimeBoundaryIsOwned(boundary) &&
6377
+ boundary.logicalOperationId === undefined &&
6378
+ (boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined),
6379
+ );
6380
+ }
6381
+
6382
+ function repairableReadOnlyBoundary(): PlaybookEffectBoundary | undefined {
6383
+ if (!recoveryCheckpoint || !repositoryCapability?.observe || !repositoryCapability.acquire ||
6384
+ retainedEffectReconciliationRequired || deferredReconciliationOperationId !== undefined) return undefined;
6385
+ const suffix = currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix);
6386
+ if (suffix.length !== 1) return undefined;
6387
+ const saved = suffix[0]!;
6388
+ if (!runtimeBoundaryIsOwned(saved) || saved.restored !== undefined ||
6389
+ saved.sourceStateId !== recoveryCheckpoint.stateId || saved.cohortId !== undefined ||
6390
+ saved.logicalOperationId !== undefined || saved.dispositions.some((item) => item !== 'unchanged') ||
6391
+ saved.physicalReceipt === undefined || saved.after?.head !== saved.baseline.head ||
6392
+ saved.physicalReceipt.classification === 'unchanged' ||
6393
+ [...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId)) return undefined;
6394
+ return saved;
6395
+ }
6396
+
6397
+ async function confirmReadOnlyRestoration(saved: PlaybookEffectBoundary, signal: AbortSignal): Promise<void> {
6398
+ const claim = await repositoryCapability!.acquire!({ signal });
6399
+ try {
6400
+ await claim.assertOwner();
6401
+ const restored = snapshotJsonValue(await repositoryCapability!.observe!()) as unknown as PlaybookRepositoryReceipt['baseline'];
6402
+ if (!isDeepStrictEqual(restored, saved.baseline)) {
6403
+ throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
6404
+ }
6405
+ signal.throwIfAborted();
6406
+ effectLedgerMirror = assertPlaybookEffectLedger(await effectLedgerCapability!.writeAhead([
6407
+ { kind: 'replace-boundaries', replacements: [{ expected: saved, next: { ...saved, restored } }] },
6408
+ ]));
6409
+ if (!isDeepStrictEqual(effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === saved.boundaryId)?.restored, restored)) {
6410
+ throw new Error(`${label} repository restoration was not durably acknowledged`);
6411
+ }
6412
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
6413
+ } finally { await claim.release(); }
6414
+ }
6415
+
6416
+ // A completed commit with saved presentation needs only the missing
6417
+ // tool-free judgment. Never repeat the player to recover that evidence.
6418
+ function recoverableJudgment(): PlaybookEffectBoundary | undefined {
6419
+ if (
6420
+ !recoveryCheckpoint || recoveryCheckpoint.result !== undefined || recoveryCheckpoint.delivered ||
6421
+ retainedEffectReconciliationRequired ||
6422
+ deferredReconciliationOperationId !== undefined
6423
+ )
6424
+ return undefined;
6425
+ const suffix = currentEffectLedger().boundaries.slice(
6426
+ recoveryCheckpoint.boundaryPrefix,
6427
+ );
6428
+ if (suffix.length !== 1) return undefined;
6429
+ const saved = suffix[0]!;
6430
+ if (
6431
+ !runtimeBoundaryIsOwned(saved) ||
6432
+ saved.sourceStateId !== recoveryCheckpoint.stateId ||
6433
+ saved.logicalOperationId !== undefined ||
6434
+ saved.correctionBudget.spent ||
6435
+ saved.physicalReceipt?.classification !== 'one-descendant-commit' ||
6436
+ !saved.finalText?.trim() ||
6437
+ governedOutcomesForBoundary(saved) === undefined ||
6438
+ (saved.semanticCandidate !== undefined &&
6439
+ persistedBoundaryReconciliation(saved, currentEffectLedger())
6440
+ ?.reconciliation.status !== 'resolved') ||
6441
+ [...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId)
6442
+ )
6443
+ return undefined;
6444
+ return saved;
6445
+ }
6446
+
6447
+ async function recoverJudgment(
6448
+ saved: PlaybookEffectBoundary,
6449
+ signal: AbortSignal,
6450
+ ): Promise<void> {
6451
+ const checkpoint = recoveryCheckpoint!;
6452
+ const persisted = checkpoint.machine as unknown as {
6453
+ children: Record<string, { snapshot: { input: PlaybookPlayerInput } }>;
6454
+ };
6455
+ const input = Object.values(persisted.children)[0]!;
6456
+ const actorInput = input.snapshot.input;
6457
+ const outcomes = governedOutcomesForBoundary(saved)!;
6458
+ const candidate =
6459
+ saved.semanticCandidate ??
6460
+ parseGovernedSemanticCandidate(
6461
+ await boundary.callJudge(
6462
+ 'player-output-adjudication',
6463
+ saved.sourceStateId,
6464
+ buildGovernedJudgePrompt(actorInput, saved.finalText!, outcomes),
6465
+ signal,
6466
+ ),
6467
+ );
6468
+ const result = reconcilePlaybookSemanticEvidence({
6469
+ outcomes,
6470
+ semanticCandidate: candidate,
6471
+ finalText: saved.finalText,
6472
+ receipt: saved.physicalReceipt,
6473
+ });
6474
+ signal.throwIfAborted();
6475
+ const next = {
6476
+ ...saved,
6477
+ semanticCandidate: snapshotJsonValue(result.evidence.semanticCandidate),
6478
+ };
6479
+ const acknowledged = assertPlaybookEffectLedger(
6480
+ await effectLedgerCapability!.writeAhead([
6481
+ {
6482
+ kind: 'replace-boundaries',
6483
+ replacements: [{ expected: saved, next }],
6484
+ },
6485
+ ]),
6486
+ );
6487
+ if (
6488
+ !isDeepStrictEqual(
6489
+ acknowledged.boundaries.find((b) => b.boundaryId === saved.boundaryId),
6490
+ next,
6491
+ )
6492
+ ) {
6493
+ throw new Error(
6494
+ `${label} recovered judgment was not acknowledged exactly`,
6495
+ );
6496
+ }
6497
+ effectLedgerMirror = acknowledged;
6498
+ refreshUnresolvedSemanticReconciliation(acknowledged);
6499
+ if (result.status !== 'resolved') {
6500
+ const error = attachPlaybookFailureCause(new Error(
6501
+ result.status === 'unresolved' && 'reason' in result.cause.evidence && result.cause.evidence.reason
6502
+ ? String(result.cause.evidence.reason)
6503
+ : `${label} saved result could not be reconciled; the playbook remains parked`,
6504
+ ), result.status === 'unresolved' ? result.cause : runtimeDefectCause('Saved result still needs a Boss answer'));
6505
+ const paused = detachPersistedMachineSnapshot(actor!.getPersistedSnapshot()) as Record<string, JsonValue>;
6506
+ stopActor();
6507
+ actor = buildActor(runtimePorts!, {
6508
+ ...paused, context: { ...(paused.context as Record<string, JsonValue>), lastError: snapshotJsonValue(normalizeErrorFull(error)) },
6509
+ });
6510
+ suppressInspectionEmissions = false;
6511
+ actor.start();
6512
+ throw error;
6513
+ }
6514
+ await resumeCheckpoint({ ...checkpoint, result: snapshotJsonValue(result.output) });
6515
+ }
6516
+
6517
+ async function resumeCheckpoint(checkpoint: NonNullable<PlaybookRuntimeSnapshot['recoveryCheckpoint']>): Promise<void> {
6518
+ acceptedStepResult = checkpoint;
6519
+ try {
6520
+ stopActor();
6521
+ actor = buildActor(runtimePorts!, checkpoint.machine);
6522
+ suppressInspectionEmissions = false;
6523
+ actor.start();
6524
+ await waitForPlaybookQuiescence(actor, { pendingCalls: nestedBridge });
6525
+ } finally { acceptedStepResult = undefined; }
6526
+ }
6527
+
6528
+ function exportRuntimeSnapshot(checkpoint?: { interrupted?: true; child?: PlaybookPendingCall }): PlaybookRuntimeSnapshot | undefined {
6529
+ if (!actor || !session || disposed || disposalPromise !== undefined) {
6530
+ return undefined;
6531
+ }
6532
+ if (activeSignal !== undefined && checkpoint === undefined) return undefined;
6533
+ if (deferredSettlementClosure !== undefined) return undefined;
6534
+ const pendingCall = checkpoint?.child ?? nestedBridge.getPendingCall();
6535
+ const bridgeSuspendedCall = checkpoint?.child ? nestedBridge.checkpointCall(checkpoint.child) : nestedBridge.getSuspendedCall();
6536
+ if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
6537
+ return undefined;
6538
+ }
6539
+ let suspendedCall: typeof bridgeSuspendedCall;
6540
+ if (bridgeSuspendedCall !== undefined) {
6541
+ if (
6542
+ pendingCall?.callId !== bridgeSuspendedCall.callId ||
6543
+ pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
6544
+ pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId
6545
+ ) {
6546
+ return undefined;
6547
+ }
6548
+ if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
6549
+ return undefined;
6550
+ }
6551
+ const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
6552
+ if (
6553
+ bridgeSuspendedCall.turnId !== undefined &&
6554
+ bridgeSuspendedCall.turnId !== turnId
6555
+ ) {
6556
+ return undefined;
6557
+ }
6558
+ suspendedCall = {
6559
+ ...bridgeSuspendedCall,
6560
+ ...(turnId === undefined ? {} : { turnId }),
6561
+ ...(hasGovernedPlayerStates
6562
+ ? {
6563
+ effectBoundaryPrefixSequence:
6564
+ playbookCallEffectPrefixes.get(
6565
+ bridgeSuspendedCall.callId,
6566
+ ) ?? null,
6567
+ }
6568
+ : {}),
6569
+ };
6570
+ }
6571
+ const interrupted = checkpoint?.interrupted && recoveryCheckpoint && machine.root.states.failed;
6572
+ const state = interrupted
6573
+ ? normalizePlaybookSnapshot(machine.resolveState({ value: 'failed', context: {} }))
6574
+ : checkpoint?.child ? normalizePlaybookSnapshot(actor.getSnapshot(), { pendingCall }) : currentState();
6575
+ if (state.status !== 'active' || !state.quiescent) return undefined;
6576
+ const context = (actor.getSnapshot() as { context?: unknown })
6577
+ .context as Record<string, unknown>;
6578
+ // DR-063 §2: the failed state's `lastError` is persisted as every live
6579
+ // surface publishes it, completed from the runtime's own cause slot, so
6580
+ // a thrown value that refused the cause still explains itself after a
6581
+ // restart.
6582
+ const machineSnapshot = detachPersistedMachineSnapshot(
6583
+ interrupted ? { ...recoveryCheckpoint!.machine as object, value: 'failed', children: {} } : actor.getPersistedSnapshot(),
6584
+ state.stateId === 'failed'
6585
+ ? { lastError: interrupted ? { name: 'InterruptedStep', message: 'The process stopped during this step. Review the recorded work before choosing how to continue.', cause: runtimeDefectCause('The process stopped during this step. Review the recorded work before choosing how to continue.') } : failedStateError(context ?? {}) }
6586
+ : {},
6587
+ );
6588
+ effectLedgerMirror = currentEffectLedger();
6589
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
6590
+ syncDeferredReconciliationOverlay();
6591
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
6592
+ const pendingQuestions = !hasUnresolvedReconciliation()
6593
+ ? pendingBossQuestionsFor(state, context ?? {})
6594
+ : [];
6595
+ const interruptedPrefix = interrupted ? activeEffectLedgerPrefixSequence : undefined;
6596
+ const interruptedAttempts = new Set(effectLedgerMirror.boundaries.filter(({ sequence }) => interruptedPrefix !== undefined && sequence > interruptedPrefix).map(({ attemptId }) => attemptId));
6597
+ const failedEffectAttempt = interrupted && interruptedPrefix !== undefined && interruptedAttempts.size <= 1
6598
+ ? { boundaryPrefix: interruptedPrefix, attemptId: interruptedAttempts.values().next().value ?? null }
6599
+ :
6600
+ hasGovernedPlayerStates &&
6601
+ !interrupted && state.stateId === 'failed' &&
6602
+ failedAttemptMatchesCurrentLedger(effectLedgerMirror)
6603
+ ? {
6604
+ boundaryPrefix: failedEffectBoundaryPrefix!,
6605
+ attemptId: failedGovernedAttemptId ?? null,
6606
+ }
6607
+ : undefined;
6608
+ return {
6609
+ schemaVersion: 4,
6610
+ playbookId: session.playbookId,
6611
+ machine: machineSnapshot,
6612
+ roleResumeTokens: snapshotRoleResumeTokens(),
6613
+ sequences: {
6614
+ trace: traceSequence,
6615
+ turn: turnSequence,
6616
+ judgeCall: judgeCallSequence,
6617
+ playerCall: playerCallSequence,
6618
+ playbookCall: playbookCallSequence,
6619
+ ...(declaredActors.has('captain')
6620
+ ? { captainCall: captainCallSequence }
6621
+ : {}),
6622
+ },
6623
+ state,
6624
+ pendingBossQuestions: pendingQuestions.map((pending) => ({
6625
+ questionId: pending.questionId,
6626
+ asker: pending.asker,
6627
+ question: pending.question,
6628
+ sourceItem: pending.sourceItem,
6629
+ })),
6630
+ effectLedger: effectLedgerMirror,
6631
+ ...(retainedEffectSourceSessionId === undefined
6632
+ ? {}
6633
+ : { retainedEffectSourceSessionId }),
6634
+ ...(retainedEffectReconciliation === undefined
6635
+ ? {}
6636
+ : { retainedEffectReconciliation }),
6637
+ ...(failedEffectAttempt === undefined
6638
+ ? {}
6639
+ : { failedEffectAttempt }),
6640
+ ...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
6641
+ ? { recoveryCheckpoint } : {}),
6642
+ ...(suspendedCall === undefined ? {} : { suspendedCall }),
6643
+ };
6644
+ }
6645
+
6646
+ async function runJournalledStep(
6647
+ kind: PlaybookStepRecord['kind'],
6648
+ input: PlaybookPlayerInput | PlaybookCaptainInput | PlaybookScriptInput,
6649
+ signal: AbortSignal,
6650
+ execute: () => Promise<PlaybookActorOutput>,
6651
+ ): Promise<PlaybookActorOutput> {
6652
+ const saved = acceptedStepResult;
6653
+ let output: PlaybookActorOutput;
6654
+ if (saved?.stateId === input.stateId && saved.result !== undefined) {
6655
+ acceptedStepResult = undefined;
6656
+ recoveryCheckpoint = saved;
6657
+ output = snapshotJsonValue(saved.result) as PlaybookActorOutput;
6658
+ } else {
6659
+ const record = savedPorts?.recordStep;
6660
+ await drainEmissions();
6661
+ signal.throwIfAborted();
6662
+ try {
6663
+ captureRecoveryCheckpoint(input.stateId, 'command' in input ? input.command : 'role' in input ? composeBoundPlayerPrompt(input) : composeCaptainPrompt(input));
6664
+ } catch (error) {
6665
+ if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
6666
+ throw error;
6667
+ }
6668
+ if (record) {
6669
+ const id = randomUUID();
6670
+ if (recoveryCheckpoint) recoveryCheckpoint = { ...recoveryCheckpoint, id };
6671
+ const step = { id, kind, stateId: input.stateId };
6672
+ await record(step, exportRuntimeSnapshot({ interrupted: true }));
6673
+ signal.throwIfAborted();
6674
+ const result = snapshotJsonValue(await execute());
6675
+ // Keep the known output if its save loses acknowledgement. A drained
6676
+ // failure can then preserve it without treating the call as unexecuted.
6677
+ if (recoveryCheckpoint?.id === id) recoveryCheckpoint = { ...recoveryCheckpoint, result };
6678
+ await record({ ...step, result });
6679
+ output = result as PlaybookActorOutput;
6680
+ } else {
6681
+ output = await execute();
6682
+ }
6683
+ }
6684
+ if (recoveryCheckpoint?.stateId === input.stateId) {
6685
+ const { result: _result, ...start } = recoveryCheckpoint;
6686
+ recoveryCheckpoint = { ...start, delivered: true };
6687
+ }
6688
+ return output;
6689
+ }
6690
+
6691
+ function validateRecoveryCheckpoint(
6692
+ checkpoint: PlaybookRuntimeSnapshot['recoveryCheckpoint'],
6693
+ ): void {
6694
+ if (!checkpoint) return;
6695
+ const node = machine.root.states[checkpoint.stateId];
6696
+ const persisted = checkpoint.machine as Record<string, JsonValue>;
6697
+ const children = persisted.children;
6698
+ if (
6699
+ !node ||
6700
+ node.invoke.length !== 1 ||
6701
+ !['player', 'captain', 'script'].includes(String(node.invoke[0]!.src)) ||
6702
+ !isPlainObject(children) ||
6703
+ Object.keys(children).length !== 1
6704
+ ) {
6705
+ throw new TypeError(
6706
+ `${label} recovery checkpoint does not name one current invocation`,
6707
+ );
6708
+ }
6709
+ const child = children[node.invoke[0]!.id] as
6710
+ Record<string, JsonValue> | undefined;
6711
+ if (
6712
+ !child ||
6713
+ child.src !== node.invoke[0]!.src ||
6714
+ !isPlainObject(child.snapshot) ||
6715
+ child.snapshot.status !== 'active' ||
6716
+ !isPlainObject(child.snapshot.input) ||
6717
+ child.snapshot.input.stateId !== checkpoint.stateId
6718
+ ) {
6719
+ throw new TypeError(
6720
+ `${label} recovery checkpoint invocation is incompatible`,
6721
+ );
6722
+ }
6723
+ if (checkpoint.result !== undefined) {
6724
+ const output = checkpoint.result;
6725
+ const input = child.snapshot.input;
6726
+ if (!isPlainObject(output) || typeof output.guard !== 'string' ||
6727
+ !isPlainObject(input.result) || !Object.hasOwn(input.result, output.guard) ||
6728
+ (node.invoke[0]!.src === 'script' &&
6729
+ (!Number.isInteger(output.exitStatus) || Object.keys(output).sort().join(',') !== 'exitStatus,guard' ||
6730
+ output.guard !== Object.keys(input.result)[output.exitStatus === 0 ? 0 : Math.min(1, Object.keys(input.result).length - 1)]))) {
6731
+ throw new TypeError(`${label} saved step result is incompatible with its invocation`);
6732
+ }
6733
+ for (const field of extractFields(String(input.result[output.guard]))) {
6734
+ if (output[field] === undefined || output[field] === null) throw new TypeError(`${label} saved step result is missing ${field}`);
6735
+ }
6736
+ }
6737
+ }
6738
+
5603
6739
  function captureEffectLedgerPrefixSequence(): number | undefined {
5604
6740
  if (!hasGovernedPlayerStates) return undefined;
5605
6741
  try {
@@ -5672,6 +6808,371 @@ export function createXStatePlaybookRuntime<
5672
6808
  failedGovernedAttemptId = attemptIds.values().next().value;
5673
6809
  }
5674
6810
 
6811
+ // -----------------------------------------------------------------
6812
+ // DR-067 §1: the parallel profile's repository cohort. The working
6813
+ // leaves of one parallel state entered together run under one
6814
+ // `runCohort` claim, each member an `unchanged` boundary: a member's
6815
+ // result is adjudicated through the one reconciler in serial, a spent
6816
+ // correction rewrites every member's boundary in one acknowledged batch,
6817
+ // members are acknowledged and released in completion order, and a
6818
+ // member's failure cancels its siblings and fails the cohort with the
6819
+ // cause decided first.
6820
+ // -----------------------------------------------------------------
6821
+
6822
+ function trackBoundaryCall<T>(call: Promise<T>): Promise<T> {
6823
+ if (parallelProfile === undefined) return call;
6824
+ activeBoundaryCalls.add(call);
6825
+ const forget = (): void => {
6826
+ activeBoundaryCalls.delete(call);
6827
+ };
6828
+ void call.then(forget, forget);
6829
+ return call;
6830
+ }
6831
+
6832
+ async function drainBoundaryCalls(): Promise<void> {
6833
+ while (activeBoundaryCalls.size > 0) {
6834
+ await Promise.allSettled([...activeBoundaryCalls]);
6835
+ }
6836
+ }
6837
+
6838
+ function cohortParallelStateFor(
6839
+ stateId: string,
6840
+ ): XStateParallelState | undefined {
6841
+ const parallelState =
6842
+ parallelProfile?.parallelStateByWorkingStateId.get(stateId);
6843
+ if (parallelState === undefined) return undefined;
6844
+ if (
6845
+ consumedCohortBoundaries.get(parallelState.stateId) ===
6846
+ publicBoundarySequence
6847
+ ) {
6848
+ return undefined;
6849
+ }
6850
+ const pending = pendingCohorts.get(parallelState.stateId);
6851
+ if (
6852
+ pending !== undefined &&
6853
+ pending.boundarySequence === publicBoundarySequence &&
6854
+ !pending.started
6855
+ ) {
6856
+ return parallelState;
6857
+ }
6858
+ const active = currentState().activeStateIds;
6859
+ return parallelState.regions.every(({ workingStateId }) =>
6860
+ active.includes(workingStateId),
6861
+ )
6862
+ ? parallelState
6863
+ : undefined;
6864
+ }
6865
+
6866
+ function startCohort(
6867
+ parallelState: XStateParallelState,
6868
+ cohort: PendingCohort,
6869
+ ): void {
6870
+ cohort.started = true;
6871
+ consumedCohortBoundaries.set(
6872
+ parallelState.stateId,
6873
+ cohort.boundarySequence,
6874
+ );
6875
+ if (pendingCohorts.get(parallelState.stateId) === cohort) {
6876
+ pendingCohorts.delete(parallelState.stateId);
6877
+ }
6878
+ }
6879
+
6880
+ async function joinCohort(
6881
+ parallelState: XStateParallelState,
6882
+ member: XStateCohortMember,
6883
+ ): Promise<PlayerResult> {
6884
+ let cohort = pendingCohorts.get(parallelState.stateId);
6885
+ if (
6886
+ cohort === undefined ||
6887
+ cohort.boundarySequence !== publicBoundarySequence ||
6888
+ cohort.started
6889
+ ) {
6890
+ cohort = {
6891
+ boundarySequence: publicBoundarySequence,
6892
+ members: new Map(),
6893
+ started: false,
6894
+ };
6895
+ pendingCohorts.set(parallelState.stateId, cohort);
6896
+ }
6897
+ const registered = cohort;
6898
+ if (registered.members.has(member.region.key)) {
6899
+ throw new Error(
6900
+ `${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`,
6901
+ );
6902
+ }
6903
+ registered.members.set(member.region.key, member);
6904
+ // A member whose invocation stops before every sibling registered —
6905
+ // its region exited, or the boundary aborted — leaves no cohort to
6906
+ // run: each registered member settles with its own cancellation and
6907
+ // the boundary's cohort is spent.
6908
+ const withdraw = (): void => {
6909
+ if (registered.started) return;
6910
+ startCohort(parallelState, registered);
6911
+ for (const candidate of registered.members.values()) {
6912
+ candidate.settled.reject(
6913
+ candidate.signal.aborted
6914
+ ? candidate.signal.reason
6915
+ : member.signal.reason,
6916
+ );
6917
+ }
6918
+ };
6919
+ if (member.signal.aborted) {
6920
+ withdraw();
6921
+ } else {
6922
+ member.signal.addEventListener('abort', withdraw, { once: true });
6923
+ }
6924
+ if (
6925
+ !registered.started &&
6926
+ registered.members.size === parallelState.regions.length
6927
+ ) {
6928
+ startCohort(parallelState, registered);
6929
+ void settleCohort(parallelState, registered);
6930
+ }
6931
+ return await member.settled.promise;
6932
+ }
6933
+
6934
+ async function settleCohort(
6935
+ parallelState: XStateParallelState,
6936
+ cohort: PendingCohort,
6937
+ ): Promise<void> {
6938
+ const members = parallelState.regions.map(
6939
+ ({ key }) => cohort.members.get(key)!,
6940
+ );
6941
+ const cancellation = new AbortController();
6942
+ const completionOrder: XStateCohortMember[] = [];
6943
+ const decided = new Map<
6944
+ XStateCohortMember,
6945
+ {
6946
+ readonly error: unknown;
6947
+ readonly cause: PlaybookFailureCause | undefined;
6948
+ readonly sequence: number;
6949
+ }
6950
+ >();
6951
+ let decisionSequence = 0;
6952
+ // DR-063 §2: each member failure is one decision, ordered as it was
6953
+ // decided, with the cause decided for exactly that value at that
6954
+ // moment — so a value a sibling later rethrows cannot rename it.
6955
+ const decide = (
6956
+ member: XStateCohortMember,
6957
+ error: unknown,
6958
+ cause: PlaybookFailureCause | undefined = failureCauses.causeOf(error),
6959
+ ): void => {
6960
+ if (decided.has(member)) return;
6961
+ decided.set(member, { error, cause, sequence: ++decisionSequence });
6962
+ };
6963
+ const isCancellation = (error: unknown): boolean =>
6964
+ cancellation.signal.aborted &&
6965
+ Object.is(error, cancellation.signal.reason) &&
6966
+ !activeAborts?.isAbortReason(error);
6967
+ const operationFor =
6968
+ (member: XStateCohortMember) =>
6969
+ async (
6970
+ context:
6971
+ | { readonly baseline?: PlaybookRepositoryReceipt['baseline'] }
6972
+ | undefined,
6973
+ ): Promise<PlayerResult> => {
6974
+ const callSignal = combineAbortSignals(
6975
+ member.signal,
6976
+ cancellation.signal,
6977
+ );
6978
+ try {
6979
+ if (activePlayerKeys.has(member.playerKey)) {
6980
+ await member.reportCollision(callSignal);
6981
+ }
6982
+ activePlayerKeys.add(member.playerKey);
6983
+ try {
6984
+ const result = await member.run(
6985
+ callSignal,
6986
+ cancellation.signal,
6987
+ effectAuthorizedPreExistingBlock(
6988
+ member.effectBoundary,
6989
+ context?.baseline,
6990
+ ),
6991
+ );
6992
+ if (result.status !== 'ok' && !cancellation.signal.aborted) {
6993
+ // DR-063 §2: the failure built for a non-`ok` member result
6994
+ // carries the cause decided for exactly that result, and its
6995
+ // siblings are cancelled with it.
6996
+ const cause =
6997
+ resultFailureCauses.get(result) ??
6998
+ runtimeDefectCause(
6999
+ `callPlayer status ${JSON.stringify(result.status)}`,
7000
+ );
7001
+ const failure = attachPlaybookFailureCause(
7002
+ markFsmResultFailure(
7003
+ new Error(
7004
+ result.error ??
7005
+ `captainBridge: callPlayer status "${result.status}"`,
7006
+ ),
7007
+ ),
7008
+ cause,
7009
+ );
7010
+ failureCauses.retain(failure, cause);
7011
+ decide(member, failure, cause);
7012
+ cancellation.abort(failure);
7013
+ }
7014
+ return result;
7015
+ } finally {
7016
+ activePlayerKeys.delete(member.playerKey);
7017
+ }
7018
+ } catch (error) {
7019
+ if (!isCancellation(error)) decide(member, error);
7020
+ if (!cancellation.signal.aborted) cancellation.abort(error);
7021
+ throw error;
7022
+ } finally {
7023
+ completionOrder.push(member);
7024
+ }
7025
+ };
7026
+ const completeEffectBoundary = (
7027
+ completion: XStateRepositoryCohortCompletion<PlayerResult>,
7028
+ ): Promise<XStateRepositoryCompletionEvidence> =>
7029
+ cohortCompletionQueue.add(async () => {
7030
+ const member = members.find(
7031
+ ({ roleId }) => roleId === completion.roleId,
7032
+ );
7033
+ if (
7034
+ member === undefined ||
7035
+ completion.boundary.boundaryId !== member.effectBoundary.boundaryId
7036
+ ) {
7037
+ throw new TypeError(
7038
+ `${label} repository cohort completed an undeclared member`,
7039
+ );
7040
+ }
7041
+ const evidence = await completionEvidenceFor(
7042
+ member.input,
7043
+ member.roleId,
7044
+ member.playerId,
7045
+ member.signal,
7046
+ undefined,
7047
+ )(completion);
7048
+ const settlement = governedSettlementsByBoundaryId.get(
7049
+ member.effectBoundary.boundaryId,
7050
+ );
7051
+ if (settlement?.status === 'unresolved') {
7052
+ decide(member, settlement.error);
7053
+ }
7054
+ return evidence;
7055
+ }) as Promise<XStateRepositoryCompletionEvidence>;
7056
+ const runSignal = activeSignal ?? members[0]!.signal;
7057
+ const invocationId = randomUUID();
7058
+ let settled: XStateRepositoryCohortResult<PlayerResult>;
7059
+ try {
7060
+ for (const member of members) member.signal.throwIfAborted();
7061
+ settled = await repositoryCapability!.runCohort!<PlayerResult>({
7062
+ signal: runSignal,
7063
+ invocationId,
7064
+ roleIds: members.map(({ roleId }) => roleId),
7065
+ dispositionsByRole: Object.fromEntries(
7066
+ members.map(({ roleId }) => [roleId, ['unchanged'] as const]),
7067
+ ),
7068
+ effectBoundaries: Object.fromEntries(
7069
+ members.map(({ roleId, effectBoundary }) => [
7070
+ roleId,
7071
+ effectBoundary,
7072
+ ]),
7073
+ ),
7074
+ operations: Object.fromEntries(
7075
+ members.map((member) => [member.roleId, operationFor(member)]),
7076
+ ),
7077
+ completeEffectBoundary,
7078
+ });
7079
+ if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
7080
+ throw new TypeError(
7081
+ `${label} repository cohort changed its invocation identity`,
7082
+ );
7083
+ }
7084
+ } catch (error) {
7085
+ for (const member of members) {
7086
+ governedSettlementsByBoundaryId.delete(
7087
+ member.effectBoundary.boundaryId,
7088
+ );
7089
+ governedCompletionEvidenceByBoundaryId.delete(
7090
+ member.effectBoundary.boundaryId,
7091
+ );
7092
+ refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
7093
+ }
7094
+ if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
7095
+ for (const member of members) member.settled.reject(error);
7096
+ return;
7097
+ }
7098
+ const releaseOrder = [
7099
+ ...completionOrder,
7100
+ ...members.filter((member) => !completionOrder.includes(member)),
7101
+ ];
7102
+ const acknowledged = new Map<XStateCohortMember, PlayerResult>();
7103
+ const failures = new Map<XStateCohortMember, unknown>();
7104
+ for (const member of releaseOrder) {
7105
+ const operation = isPlainObject(settled.operations)
7106
+ ? settled.operations[member.roleId]
7107
+ : undefined;
7108
+ try {
7109
+ const result = acknowledgeGovernedPlayerResult(
7110
+ {
7111
+ operation,
7112
+ receipt: isPlainObject(settled.receipts)
7113
+ ? settled.receipts[member.roleId]
7114
+ : undefined,
7115
+ effectLedger: settled.effectLedger,
7116
+ },
7117
+ member.effectBoundary.boundaryId,
7118
+ 'runCohort',
7119
+ );
7120
+ acknowledged.set(member, result);
7121
+ const settlement = governedPlayerSettlements.get(result);
7122
+ if (settlement?.status === 'unresolved') {
7123
+ failures.set(member, settlement.error);
7124
+ } else if (result.status !== 'ok') {
7125
+ failures.set(member, decided.get(member)?.error);
7126
+ }
7127
+ } catch (error) {
7128
+ failures.set(member, error);
7129
+ const operationReason =
7130
+ isPlainObject(operation) &&
7131
+ operation.status === 'rejected' &&
7132
+ Object.is(operation.reason, error);
7133
+ if (!operationReason) {
7134
+ // An acknowledgement the host could not prove is a control-plane
7135
+ // defect, exactly as on the exclusive path.
7136
+ if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
7137
+ decide(member, error);
7138
+ }
7139
+ } finally {
7140
+ governedSettlementsByBoundaryId.delete(
7141
+ member.effectBoundary.boundaryId,
7142
+ );
7143
+ governedCompletionEvidenceByBoundaryId.delete(
7144
+ member.effectBoundary.boundaryId,
7145
+ );
7146
+ }
7147
+ }
7148
+ if (failures.size === 0) {
7149
+ for (const member of releaseOrder) {
7150
+ member.settled.resolve(acknowledged.get(member)!);
7151
+ }
7152
+ return;
7153
+ }
7154
+ for (const result of acknowledged.values()) {
7155
+ governedPlayerSettlements.delete(result);
7156
+ }
7157
+ const first = [...decided.values()].sort(
7158
+ (left, right) => left.sequence - right.sequence,
7159
+ )[0];
7160
+ const failure =
7161
+ first?.error ??
7162
+ failures.get(releaseOrder.find((member) => failures.has(member))!) ??
7163
+ markFsmResultFailure(
7164
+ new Error(
7165
+ `${label} parallel state ${parallelState.stateId} cohort failed`,
7166
+ ),
7167
+ );
7168
+ const cause = first?.cause ?? failureCauses.causeOf(failure);
7169
+ if (cause !== undefined) {
7170
+ failureCauses.retain(failure, cause);
7171
+ attachPlaybookFailureCause(failure, cause);
7172
+ }
7173
+ for (const member of members) member.settled.reject(failure);
7174
+ }
7175
+
5675
7176
  const boundary: RuntimeBoundaryCalls = {
5676
7177
  async callPlayer(
5677
7178
  input,
@@ -5729,16 +7230,192 @@ export function createXStatePlaybookRuntime<
5729
7230
  };
5730
7231
 
5731
7232
  const playerKey = continuationKey(roleId, playerId);
5732
- if (activePlayerKeys.has(playerKey)) {
7233
+ const runTracedPlayerCall = async (
7234
+ resume: string | false = selectedResume,
7235
+ // DR-062 §4: composed from this call's own baseline observation,
7236
+ // so the prompt the trace records is the prompt the player is sent.
7237
+ preExistingBlock?: string,
7238
+ // DR-067 §1: a cohort member's call signal also carries its
7239
+ // cohort's sibling cancellation.
7240
+ callSignal: AbortSignal = signal,
7241
+ cohortCancellation?: AbortSignal,
7242
+ ): Promise<PlayerResult> => {
7243
+ const identity = callIdentity(resume);
7244
+ // DR-067 §1: a parallel machine classifies aborts per call, so a
7245
+ // trace sink rejecting with a sibling's cancellation is forgiven
7246
+ // for that call alone.
7247
+ const traceAborts =
7248
+ parallelProfile === undefined
7249
+ ? undefined
7250
+ : abortReasonClassifier(callSignal, activeAborts);
7251
+ const withPreExisting = (text: string): string =>
7252
+ preExistingBlock === undefined
7253
+ ? text
7254
+ : `${text}\n\n${preExistingBlock}`;
7255
+ const callPrompt = withPreExisting(
7256
+ resume === false ? freshPrompt : prompt,
7257
+ );
7258
+ const callFreshPrompt = withPreExisting(freshPrompt);
7259
+ await emitCallStarted(
7260
+ 'player.call.started',
7261
+ 'player.call.finished',
7262
+ { ...identity, prompt: callPrompt },
7263
+ position,
7264
+ callSignal,
7265
+ undefined,
7266
+ traceAborts,
7267
+ );
7268
+
7269
+ let rawResult: unknown;
7270
+ try {
7271
+ // An abort may land while the awaited started emission drains
7272
+ // (e.g. fired from the trace sink itself); the host call must
7273
+ // never start after abort, so settle the already-started pair
7274
+ // as `aborted` through the catch below.
7275
+ callSignal.throwIfAborted();
7276
+ rawResult = await requireHostPorts().callPlayer(
7277
+ roleId,
7278
+ callPrompt,
7279
+ callSignal,
7280
+ {
7281
+ resume,
7282
+ ...(callPrompt === callFreshPrompt
7283
+ ? {}
7284
+ : { freshPrompt: callFreshPrompt }),
7285
+ },
7286
+ );
7287
+ // A host promise is not required to honor cancellation. Do not
7288
+ // let a late result mutate continuity or publish a successful
7289
+ // finish.
7290
+ callSignal.throwIfAborted();
7291
+ } catch (error) {
7292
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
7293
+ try {
7294
+ await emitTrace(
7295
+ 'player.call.finished',
7296
+ {
7297
+ ...identity,
7298
+ status: isAbortFailure(error, callSignal)
7299
+ ? 'aborted'
7300
+ : 'error',
7301
+ error: normalizeError(error),
7302
+ },
7303
+ position,
7304
+ traceAborts,
7305
+ );
7306
+ } catch {
7307
+ // The original non-abort port rejection remains authoritative.
7308
+ }
7309
+ // A thrown port call carries no authoritative result, so the
7310
+ // prior token remains available for a later explicit resume.
7311
+ throw decidePlayerCallFailure(
7312
+ error,
7313
+ roleId,
7314
+ playerId,
7315
+ callSignal,
7316
+ cohortCancellation,
7317
+ );
7318
+ }
7319
+
7320
+ let result: PlayerResult;
7321
+ try {
7322
+ result = validatePlayerResult(rawResult);
7323
+ } catch (error) {
7324
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
7325
+ try {
7326
+ await emitTrace(
7327
+ 'player.call.finished',
7328
+ { ...identity, status: 'error', error: normalizeError(error) },
7329
+ position,
7330
+ traceAborts,
7331
+ );
7332
+ } catch {
7333
+ // The malformed host result remains authoritative.
7334
+ }
7335
+ throw decidePlayerCallFailure(
7336
+ error,
7337
+ roleId,
7338
+ playerId,
7339
+ callSignal,
7340
+ cohortCancellation,
7341
+ );
7342
+ }
7343
+
7344
+ try {
7345
+ updatePlayerResume(roleId, playerId, result);
7346
+ } catch (error) {
7347
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
7348
+ try {
7349
+ await emitTrace(
7350
+ 'player.call.finished',
7351
+ { ...identity, status: 'error', error: normalizeError(error) },
7352
+ position,
7353
+ traceAborts,
7354
+ );
7355
+ } catch {
7356
+ // The continuation-store failure remains authoritative.
7357
+ }
7358
+ throw error;
7359
+ }
7360
+
7361
+ await emitTrace(
7362
+ 'player.call.finished',
7363
+ {
7364
+ ...identity,
7365
+ status: result.status,
7366
+ ...(result.finalText !== undefined
7367
+ ? { finalText: result.finalText }
7368
+ : {}),
7369
+ ...(result.error !== undefined
7370
+ ? { error: normalizeError(result.error) }
7371
+ : {}),
7372
+ ...(result.resumeToken !== undefined
7373
+ ? { resumeToken: result.resumeToken }
7374
+ : {}),
7375
+ },
7376
+ position,
7377
+ traceAborts,
7378
+ );
7379
+ // DR-063 §2: the player bridge builds the failure Error for a
7380
+ // non-`ok` result, so the cause is decided here, where the role,
7381
+ // the player, and the reported error are known, and a successful
7382
+ // result clears it so no later failure inherits it.
7383
+ if (result.status === 'ok') {
7384
+ pendingResultFailureCause = undefined;
7385
+ } else if (result.status === 'aborted') {
7386
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
7387
+ } else {
7388
+ pendingResultFailureCause = playerFailureCause({
7389
+ roleId,
7390
+ ...(playerId === undefined ? {} : { playerId }),
7391
+ error:
7392
+ result.error ??
7393
+ `callPlayer status ${JSON.stringify(result.status)}`,
7394
+ });
7395
+ }
7396
+ if (pendingResultFailureCause !== undefined) {
7397
+ resultFailureCauses.set(result, pendingResultFailureCause);
7398
+ }
7399
+ return result;
7400
+ };
7401
+ const reportPlayerKeyCollision = async (
7402
+ callSignal: AbortSignal,
7403
+ ): Promise<never> => {
5733
7404
  const error = new Error(
5734
7405
  `simultaneous calls to player key ${playerKey} are not allowed`,
5735
7406
  );
7407
+ const traceAborts =
7408
+ parallelProfile === undefined
7409
+ ? undefined
7410
+ : abortReasonClassifier(callSignal, activeAborts);
5736
7411
  await emitCallStarted(
5737
7412
  'player.call.started',
5738
7413
  'player.call.finished',
5739
7414
  { ...callIdentity(selectedResume), prompt },
5740
7415
  position,
5741
- signal,
7416
+ callSignal,
7417
+ undefined,
7418
+ traceAborts,
5742
7419
  );
5743
7420
  await emitTrace(
5744
7421
  'player.call.finished',
@@ -5748,147 +7425,63 @@ export function createXStatePlaybookRuntime<
5748
7425
  error: normalizeError(error),
5749
7426
  },
5750
7427
  position,
7428
+ traceAborts,
5751
7429
  );
5752
7430
  throw error;
5753
- }
5754
- activePlayerKeys.add(playerKey);
7431
+ };
5755
7432
 
5756
- try {
5757
- const runTracedPlayerCall = async (
5758
- resume: string | false = selectedResume,
5759
- // DR-062 §4: composed from this call's own baseline observation,
5760
- // so the prompt the trace records is the prompt the player is sent.
5761
- preExistingBlock?: string,
5762
- ): Promise<PlayerResult> => {
5763
- const identity = callIdentity(resume);
5764
- const withPreExisting = (text: string): string =>
5765
- preExistingBlock === undefined
5766
- ? text
5767
- : `${text}\n\n${preExistingBlock}`;
5768
- const callPrompt = withPreExisting(
5769
- resume === false ? freshPrompt : prompt,
5770
- );
5771
- const callFreshPrompt = withPreExisting(freshPrompt);
5772
- await emitCallStarted(
5773
- 'player.call.started',
5774
- 'player.call.finished',
5775
- { ...identity, prompt: callPrompt },
5776
- position,
5777
- signal,
7433
+ // DR-067 §1: the working leaves of one parallel state entered
7434
+ // together run as one all-`unchanged` repository cohort, at most once
7435
+ // per public boundary; a corrective re-ask and a single region
7436
+ // resuming after its Boss reply take the exclusive path below.
7437
+ const cohortState =
7438
+ deferredContinuation === undefined
7439
+ ? cohortParallelStateFor(input.stateId)
7440
+ : undefined;
7441
+ if (cohortState !== undefined) {
7442
+ const effectBoundary = governedBoundarySeed(
7443
+ input,
7444
+ roleId,
7445
+ callId,
7446
+ turnId,
7447
+ );
7448
+ if (
7449
+ effectBoundary === undefined ||
7450
+ repositoryCapability?.runCohort === undefined
7451
+ ) {
7452
+ throw new Error(
7453
+ `${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`,
5778
7454
  );
7455
+ }
7456
+ activeGovernedBoundarySeen = true;
7457
+ return await joinCohort(cohortState, {
7458
+ region: cohortState.regions.find(
7459
+ ({ workingStateId }) => workingStateId === input.stateId,
7460
+ )!,
7461
+ input,
7462
+ roleId,
7463
+ playerId,
7464
+ playerKey,
7465
+ signal,
7466
+ effectBoundary,
7467
+ run: (callSignal, cohortCancellation, preExistingBlock) =>
7468
+ runTracedPlayerCall(
7469
+ selectedResume,
7470
+ preExistingBlock,
7471
+ callSignal,
7472
+ cohortCancellation,
7473
+ ),
7474
+ reportCollision: reportPlayerKeyCollision,
7475
+ settled: deferredValue<PlayerResult>(),
7476
+ });
7477
+ }
5779
7478
 
5780
- let rawResult: unknown;
5781
- try {
5782
- // An abort may land while the awaited started emission drains
5783
- // (e.g. fired from the trace sink itself); the host call must
5784
- // never start after abort, so settle the already-started pair
5785
- // as `aborted` through the catch below.
5786
- signal.throwIfAborted();
5787
- rawResult = await requireHostPorts().callPlayer(
5788
- roleId,
5789
- callPrompt,
5790
- signal,
5791
- {
5792
- resume,
5793
- ...(callPrompt === callFreshPrompt
5794
- ? {}
5795
- : { freshPrompt: callFreshPrompt }),
5796
- },
5797
- );
5798
- // A host promise is not required to honor cancellation. Do not
5799
- // let a late result mutate continuity or publish a successful
5800
- // finish.
5801
- signal.throwIfAborted();
5802
- } catch (error) {
5803
- if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
5804
- try {
5805
- await emitTrace(
5806
- 'player.call.finished',
5807
- {
5808
- ...identity,
5809
- status: isAbortFailure(error, signal) ? 'aborted' : 'error',
5810
- error: normalizeError(error),
5811
- },
5812
- position,
5813
- );
5814
- } catch {
5815
- // The original non-abort port rejection remains authoritative.
5816
- }
5817
- // A thrown port call carries no authoritative result, so the
5818
- // prior token remains available for a later explicit resume.
5819
- throw decidePlayerCallFailure(error, roleId, playerId, signal);
5820
- }
5821
-
5822
- let result: PlayerResult;
5823
- try {
5824
- result = validatePlayerResult(rawResult);
5825
- } catch (error) {
5826
- if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
5827
- try {
5828
- await emitTrace(
5829
- 'player.call.finished',
5830
- { ...identity, status: 'error', error: normalizeError(error) },
5831
- position,
5832
- );
5833
- } catch {
5834
- // The malformed host result remains authoritative.
5835
- }
5836
- throw decidePlayerCallFailure(error, roleId, playerId, signal);
5837
- }
5838
-
5839
- try {
5840
- updatePlayerResume(roleId, playerId, result);
5841
- } catch (error) {
5842
- if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
5843
- try {
5844
- await emitTrace(
5845
- 'player.call.finished',
5846
- { ...identity, status: 'error', error: normalizeError(error) },
5847
- position,
5848
- );
5849
- } catch {
5850
- // The continuation-store failure remains authoritative.
5851
- }
5852
- throw error;
5853
- }
5854
-
5855
- await emitTrace(
5856
- 'player.call.finished',
5857
- {
5858
- ...identity,
5859
- status: result.status,
5860
- ...(result.finalText !== undefined
5861
- ? { finalText: result.finalText }
5862
- : {}),
5863
- ...(result.error !== undefined
5864
- ? { error: normalizeError(result.error) }
5865
- : {}),
5866
- ...(result.resumeToken !== undefined
5867
- ? { resumeToken: result.resumeToken }
5868
- : {}),
5869
- },
5870
- position,
5871
- );
5872
- // DR-063 §2: the player bridge builds the failure Error for a
5873
- // non-`ok` result, so the cause is decided here, where the role,
5874
- // the player, and the reported error are known, and a successful
5875
- // result clears it so no later failure inherits it.
5876
- if (result.status === 'ok') {
5877
- pendingResultFailureCause = undefined;
5878
- } else if (result.status === 'aborted') {
5879
- pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
5880
- } else {
5881
- pendingResultFailureCause = playerFailureCause({
5882
- roleId,
5883
- ...(playerId === undefined ? {} : { playerId }),
5884
- error:
5885
- result.error ??
5886
- `callPlayer status ${JSON.stringify(result.status)}`,
5887
- });
5888
- }
5889
- return result;
5890
- };
7479
+ if (activePlayerKeys.has(playerKey)) {
7480
+ await reportPlayerKeyCollision(signal);
7481
+ }
7482
+ activePlayerKeys.add(playerKey);
5891
7483
 
7484
+ try {
5892
7485
  const effectBoundary = governedBoundarySeed(
5893
7486
  input,
5894
7487
  roleId,
@@ -5955,14 +7548,23 @@ export function createXStatePlaybookRuntime<
5955
7548
  // captured it under this exclusive claim. A host that supplies
5956
7549
  // none states no pre-existing change, exactly as an empty
5957
7550
  // baseline projection does.
5958
- operation: (context) =>
5959
- runTracedPlayerCall(
7551
+ operation: async (context) => {
7552
+ if (requiredRecoveryBaseline !== undefined) {
7553
+ if (!isDeepStrictEqual(snapshotJsonValue(context.baseline), requiredRecoveryBaseline)) {
7554
+ throw new Error(`${label} repository changed after preparation; the stopped step remains paused`);
7555
+ }
7556
+ requiredRecoveryBaseline = undefined;
7557
+ captureRecoveryCheckpoint(input.stateId, freshPrompt,
7558
+ currentEffectLedger().boundaries.findIndex(({ boundaryId }) => boundaryId === effectBoundary.boundaryId));
7559
+ }
7560
+ return runTracedPlayerCall(
5960
7561
  selectedResume,
5961
7562
  effectAuthorizedPreExistingBlock(
5962
7563
  effectBoundary,
5963
7564
  context?.baseline,
5964
7565
  ),
5965
- ),
7566
+ );
7567
+ },
5966
7568
  completeEffectBoundary: completionEvidenceFor(
5967
7569
  input,
5968
7570
  roleId,
@@ -5993,10 +7595,13 @@ export function createXStatePlaybookRuntime<
5993
7595
  }
5994
7596
  },
5995
7597
 
5996
- markPlayerResultFailure(error): Error {
5997
- if (pendingResultFailureCause === undefined) return error;
5998
- failureCauses.retain(error, pendingResultFailureCause);
5999
- return attachPlaybookFailureCause(error, pendingResultFailureCause);
7598
+ markPlayerResultFailure(error, result): Error {
7599
+ const cause =
7600
+ (result === undefined ? undefined : resultFailureCauses.get(result)) ??
7601
+ pendingResultFailureCause;
7602
+ if (cause === undefined) return error;
7603
+ failureCauses.retain(error, cause);
7604
+ return attachPlaybookFailureCause(error, cause);
6000
7605
  },
6001
7606
 
6002
7607
  takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
@@ -6237,12 +7842,23 @@ export function createXStatePlaybookRuntime<
6237
7842
  }) as Promise<CaptainResult>;
6238
7843
  },
6239
7844
  };
7845
+ if (parallelProfile !== undefined) {
7846
+ // DR-067 §1: a parallel machine's boundary drain waits for every player
7847
+ // and judge call it started.
7848
+ const untrackedCallPlayer = boundary.callPlayer;
7849
+ const untrackedCallJudge = boundary.callJudge;
7850
+ boundary.callPlayer = (input, roleId, freshPrompt, signal) =>
7851
+ trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
7852
+ boundary.callJudge = (purpose, stateId, prompt, signal) =>
7853
+ trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
7854
+ }
6240
7855
 
6241
7856
  function playerActor(
6242
7857
  ports: PlaybookPorts,
6243
7858
  ): PromiseActorLogic<PlaybookActorOutput, PlaybookPlayerInput> {
6244
7859
  return createPlayerBridge(
6245
7860
  {
7861
+ run: (input, signal, execute) => runJournalledStep('player', input, signal, execute),
6246
7862
  resolveRoleId: requireRoleId,
6247
7863
  validateInput: (input) =>
6248
7864
  assertGovernedPlayerInput(
@@ -6276,7 +7892,7 @@ export function createXStatePlaybookRuntime<
6276
7892
  PlaybookCaptainInput
6277
7893
  > {
6278
7894
  return fromPromise<PlaybookActorOutput, PlaybookCaptainInput>(
6279
- async ({ input, signal }) => {
7895
+ ({ input, signal }) => runJournalledStep('captain', input, signal, async () => {
6280
7896
  const active = combineAbortSignals(signal, activeSignal);
6281
7897
  try {
6282
7898
  await drainEmissions();
@@ -6353,7 +7969,7 @@ export function createXStatePlaybookRuntime<
6353
7969
  }
6354
7970
  throw error;
6355
7971
  }
6356
- },
7972
+ }),
6357
7973
  );
6358
7974
  }
6359
7975
 
@@ -6367,7 +7983,7 @@ export function createXStatePlaybookRuntime<
6367
7983
  PlaybookScriptInput
6368
7984
  > {
6369
7985
  return fromPromise<PlaybookActorOutput, PlaybookScriptInput>(
6370
- async ({ input, signal }) => {
7986
+ ({ input, signal }) => runJournalledStep('script', input, signal, async () => {
6371
7987
  await drainEmissions();
6372
7988
  const active = combineAbortSignals(signal, activeSignal);
6373
7989
  const guards = Object.keys(input.result);
@@ -6556,15 +8172,19 @@ export function createXStatePlaybookRuntime<
6556
8172
  if (killTimer !== undefined) clearTimeout(killTimer);
6557
8173
  }
6558
8174
  }
6559
- },
8175
+ }),
6560
8176
  );
6561
8177
  }
6562
8178
 
6563
8179
  const nestedBridge = createNestedPlaybookBridge({
6564
8180
  nextCallId: () => `playbook-${++playbookCallSequence}`,
6565
8181
  getBoundarySignal: () => activeSignal,
6566
- callPlaybook: (request, signal) =>
6567
- requireHostPorts().callPlaybook(request, signal),
8182
+ callPlaybook: (request, signal) => {
8183
+ const call = requireHostPorts().callPlaybook(request, signal);
8184
+ return parallelProfile === undefined
8185
+ ? call
8186
+ : trackBoundaryCall(Promise.resolve(call));
8187
+ },
6568
8188
  emitStarted: async (event, aborts) => {
6569
8189
  playbookCallTurnIds.set(event.callId, activeTurnId);
6570
8190
  if (hasGovernedPlayerStates) {
@@ -6615,7 +8235,8 @@ export function createXStatePlaybookRuntime<
6615
8235
  activeAborts = aborts ?? abortReasonClassifier(signal);
6616
8236
  },
6617
8237
  bindActorSettlement: (aborts) => {
6618
- actorSettlementAborts = aborts;
8238
+ if (parallelProfile === undefined) actorSettlementAborts.length = 0;
8239
+ actorSettlementAborts.push(aborts);
6619
8240
  },
6620
8241
  onControlPlaneError: (error, aborts) => {
6621
8242
  // The shared bridge classifies before reporting against its own
@@ -6662,14 +8283,44 @@ export function createXStatePlaybookRuntime<
6662
8283
  };
6663
8284
  }
6664
8285
 
8286
+ // DR-063 §2: the failure that parks the machine is the error its parking
8287
+ // transition carried, but a compiled machine keeps a JSON-safe record of
8288
+ // it as `lastError` (gears2fsm requires JSON-safe context), so neither
8289
+ // that value's identity nor its marker reaches the failed state. The cause
8290
+ // decided for the carried error is retained under the value the machine
8291
+ // kept, so every surface of this turn and of each later one publishes the
8292
+ // runtime's own decision for as long as that failure stands. A kept value
8293
+ // that already answers keeps its cause.
8294
+ function bindParkedFailureCause(
8295
+ context: Record<string, unknown>,
8296
+ event: unknown,
8297
+ ): void {
8298
+ const kept = context.lastError;
8299
+ if (kept === undefined || kept === null) return;
8300
+ if (failureCauses.causeOf(kept) !== undefined) return;
8301
+ const decided = failureCauses.causeOf(actorErrorOf(event));
8302
+ if (decided !== undefined) failureCauses.retain(kept, decided);
8303
+ }
8304
+
6665
8305
  // DR-063 §2: a rejected player port or a result the runtime cannot read is
6666
- // a `player-failed` failure named by role; an abort is `aborted`.
8306
+ // a `player-failed` failure named by role; an abort is `aborted`. A
8307
+ // cancellation the runtime itself issued to a cohort member because its
8308
+ // sibling failed is not this player's failure and decides nothing, so the
8309
+ // originator's cause stands (DR-067 §1, playbook-55).
6667
8310
  function decidePlayerCallFailure(
6668
8311
  error: unknown,
6669
8312
  roleId: string,
6670
8313
  playerId: string | undefined,
6671
8314
  signal: AbortSignal,
8315
+ cohortCancellation?: AbortSignal,
6672
8316
  ): unknown {
8317
+ if (
8318
+ cohortCancellation?.aborted === true &&
8319
+ Object.is(error, cohortCancellation.reason) &&
8320
+ !activeAborts?.isAbortReason(error)
8321
+ ) {
8322
+ return error;
8323
+ }
6673
8324
  const cause = isAbortFailure(error, signal)
6674
8325
  ? ABORTED_FAILURE_CAUSE
6675
8326
  : playerFailureCause({
@@ -6728,12 +8379,21 @@ export function createXStatePlaybookRuntime<
6728
8379
  previousState: previousState ?? null,
6729
8380
  state,
6730
8381
  };
6731
- const pendingBossQuestion = pendingBossQuestionForState(state, context);
6732
- if (
6733
- pendingBossQuestion !== undefined &&
6734
- !hasUnresolvedReconciliation()
6735
- ) {
6736
- payload.pendingBossQuestion = pendingBossQuestion;
8382
+ if (parallelProfile === undefined) {
8383
+ const pendingBossQuestion = pendingBossQuestionForState(state, context);
8384
+ if (
8385
+ pendingBossQuestion !== undefined &&
8386
+ !hasUnresolvedReconciliation()
8387
+ ) {
8388
+ payload.pendingBossQuestion = pendingBossQuestion;
8389
+ }
8390
+ } else if (!hasUnresolvedReconciliation()) {
8391
+ // DR-067 §1: a parallel machine's telemetry carries every question
8392
+ // pending in an active wait, in the plural.
8393
+ const pendingBossQuestions = pendingBossQuestionsFor(state, context);
8394
+ if (pendingBossQuestions.length > 0) {
8395
+ payload.pendingBossQuestions = pendingBossQuestions;
8396
+ }
6737
8397
  }
6738
8398
  if (state.stateId === 'failed') {
6739
8399
  const lastError = failedStateError(context);
@@ -6768,7 +8428,7 @@ export function createXStatePlaybookRuntime<
6768
8428
  message,
6769
8429
  ...(data === undefined ? {} : { data }),
6770
8430
  state,
6771
- ...stateIdentity(state.stateId),
8431
+ ...stateIdentity(statusTraceStateId(state)),
6772
8432
  },
6773
8433
  position,
6774
8434
  ),
@@ -6818,8 +8478,7 @@ export function createXStatePlaybookRuntime<
6818
8478
  function consumeActorSettlementAborts(
6819
8479
  forSnapshot = false,
6820
8480
  ): AbortReasonClassifier | undefined {
6821
- const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
6822
- actorSettlementAborts = undefined;
8481
+ const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
6823
8482
  actorSettlementErrorAborts = undefined;
6824
8483
  if (forSnapshot && aborts !== undefined) {
6825
8484
  // XState can report an errored root through both its inspection
@@ -6891,7 +8550,14 @@ export function createXStatePlaybookRuntime<
6891
8550
  try {
6892
8551
  const snap = inspectionEvent.snapshot;
6893
8552
  const state = normalizePlaybookSnapshot(snap);
6894
- if (state.stateId === undefined) {
8553
+ if (
8554
+ state.stateId === undefined &&
8555
+ !(
8556
+ parallelProfile?.states.some(({ stateId }) =>
8557
+ state.activeStateIds.includes(stateId),
8558
+ ) ?? false
8559
+ )
8560
+ ) {
6895
8561
  throw new Error(
6896
8562
  `${label} root snapshot must expose exactly one playbook state id`,
6897
8563
  );
@@ -6908,6 +8574,10 @@ export function createXStatePlaybookRuntime<
6908
8574
  latchRuntimeError(error);
6909
8575
  }
6910
8576
  if (state.stateId === 'failed') {
8577
+ if (previousState?.stateId !== 'failed' &&
8578
+ previousState?.stateId !== recoveryCheckpoint?.stateId) {
8579
+ recoveryCheckpoint = undefined;
8580
+ }
6911
8581
  if (
6912
8582
  previousState?.stateId !== 'failed' ||
6913
8583
  activeGovernedBoundarySeen
@@ -6921,6 +8591,9 @@ export function createXStatePlaybookRuntime<
6921
8591
  }
6922
8592
  const context = ((snap as { context?: unknown }).context ??
6923
8593
  {}) as Record<string, unknown>;
8594
+ if (state.stateId === 'failed') {
8595
+ bindParkedFailureCause(context, inspectionEvent.event);
8596
+ }
6924
8597
  if (
6925
8598
  state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
6926
8599
  deferredReconciliationOperationId !== undefined
@@ -6942,7 +8615,9 @@ export function createXStatePlaybookRuntime<
6942
8615
  context,
6943
8616
  );
6944
8617
  const stateStatuses = completeFailedStatuses(
6945
- statusesForState(state, context, inspectionEvent.event),
8618
+ bossRelevantStateIds !== undefined && usesDefaultStatuses
8619
+ ? parallelStatusesFor(previousState, state, context)
8620
+ : statusesForState(state, context, inspectionEvent.event),
6946
8621
  state,
6947
8622
  context,
6948
8623
  );
@@ -7149,13 +8824,17 @@ export function createXStatePlaybookRuntime<
7149
8824
  emissionQueue.clear();
7150
8825
  judgeQueue.clear();
7151
8826
  appliedReceipts.clear();
8827
+ pendingCohorts.clear();
8828
+ consumedCohortBoundaries.clear();
8829
+ activeBoundaryCalls.clear();
8830
+ cohortCompletionQueue.clear();
7152
8831
  actor = undefined;
7153
8832
  session = undefined;
7154
8833
  savedPorts = undefined;
7155
8834
  runtimePorts = undefined;
7156
8835
  activeSignal = undefined;
7157
8836
  activeAborts = undefined;
7158
- actorSettlementAborts = undefined;
8837
+ actorSettlementAborts.length = 0;
7159
8838
  actorSettlementErrorAborts = undefined;
7160
8839
  activeAbortEmission = undefined;
7161
8840
  activeTurnId = undefined;
@@ -7183,7 +8862,7 @@ export function createXStatePlaybookRuntime<
7183
8862
  activeDeferredContinuation = undefined;
7184
8863
  deferInspectionEmissions = false;
7185
8864
  deferredInspectionEmissions = [];
7186
- lastBossEvent = undefined;
8865
+ recoveryCheckpoint = undefined;
7187
8866
  suppressInspectionEmissions = false;
7188
8867
  initialized = false;
7189
8868
  traceSequence = 0;
@@ -7205,6 +8884,9 @@ export function createXStatePlaybookRuntime<
7205
8884
  event?: EventObject;
7206
8885
  deferredRestoreOperationId?: string;
7207
8886
  unresolvedEffectAction?: 'reconcile' | 'abandon';
8887
+ retryStep?: true;
8888
+ retryJudgment?: PlaybookEffectBoundary;
8889
+ restoreReadOnly?: PlaybookEffectBoundary;
7208
8890
  }
7209
8891
 
7210
8892
  function snapshotCan(snapshot: unknown, event: EventObject): boolean {
@@ -7216,78 +8898,24 @@ export function createXStatePlaybookRuntime<
7216
8898
  );
7217
8899
  }
7218
8900
 
7219
- // DR-034: where the artifact names the FSM context member its entry
7220
- // action copies the exact Boss text into, that member of the live
7221
- // snapshot is the retry payload's source. The persisted machine snapshot
7222
- // carries it, so the candidate derives identically in the process that
7223
- // exported the snapshot and in one that restored it, and a failure
7224
- // reached after a Boss reply — whose recorded event the failure state
7225
- // refuses — is recoverable too. Naming the member is the artifact's
7226
- // statement that it holds the entry text: a same-named member is never
7227
- // assumed, since inferring one would turn any matching context member
7228
- // into a replay payload without its author saying so.
7229
- // Declared and absent or empty excludes the candidate rather than
7230
- // falling back to the record, which would make the action depend on the
7231
- // process again — the very thing this source exists to end.
7232
- function retryEventFrom(snapshot: unknown): EventObject | undefined {
7233
- const entryEvent = spec.entryEvent;
7234
- if (entryEvent?.contextField === undefined) return lastBossEvent;
7235
- const context = (snapshot as { context?: unknown } | null)?.context;
7236
- const text = isPlainObject(context)
7237
- ? context[entryEvent.contextField]
7238
- : undefined;
7239
- if (typeof text !== 'string' || text.trim() === '') return undefined;
7240
- return { type: entryEvent.type, [entryEvent.textField]: text };
7241
- }
7242
-
7243
- // The failure-state retry entry replays the recorded last classified
7244
- // event with its recorded payload, or the entry event the declared
7245
- // context member above sources. A candidate whose event the live
7246
- // snapshot does not accept — or whose payload the runtime can source
7247
- // from neither — is excluded rather than completed with invented text.
7248
- function retryActionFor(
7249
- snapshot: unknown,
7250
- stateId: string | undefined,
7251
- ): DerivedControlAction | undefined {
7252
- if (stateId !== 'failed') return undefined;
7253
- if (!failedAttemptAllowsReplay()) return undefined;
7254
- const retryEvent = retryEventFrom(snapshot);
7255
- if (retryEvent === undefined) return undefined;
7256
- if (!snapshotCan(snapshot, retryEvent)) return undefined;
7257
- // A recorded explicit-state-jump event names the exact state its
7258
- // replay re-enters: the root BOSS_INTERRUPT shape is a guarded
7259
- // multi-arm list keyed on `targetId`, so the first configured arm
7260
- // may label a different state than the one the recorded event
7261
- // actually resumes.
7262
- const recordedTargetId =
7263
- retryEvent.type === JUMP_EVENT_TYPE
7264
- ? (retryEvent as { targetId?: unknown }).targetId
7265
- : undefined;
7266
- const target =
7267
- typeof recordedTargetId === 'string' &&
7268
- recordedTargetId.trim().length > 0
7269
- ? recordedTargetId
7270
- : firstTransitionTarget(machine, stateId, retryEvent.type);
7271
- // PBRT-52: a label is written from a source state description, never
7272
- // from an identifier. Falling back to the target id — or, with no
7273
- // resolvable target, to the FSM event type — makes the label *be* the
7274
- // internal name, which defeats the substitution the label exists for
7275
- // and puts a machine identifier into Boss-facing text
7276
- // (CAPPLAY-5). A candidate whose label can only be an id is excluded
7277
- // exactly like one whose payload cannot be sourced.
7278
- const description =
7279
- (target === undefined ? undefined : stateDescriptions.get(target)) ??
7280
- stateDescriptions.get(stateId);
7281
- if (description === undefined) return undefined;
8901
+ function retryActionFor(stateId: string | undefined): DerivedControlAction | undefined {
8902
+ if (stateId !== 'failed' || !recoveryCheckpoint) return undefined;
8903
+ const description = stateDescriptions.get(recoveryCheckpoint.stateId);
8904
+ const savedResult = recoveryCheckpoint.result !== undefined;
8905
+ const safe = savedResult
8906
+ ? !hasUnresolvedReconciliation() && currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
8907
+ .every((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint!.stateId)
8908
+ : stepReplayIsSafe();
8909
+ if (description === undefined || !safe) return undefined;
8910
+ const entry = spec.entryEvent;
8911
+ const target = entry && firstTransitionTarget(machine, stateId, entry.type);
7282
8912
  return {
7283
8913
  action: {
7284
- id: `retry:${retryEvent.type}`,
7285
- label: `Retry: ${description}`,
7286
- // DR-063 §3: retrying a failed call is `ready` — the runtime cannot
7287
- // see whether the Boss changed the outside world since it failed.
8914
+ id: target === recoveryCheckpoint.stateId ? `retry:${entry!.type}` : 'retry:step',
8915
+ label: `${savedResult ? 'Continue from saved result' : 'Retry'}: ${description}`,
7288
8916
  standing: 'ready',
7289
8917
  },
7290
- event: retryEvent,
8918
+ retryStep: true,
7291
8919
  };
7292
8920
  }
7293
8921
 
@@ -7344,6 +8972,18 @@ export function createXStatePlaybookRuntime<
7344
8972
  return [];
7345
8973
  }
7346
8974
  const derived: DerivedControlAction[] = [];
8975
+ const repairable = state.stateId === 'failed' ? repairableReadOnlyBoundary() : undefined;
8976
+ if (repairable !== undefined) derived.push({
8977
+ action: { id: 'retry:restored-step', label: 'Restore the repository and retry the read-only step', standing: 'ready' },
8978
+ retryStep: true,
8979
+ restoreReadOnly: repairable,
8980
+ });
8981
+ const judgment = state.stateId === 'failed' ? recoverableJudgment() : undefined;
8982
+ if (judgment !== undefined) derived.push({
8983
+ action: { id: 'retry:adjudication', label: 'Retry assessment of the saved result', standing: 'ready' },
8984
+ retryJudgment: judgment,
8985
+ });
8986
+ if (judgment !== undefined && !hasUnresolvedReconciliation()) return derived;
7347
8987
  if (hasUnresolvedReconciliation()) {
7348
8988
  const operation =
7349
8989
  deferredReconciliationOperationId === undefined
@@ -7380,8 +9020,13 @@ export function createXStatePlaybookRuntime<
7380
9020
  );
7381
9021
  return derived;
7382
9022
  }
7383
- const retry = retryActionFor(snapshot, state.stateId);
9023
+ const retry = retryActionFor(state.stateId);
7384
9024
  if (retry !== undefined) derived.push(retry);
9025
+ // A jump can repeat earlier work, unlike checkpoint retry. Its whole
9026
+ // failed attempt must therefore still pass the replay fence.
9027
+ if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
9028
+ return derived;
9029
+ }
7385
9030
  // Jump entries: resumable targets whose explicit-state-jump event the
7386
9031
  // live snapshot accepts (state guards included), sent with the
7387
9032
  // advertised target id and optional textual fields omitted.
@@ -7447,9 +9092,12 @@ export function createXStatePlaybookRuntime<
7447
9092
  // state whose source declares no description publishes none: an id is
7448
9093
  // never promoted into a description by default.
7449
9094
  function stateDescriptionFor(state: PlaybookState): string | undefined {
9095
+ // DR-067 §1: inside a parallel state, the meaning published is the
9096
+ // root state's — the parallel parent's own description.
7450
9097
  const keys = [
7451
9098
  ...(state.stateId === undefined ? [] : [state.stateId]),
7452
9099
  ...(typeof state.value === 'string' ? [state.value] : []),
9100
+ ...(isPlainObject(state.value) ? Object.keys(state.value) : []),
7453
9101
  ...state.activeStateIds,
7454
9102
  ];
7455
9103
  for (const key of keys) {
@@ -7510,6 +9158,7 @@ export function createXStatePlaybookRuntime<
7510
9158
  boundSession.playbookId,
7511
9159
  { allowSuspendedCall: true },
7512
9160
  );
9161
+ validateRecoveryCheckpoint(boundSnapshot.recoveryCheckpoint);
7513
9162
  if (
7514
9163
  kind === 'adopt' &&
7515
9164
  effectLedgerCapability !== undefined &&
@@ -7617,6 +9266,7 @@ export function createXStatePlaybookRuntime<
7617
9266
  initInFlight = initialization;
7618
9267
  const initTask = (async () => {
7619
9268
  session = boundSession;
9269
+ recoveryCheckpoint = boundSnapshot.recoveryCheckpoint;
7620
9270
  syncDeferredReconciliationOverlay();
7621
9271
  refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
7622
9272
  prepareReconstructedGovernedDelivery(
@@ -7820,7 +9470,7 @@ export function createXStatePlaybookRuntime<
7820
9470
  ?.context ?? {}) as Record<string, unknown>;
7821
9471
  const actual =
7822
9472
  state?.stateId === BOSS_REPLY_WAIT_STATE_ID
7823
- ? pendingBossQuestionFromContext(context)
9473
+ ? singlePendingBossQuestion(state, context)
7824
9474
  : undefined;
7825
9475
  if (!isDeepStrictEqual(actual, expected)) {
7826
9476
  throw new Error(
@@ -7851,6 +9501,7 @@ export function createXStatePlaybookRuntime<
7851
9501
  signal: AbortSignal,
7852
9502
  turnId: number,
7853
9503
  classificationLine?: string,
9504
+ onAccepted?: () => void,
7854
9505
  ): Promise<'continued' | 'unresolved'> {
7855
9506
  if (repositoryCapability === undefined) {
7856
9507
  throw new Error(
@@ -7901,6 +9552,7 @@ export function createXStatePlaybookRuntime<
7901
9552
  effectBoundary.roleId, boundPlayerId,
7902
9553
  );
7903
9554
  continuationStarted = true;
9555
+ onAccepted?.();
7904
9556
  actor!.send(event);
7905
9557
  // The host has durably started this boundary and the FSM has
7906
9558
  // moved: publish the buffered classification line and the
@@ -8177,7 +9829,7 @@ export function createXStatePlaybookRuntime<
8177
9829
  const state = normalizePlaybookSnapshot(snapshot);
8178
9830
  const context = ((snapshot as { context?: unknown }).context ??
8179
9831
  {}) as Record<string, unknown>;
8180
- const pending = pendingBossQuestionForState(state, context);
9832
+ const pending = singlePendingBossQuestion(state, context);
8181
9833
  if (
8182
9834
  pending === undefined ||
8183
9835
  currentBoundDeferredOperation(pending)?.operationId !== operationId
@@ -8237,120 +9889,7 @@ export function createXStatePlaybookRuntime<
8237
9889
  // Defined only at a safe capture point — initialized, not disposing
8238
9890
  // or disposed, no active public boundary, and the actor quiescent with
8239
9891
  // status `active`.
8240
- exportSnapshot(): PlaybookRuntimeSnapshot | undefined {
8241
- if (!actor || !session || disposed || disposalPromise !== undefined) {
8242
- return undefined;
8243
- }
8244
- if (activeSignal !== undefined) return undefined;
8245
- if (deferredSettlementClosure !== undefined) return undefined;
8246
- const pendingCall = nestedBridge.getPendingCall();
8247
- const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
8248
- if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
8249
- return undefined;
8250
- }
8251
- let suspendedCall: typeof bridgeSuspendedCall;
8252
- if (bridgeSuspendedCall !== undefined) {
8253
- if (
8254
- pendingCall?.callId !== bridgeSuspendedCall.callId ||
8255
- pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
8256
- pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId
8257
- ) {
8258
- return undefined;
8259
- }
8260
- if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
8261
- return undefined;
8262
- }
8263
- const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
8264
- if (
8265
- bridgeSuspendedCall.turnId !== undefined &&
8266
- bridgeSuspendedCall.turnId !== turnId
8267
- ) {
8268
- return undefined;
8269
- }
8270
- suspendedCall = {
8271
- ...bridgeSuspendedCall,
8272
- ...(turnId === undefined ? {} : { turnId }),
8273
- ...(hasGovernedPlayerStates
8274
- ? {
8275
- effectBoundaryPrefixSequence:
8276
- playbookCallEffectPrefixes.get(
8277
- bridgeSuspendedCall.callId,
8278
- ) ?? null,
8279
- }
8280
- : {}),
8281
- };
8282
- }
8283
- const state = currentState();
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.
8291
- const machineSnapshot = detachPersistedMachineSnapshot(
8292
- actor.getPersistedSnapshot(),
8293
- state.stateId === 'failed'
8294
- ? { lastError: failedStateError(context ?? {}) }
8295
- : {},
8296
- );
8297
- effectLedgerMirror = currentEffectLedger();
8298
- refreshRetainedEffectReconciliation(effectLedgerMirror);
8299
- syncDeferredReconciliationOverlay();
8300
- refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
8301
- const pending =
8302
- !hasUnresolvedReconciliation()
8303
- ? pendingBossQuestionForState(state, context ?? {})
8304
- : undefined;
8305
- const failedEffectAttempt =
8306
- hasGovernedPlayerStates &&
8307
- state.stateId === 'failed' &&
8308
- failedAttemptMatchesCurrentLedger(effectLedgerMirror)
8309
- ? {
8310
- boundaryPrefix: failedEffectBoundaryPrefix!,
8311
- attemptId: failedGovernedAttemptId ?? null,
8312
- }
8313
- : undefined;
8314
- return {
8315
- schemaVersion: 4,
8316
- playbookId: session.playbookId,
8317
- machine: machineSnapshot,
8318
- roleResumeTokens: snapshotRoleResumeTokens(),
8319
- sequences: {
8320
- trace: traceSequence,
8321
- turn: turnSequence,
8322
- judgeCall: judgeCallSequence,
8323
- playerCall: playerCallSequence,
8324
- playbookCall: playbookCallSequence,
8325
- ...(declaredActors.has('captain')
8326
- ? { captainCall: captainCallSequence }
8327
- : {}),
8328
- },
8329
- state,
8330
- pendingBossQuestions:
8331
- pending === undefined
8332
- ? []
8333
- : [
8334
- {
8335
- questionId: pending.questionId,
8336
- asker: pending.asker,
8337
- question: pending.question,
8338
- sourceItem: pending.sourceItem,
8339
- },
8340
- ],
8341
- effectLedger: effectLedgerMirror,
8342
- ...(retainedEffectSourceSessionId === undefined
8343
- ? {}
8344
- : { retainedEffectSourceSessionId }),
8345
- ...(retainedEffectReconciliation === undefined
8346
- ? {}
8347
- : { retainedEffectReconciliation }),
8348
- ...(failedEffectAttempt === undefined
8349
- ? {}
8350
- : { failedEffectAttempt }),
8351
- ...(suspendedCall === undefined ? {} : { suspendedCall }),
8352
- };
8353
- },
9892
+ exportSnapshot: (checkpoint?: { child?: PlaybookPendingCall }) => exportRuntimeSnapshot(checkpoint?.child ? { child: checkpoint.child } : undefined),
8354
9893
 
8355
9894
  // DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
8356
9895
  // exported snapshot under the same immutable session identity.
@@ -8365,17 +9904,23 @@ export function createXStatePlaybookRuntime<
8365
9904
  },
8366
9905
 
8367
9906
  // DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
8368
- // engagement identity and counter lineage, exposed separately so
8369
- // capability-less bespoke runtimes can omit it. Runtime-visible
8370
- // preflight mismatches reject before effects; after preflight the new
9907
+ // engagement identity and counter lineage, exposed separately so a
9908
+ // capability-less runtime can omit it. Runtime-visible preflight
9909
+ // mismatches reject before effects; after preflight the new
8371
9910
  // session.started boundary owns failed-start cleanup just like init.
8372
- async adopt(
8373
- nextSession: PlaybookSession,
8374
- snapshot: PlaybookRuntimeSnapshot,
8375
- context: PlaybookAdoptionContext,
8376
- ): Promise<void> {
8377
- await rehydrateSnapshot('adopt', nextSession, snapshot, context);
8378
- },
9911
+ // DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
9912
+ // machine that declares a parallel state omits the member.
9913
+ ...(parallelProfile === undefined
9914
+ ? {
9915
+ async adopt(
9916
+ nextSession: PlaybookSession,
9917
+ snapshot: PlaybookRuntimeSnapshot,
9918
+ context: PlaybookAdoptionContext,
9919
+ ): Promise<void> {
9920
+ await rehydrateSnapshot('adopt', nextSession, snapshot, context);
9921
+ },
9922
+ }
9923
+ : {}),
8379
9924
 
8380
9925
  // DR-029 / PBRT-52: side-effect-free control view over the live
8381
9926
  // snapshot, valid at parked quiescence outside an active boundary.
@@ -8403,10 +9948,9 @@ export function createXStatePlaybookRuntime<
8403
9948
  const state = currentState();
8404
9949
  const context = ((snapshot as { context?: unknown }).context ??
8405
9950
  {}) as Record<string, unknown>;
8406
- const pending =
8407
- !hasUnresolvedReconciliation()
8408
- ? pendingBossQuestionForState(state, context)
8409
- : undefined;
9951
+ const pendingQuestions = !hasUnresolvedReconciliation()
9952
+ ? pendingBossQuestionsFor(state, context)
9953
+ : [];
8410
9954
  const lastError =
8411
9955
  state.stateId === 'failed'
8412
9956
  ? failedStateError(context)
@@ -8416,25 +9960,46 @@ export function createXStatePlaybookRuntime<
8416
9960
  !hasUnresolvedReconciliation()
8417
9961
  ? stateDescriptionFor(state)
8418
9962
  : undefined;
9963
+ const actions = deriveControlActions(snapshot).map(({ action }) => action);
9964
+ const retry = actions.find(({ id, standing }) =>
9965
+ id.startsWith('retry:') &&
9966
+ (standing ?? 'ready') === 'ready');
9967
+ const stoppedBoundary = recoveryCheckpoint === undefined ? undefined :
9968
+ currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
9969
+ .filter((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint!.stateId).at(-1);
9970
+ const recovery = recoveryCheckpoint === undefined ||
9971
+ (pendingQuestions.length === 0 && retry === undefined) ? undefined : {
9972
+ prompt: recoveryCheckpoint.prompt,
9973
+ ...(stoppedBoundary === undefined ? {} : { evidence: snapshotJsonValue({
9974
+ declaredResults: stoppedBoundary.sourceOutcomeSchema,
9975
+ ...(stoppedBoundary.finalText === undefined ? {} : { savedPlayerText: stoppedBoundary.finalText }),
9976
+ ...(stoppedBoundary.semanticCandidate === undefined ? {} : { selectedResult: stoppedBoundary.semanticCandidate }),
9977
+ ...(stoppedBoundary.physicalReceipt === undefined ? {} : { repositoryReceipt: stoppedBoundary.physicalReceipt }),
9978
+ }) }),
9979
+ ...(repairableReadOnlyBoundary() === undefined ? {} : {
9980
+ preparation: `This read-only step changed the repository. Restore its exact original HEAD and visible files before retrying; do not accept its saved result. Preserve user work. You may put generated outputs aside and prevent their return using local environment or ignore settings. Original observation: ${JSON.stringify(repairableReadOnlyBoundary()!.baseline)}. Observed after the failed step: ${JSON.stringify(repairableReadOnlyBoundary()!.after)}.`,
9981
+ }),
9982
+ ...(stateDescriptions.get(recoveryCheckpoint.stateId) === undefined ? {} :
9983
+ { description: stateDescriptions.get(recoveryCheckpoint.stateId)! }),
9984
+ continuation: pendingQuestions.length === 0
9985
+ ? { kind: 'runtime' as const, actionId: retry!.id }
9986
+ : { kind: 'reply' as const },
9987
+ };
8419
9988
  return deepFreeze({
8420
9989
  state,
8421
9990
  ...(stateDescription === undefined ? {} : { stateDescription }),
8422
9991
  ...(projectedContext !== undefined
8423
9992
  ? { context: projectedContext }
8424
9993
  : {}),
8425
- pendingQuestions:
8426
- pending === undefined
8427
- ? []
8428
- : [
8429
- {
8430
- questionId: pending.questionId,
8431
- asker: pending.asker,
8432
- question: pending.question,
8433
- sourceItem: pending.sourceItem,
8434
- },
8435
- ],
9994
+ pendingQuestions: pendingQuestions.map((pending) => ({
9995
+ questionId: pending.questionId,
9996
+ asker: pending.asker,
9997
+ question: pending.question,
9998
+ sourceItem: pending.sourceItem,
9999
+ })),
8436
10000
  ...(lastError !== undefined ? { lastError } : {}),
8437
- actions: deriveControlActions(snapshot).map(({ action }) => action),
10001
+ actions,
10002
+ ...(recovery === undefined ? {} : { recovery }),
8438
10003
  });
8439
10004
  },
8440
10005
 
@@ -8501,6 +10066,7 @@ export function createXStatePlaybookRuntime<
8501
10066
  const callId = `apply-${++applyCallSequence}`;
8502
10067
  const position: TracePosition = { turnId, callId };
8503
10068
  activeTurnId = turnId;
10069
+ publicBoundarySequence += 1;
8504
10070
  beginAutomaticReplayBoundary();
8505
10071
  activeSignal = signal;
8506
10072
  activeAborts = abortReasonClassifier(signal);
@@ -8634,7 +10200,10 @@ export function createXStatePlaybookRuntime<
8634
10200
  accepted = true;
8635
10201
  try {
8636
10202
  let run: PlaybookRunResult;
8637
- if (candidate.unresolvedEffectAction === 'abandon') {
10203
+ if (candidate.retryJudgment !== undefined) {
10204
+ await recoverJudgment(candidate.retryJudgment, signal);
10205
+ run = runResultFor(settledOutcome(signal));
10206
+ } else if (candidate.unresolvedEffectAction === 'abandon') {
8638
10207
  signal.throwIfAborted();
8639
10208
  run = runResultFor('unresolved-effect');
8640
10209
  } else if (
@@ -8657,6 +10226,19 @@ export function createXStatePlaybookRuntime<
8657
10226
  ? 'no-action'
8658
10227
  : 'quiescent',
8659
10228
  );
10229
+ } else if (candidate.retryStep) {
10230
+ if (candidate.restoreReadOnly !== undefined) {
10231
+ await confirmReadOnlyRestoration(candidate.restoreReadOnly, signal);
10232
+ }
10233
+ const checkpoint = recoveryCheckpoint!;
10234
+ requiredRecoveryBaseline = currentEffectLedger().boundaries
10235
+ .slice(checkpoint.boundaryPrefix).find(({ restored }) => restored !== undefined)?.restored;
10236
+ try {
10237
+ await resumeCheckpoint(checkpoint);
10238
+ } finally {
10239
+ requiredRecoveryBaseline = undefined;
10240
+ }
10241
+ run = runResultFor(settledOutcome(signal));
8660
10242
  } else {
8661
10243
  actor.send(candidate.event!);
8662
10244
  await waitForPlaybookQuiescence(actor, {
@@ -8669,6 +10251,7 @@ export function createXStatePlaybookRuntime<
8669
10251
  run.outcome === 'failed' || run.outcome === 'aborted'
8670
10252
  ? {
8671
10253
  disposition: 'failed',
10254
+ run,
8672
10255
  error:
8673
10256
  ('error' in run ? run.error : undefined) ??
8674
10257
  normalizeError(
@@ -8701,6 +10284,7 @@ export function createXStatePlaybookRuntime<
8701
10284
  if (receipt !== undefined && receipt.disposition !== 'rejected') {
8702
10285
  appliedReceipts.set(key, receipt);
8703
10286
  }
10287
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
8704
10288
  try {
8705
10289
  await drainEmissions();
8706
10290
  } catch (error) {
@@ -8781,9 +10365,11 @@ export function createXStatePlaybookRuntime<
8781
10365
  async handleBossInput({
8782
10366
  text,
8783
10367
  signal,
10368
+ onAccepted,
8784
10369
  }: {
8785
10370
  text: string;
8786
10371
  signal: AbortSignal;
10372
+ onAccepted?: () => void;
8787
10373
  }): Promise<PlaybookRunResult> {
8788
10374
  if (!actor || !savedPorts) {
8789
10375
  throw new Error(
@@ -8804,6 +10390,7 @@ export function createXStatePlaybookRuntime<
8804
10390
  refreshRetainedEffectFenceFromHost();
8805
10391
  const turnId = ++turnSequence;
8806
10392
  activeTurnId = turnId;
10393
+ publicBoundarySequence += 1;
8807
10394
  beginAutomaticReplayBoundary();
8808
10395
  activeSignal = signal;
8809
10396
  activeAborts = abortReasonClassifier(signal);
@@ -8845,7 +10432,7 @@ export function createXStatePlaybookRuntime<
8845
10432
  const stateId = normalizePlaybookSnapshot(snapshot).stateId;
8846
10433
  const snapshotContext = ((snapshot as { context?: unknown })
8847
10434
  .context ?? {}) as Record<string, unknown>;
8848
- deferredPending = pendingBossQuestionForState(
10435
+ deferredPending = singlePendingBossQuestion(
8849
10436
  normalizePlaybookSnapshot(snapshot),
8850
10437
  snapshotContext,
8851
10438
  );
@@ -8928,16 +10515,9 @@ export function createXStatePlaybookRuntime<
8928
10515
  signal,
8929
10516
  turnId,
8930
10517
  statusLine,
10518
+ onAccepted,
8931
10519
  );
8932
10520
  if (continuation === 'continued') {
8933
- try {
8934
- lastBossEvent = snapshotJsonValue(
8935
- event,
8936
- 'recorded Boss event',
8937
- ) as unknown as EventObject;
8938
- } catch {
8939
- lastBossEvent = undefined;
8940
- }
8941
10521
  if (controlPlaneError !== undefined) {
8942
10522
  throw controlPlaneError;
8943
10523
  }
@@ -8947,7 +10527,6 @@ export function createXStatePlaybookRuntime<
8947
10527
  : 'no-action',
8948
10528
  );
8949
10529
  } else {
8950
- lastBossEvent = undefined;
8951
10530
  result = runResultFor('no-action');
8952
10531
  }
8953
10532
  handledDeferred = true;
@@ -8960,7 +10539,6 @@ export function createXStatePlaybookRuntime<
8960
10539
  deferredOperation.operationId,
8961
10540
  signal,
8962
10541
  );
8963
- lastBossEvent = undefined;
8964
10542
  result = runResultFor('no-action');
8965
10543
  handledDeferred = true;
8966
10544
  }
@@ -8989,18 +10567,7 @@ export function createXStatePlaybookRuntime<
8989
10567
  suppressInspectionEmissions = false;
8990
10568
  actor.start();
8991
10569
  }
8992
- // DR-029: keep the classified event with its recorded payload
8993
- // as the retry-replay source. Recording is sanitizing, not
8994
- // load-bearing: an override classifier's non-JSON-safe event is
8995
- // simply not recorded, and the turn proceeds unchanged.
8996
- try {
8997
- lastBossEvent = snapshotJsonValue(
8998
- event,
8999
- 'recorded Boss event',
9000
- ) as unknown as EventObject;
9001
- } catch {
9002
- lastBossEvent = undefined;
9003
- }
10570
+ onAccepted?.();
9004
10571
  actor.send(event);
9005
10572
  await waitForPlaybookQuiescence(actor, {
9006
10573
  pendingCalls: nestedBridge,
@@ -9012,6 +10579,7 @@ export function createXStatePlaybookRuntime<
9012
10579
  operationError = error;
9013
10580
  }
9014
10581
 
10582
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
9015
10583
  let drainError: unknown;
9016
10584
  try {
9017
10585
  await drainEmissions();
@@ -9125,6 +10693,7 @@ export function createXStatePlaybookRuntime<
9125
10693
  return runResultFor('no-action');
9126
10694
  }
9127
10695
  activeTurnId = playbookCallTurnIds.get(input.callId);
10696
+ publicBoundarySequence += 1;
9128
10697
  bindAutomaticReplayBoundary(
9129
10698
  playbookCallEffectPrefixes.has(input.callId)
9130
10699
  ? playbookCallEffectPrefixes.get(input.callId)
@@ -9169,6 +10738,7 @@ export function createXStatePlaybookRuntime<
9169
10738
  error: normalizeError(input.signal.reason),
9170
10739
  };
9171
10740
  }
10741
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
9172
10742
  let drainError: unknown;
9173
10743
  try {
9174
10744
  await drainEmissions();
@@ -9286,10 +10856,14 @@ export function createXStatePlaybookRuntime<
9286
10856
  emissionQueue.clear();
9287
10857
  judgeQueue.clear();
9288
10858
  appliedReceipts.clear();
10859
+ pendingCohorts.clear();
10860
+ consumedCohortBoundaries.clear();
10861
+ activeBoundaryCalls.clear();
10862
+ cohortCompletionQueue.clear();
9289
10863
  actor = undefined;
9290
10864
  activeSignal = undefined;
9291
10865
  activeAborts = undefined;
9292
- actorSettlementAborts = undefined;
10866
+ actorSettlementAborts.length = 0;
9293
10867
  actorSettlementErrorAborts = undefined;
9294
10868
  activeAbortEmission = undefined;
9295
10869
  activeTurnId = undefined;
@@ -9301,7 +10875,6 @@ export function createXStatePlaybookRuntime<
9301
10875
  failedGovernedAttemptId = undefined;
9302
10876
  controlPlaneError = undefined;
9303
10877
  emissionFailure = undefined;
9304
- lastBossEvent = undefined;
9305
10878
  retainedEffectSourceSessionId = undefined;
9306
10879
  retainedEffectReconciliation = undefined;
9307
10880
  retainedEffectReconciliationRequired = false;