@sublang/playbook 15.1.0 → 16.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 (76) hide show
  1. package/docs/embedding.md +2 -2
  2. package/package.json +1 -1
  3. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  4. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  5. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  6. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  7. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  8. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  9. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  10. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  11. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  12. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  13. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  14. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  15. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  16. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  17. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  18. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  19. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  20. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  21. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  22. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  23. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  24. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  25. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  26. package/reference/sdlc/decide.md +1 -0
  27. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  28. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  29. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  30. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  31. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  32. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  33. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  34. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  35. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  36. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  37. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  38. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  39. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  40. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  41. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  42. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  43. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  44. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  45. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  46. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  47. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  48. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  49. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  50. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  51. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  52. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  53. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  54. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  55. package/reference/sdlc/review.md +3 -3
  56. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  57. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  58. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  59. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  60. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  61. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  62. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  63. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  64. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  65. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  66. package/slc/gears2fsm.md +72 -3
  67. package/slc/link.md +50 -27
  68. package/slc/materialize-link.mjs +51 -13
  69. package/slc/optimize.md +4 -1
  70. package/slc/prefix-prompts.mjs +29 -28
  71. package/slc/prefix.md +27 -24
  72. package/slc/slc.pin-inputs.json +11 -2
  73. package/slc/text2gears.md +71 -11
  74. package/src/xstate-playbook-runtime.d.ts +54 -15
  75. package/src/xstate-playbook-runtime.js +1154 -248
  76. package/src/xstate-playbook-runtime.ts +1603 -317
@@ -158,7 +158,7 @@ export interface RuntimeBoundaryCalls {
158
158
  * with the cause the boundary decided at the call itself, where the role,
159
159
  * the resolved player, and the reported error are known.
160
160
  */
161
- markPlayerResultFailure?(error: Error): Error;
161
+ markPlayerResultFailure?(error: Error, result?: PlayerResult): Error;
162
162
  callJudge(
163
163
  purpose: JudgePurpose,
164
164
  stateId: string | undefined,
@@ -339,8 +339,8 @@ export function playerFailureCause(input: {
339
339
  /**
340
340
  * DR-063 §1: the cause of an unresolved governed settlement. A reconciled
341
341
  * mismatch supplies its own receipt-read cause; the reasons a runtime owns map
342
- * to adjudication, abort, and runtime-defect codes. Shared with DECIDE's
343
- * bespoke runtime, which decides the same reasons.
342
+ * to adjudication, abort, and runtime-defect codes. Exported so linked
343
+ * machinery of an artifact's own decides the same reasons.
344
344
  */
345
345
  export function governedSettlementCause(
346
346
  reason: string,
@@ -365,11 +365,12 @@ export function governedSettlementCause(
365
365
 
366
366
  /**
367
367
  * DR-063 §2: the causes decided for thrown values that cannot carry the
368
- * marker — a string, a frozen error — kept by identity for as long as the
369
- * failure stands, so every surface that reads the FSM's own `lastError`
370
- * publishes the cause decided for exactly that value, whatever turn reads it.
371
- * A value that carries the marker answers from it; the map is bounded, since
372
- * only values that refuse the marker need an entry.
368
+ * marker — a string, a frozen error — and for the record a machine keeps in
369
+ * place of the thrown value, kept by identity for as long as the failure
370
+ * stands, so every surface that reads the FSM's own `lastError` publishes the
371
+ * cause decided for exactly that failure, whatever turn reads it. A value
372
+ * that carries the marker answers from it; the map is bounded, since only
373
+ * values that lack the marker need an entry.
373
374
  */
374
375
  export interface PlaybookFailureCauseRetention {
375
376
  retain(error: unknown, cause: PlaybookFailureCause): void;
@@ -396,6 +397,23 @@ export function createFailureCauseRetention(): PlaybookFailureCauseRetention {
396
397
  };
397
398
  }
398
399
 
400
+ /**
401
+ * DR-063 §2: the value an invoked actor's rejection carried into the machine,
402
+ * read from the XState error event that delivered it, or `undefined` for any
403
+ * other event.
404
+ */
405
+ function actorErrorOf(event: unknown): unknown {
406
+ if (typeof event !== 'object' || event === null) return undefined;
407
+ try {
408
+ const { type, error } = event as { type?: unknown; error?: unknown };
409
+ return typeof type === 'string' && type.startsWith('xstate.error.actor.')
410
+ ? error
411
+ : undefined;
412
+ } catch {
413
+ return undefined;
414
+ }
415
+ }
416
+
399
417
 
400
418
  // ---------------------------------------------------------------------------
401
419
  // DR-028: both call boundaries treat an `ok` result whose `finalText` is
@@ -508,6 +526,23 @@ type XStateEffectBoundarySeed = Omit<
508
526
  'playbookId' | 'canonicalWorktree' | 'baseline' | 'cohortId'
509
527
  >;
510
528
 
529
+ /** One member's completion inside a repository cohort (DR-067 §1). */
530
+ type XStateRepositoryCohortCompletion<T> =
531
+ XStateRepositoryExclusiveCompletion<T> & { readonly roleId: string };
532
+
533
+ interface XStateRepositoryCohortResult<T> {
534
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
535
+ readonly invocationId: string;
536
+ readonly operations: Readonly<
537
+ Record<
538
+ string,
539
+ XStateRepositoryOperationSettlement<T> | XStateRepositoryOperationRejection
540
+ >
541
+ >;
542
+ readonly receipts: Readonly<Record<string, PlaybookRepositoryReceipt>>;
543
+ readonly effectLedger: PlaybookEffectLedger;
544
+ }
545
+
511
546
  export interface XStateRepositoryCapability {
512
547
  runExclusive<T>(options: {
513
548
  readonly signal: AbortSignal;
@@ -542,6 +577,34 @@ export interface XStateRepositoryCapability {
542
577
  readonly signal: AbortSignal;
543
578
  readonly operationId: string;
544
579
  }): Promise<XStateRepositoryDeferredParked | XStateRepositoryDeferredRestoreResult>;
580
+ /**
581
+ * DR-067 §1 / PBRT-73: one repository claim over the working leaves of a
582
+ * parallel state entered together — every member an `unchanged` boundary
583
+ * of one cohort, the operations run concurrently under that claim, and
584
+ * one completion callback per member. Required for an artifact that
585
+ * declares a parallel state.
586
+ */
587
+ runCohort?<T>(options: {
588
+ readonly signal: AbortSignal;
589
+ readonly invocationId: string;
590
+ readonly roleIds: readonly string[];
591
+ readonly dispositionsByRole: Readonly<Record<string, readonly ['unchanged']>>;
592
+ readonly effectBoundaries: Readonly<Record<string, XStateEffectBoundarySeed>>;
593
+ readonly operations: Readonly<
594
+ Record<
595
+ string,
596
+ (context: {
597
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
598
+ readonly identity: unknown;
599
+ readonly invocationId: string;
600
+ readonly roleId: string;
601
+ }) => Promise<T>
602
+ >
603
+ >;
604
+ readonly completeEffectBoundary: (
605
+ completion: XStateRepositoryCohortCompletion<T>,
606
+ ) => XStateRepositoryCompletionEvidence | Promise<XStateRepositoryCompletionEvidence>;
607
+ }): Promise<XStateRepositoryCohortResult<T>>;
545
608
  }
546
609
 
547
610
  function assertNoConfiguredHostCapabilities(value: unknown, label: string): void {
@@ -643,6 +706,7 @@ function configuredOptionsFromFactoryInput(
643
706
  function repositoryCapabilityFromHostCapabilities(
644
707
  hostCapabilities: object | undefined,
645
708
  label: string,
709
+ requireCohort = false,
646
710
  ): XStateRepositoryCapability {
647
711
  const descriptor =
648
712
  hostCapabilities === undefined
@@ -659,10 +723,15 @@ function repositoryCapabilityFromHostCapabilities(
659
723
  Array.isArray(repository) ||
660
724
  typeof (repository as { runExclusive?: unknown }).runExclusive !==
661
725
  'function' ||
662
- typeof (repository as { runDeferred?: unknown }).runDeferred !== 'function'
726
+ typeof (repository as { runDeferred?: unknown }).runDeferred !==
727
+ 'function' ||
728
+ (requireCohort &&
729
+ typeof (repository as { runCohort?: unknown }).runCohort !== 'function')
663
730
  ) {
664
731
  throw new TypeError(
665
- `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`,
732
+ requireCohort
733
+ ? `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive, runCohort, and runDeferred`
734
+ : `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`,
666
735
  );
667
736
  }
668
737
  return repository as XStateRepositoryCapability;
@@ -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,
@@ -1943,7 +2043,7 @@ export function createPlayerBridge(
1943
2043
  const failure = new Error(
1944
2044
  result.error ?? `captainBridge: callPlayer status "${result.status}"`,
1945
2045
  );
1946
- throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
2046
+ throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
1947
2047
  }
1948
2048
  const finalText = result.finalText ?? '';
1949
2049
  if (isEmptyFinalText(finalText)) {
@@ -2170,7 +2270,81 @@ function transitionTargets(transition: unknown): string[] {
2170
2270
  return targets;
2171
2271
  }
2172
2272
 
2173
- /** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
2273
+ function transitionArmTargets(transition: unknown): string[] {
2274
+ const arms = Array.isArray(transition) ? transition : [transition];
2275
+ const targets: string[] = [];
2276
+ for (const arm of arms) {
2277
+ if (typeof arm === 'string') {
2278
+ targets.push(arm);
2279
+ } else if (isPlainObject(arm) && typeof arm.target === 'string') {
2280
+ targets.push(arm.target);
2281
+ }
2282
+ }
2283
+ return targets;
2284
+ }
2285
+
2286
+ function playbookStateIdOf(stateDef: unknown): string | undefined {
2287
+ if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
2288
+ return undefined;
2289
+ }
2290
+ const playbook = stateDef.meta.playbook;
2291
+ return isPlainObject(playbook) && typeof playbook.stateId === 'string'
2292
+ ? playbook.stateId
2293
+ : undefined;
2294
+ }
2295
+
2296
+ function stateTagsOf(stateDef: Record<string, unknown>): readonly string[] {
2297
+ const tags = stateDef.tags;
2298
+ if (typeof tags === 'string') return [tags];
2299
+ return Array.isArray(tags)
2300
+ ? tags.filter((tag): tag is string => typeof tag === 'string')
2301
+ : [];
2302
+ }
2303
+
2304
+ /** Every explicit node `id` in the config, mapped to its playbook state id. */
2305
+ function playbookStateIdsByNodeId(config: unknown): ReadonlyMap<string, string> {
2306
+ const byNodeId = new Map<string, string>();
2307
+ const visit = (stateDef: unknown): void => {
2308
+ if (!isPlainObject(stateDef)) return;
2309
+ const stateId = playbookStateIdOf(stateDef);
2310
+ if (typeof stateDef.id === 'string' && stateId !== undefined) {
2311
+ byNodeId.set(stateDef.id, stateId);
2312
+ }
2313
+ if (isPlainObject(stateDef.states)) {
2314
+ for (const child of Object.values(stateDef.states)) visit(child);
2315
+ }
2316
+ };
2317
+ if (isPlainObject(config) && isPlainObject(config.states)) {
2318
+ for (const child of Object.values(config.states)) visit(child);
2319
+ }
2320
+ return byNodeId;
2321
+ }
2322
+
2323
+ /**
2324
+ * One transition target written inside a parallel region, resolved to the
2325
+ * playbook state id it names: an `#id` reference through the node ids, and a
2326
+ * bare sibling key through the region's own leaves.
2327
+ */
2328
+ function resolveRegionTarget(
2329
+ target: string,
2330
+ regionStates: Record<string, unknown>,
2331
+ byNodeId: ReadonlyMap<string, string>,
2332
+ ): string {
2333
+ if (target.startsWith('#')) {
2334
+ const nodeId = target.slice(1);
2335
+ return byNodeId.get(nodeId) ?? nodeId;
2336
+ }
2337
+ if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
2338
+ return playbookStateIdOf(regionStates[target]) ?? target;
2339
+ }
2340
+ return target;
2341
+ }
2342
+
2343
+ /**
2344
+ * Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
2345
+ * the parallel shape gears2fsm compiles (DR-067) — the targets of each
2346
+ * region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
2347
+ */
2174
2348
  export function resumableStateIdsFromMachine(
2175
2349
  machine: AnyStateMachine,
2176
2350
  ): ReadonlySet<string> {
@@ -2178,13 +2352,321 @@ export function resumableStateIdsFromMachine(
2178
2352
  if (!isPlainObject(config) || !isPlainObject(config.states)) {
2179
2353
  return new Set();
2180
2354
  }
2355
+ const resumable = new Set<string>();
2181
2356
  const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
2182
- if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
2183
- return new Set();
2357
+ if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
2358
+ const bossReply = awaitState.on.BOSS_REPLY;
2359
+ if (bossReply !== undefined) {
2360
+ for (const target of transitionTargets(bossReply)) resumable.add(target);
2361
+ }
2362
+ }
2363
+ const byNodeId = playbookStateIdsByNodeId(config);
2364
+ for (const stateDef of Object.values(config.states)) {
2365
+ if (
2366
+ !isPlainObject(stateDef) ||
2367
+ stateDef.type !== 'parallel' ||
2368
+ !isPlainObject(stateDef.states)
2369
+ ) {
2370
+ continue;
2371
+ }
2372
+ for (const region of Object.values(stateDef.states)) {
2373
+ if (!isPlainObject(region) || !isPlainObject(region.states)) continue;
2374
+ const regionStates = region.states;
2375
+ for (const leaf of Object.values(regionStates)) {
2376
+ if (!isPlainObject(leaf) || !isPlainObject(leaf.on)) continue;
2377
+ const bossReply = leaf.on.BOSS_REPLY;
2378
+ if (bossReply === undefined) continue;
2379
+ for (const target of transitionArmTargets(bossReply)) {
2380
+ resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
2381
+ }
2382
+ }
2383
+ }
2384
+ }
2385
+ return resumable;
2386
+ }
2387
+
2388
+ /** One region of a compiled parallel state (DR-067 §1). */
2389
+ interface XStateParallelRegion {
2390
+ readonly key: string;
2391
+ readonly roleId: string;
2392
+ readonly workingStateId: string;
2393
+ readonly waitStateId: string;
2394
+ readonly finalStateId: string;
2395
+ }
2396
+
2397
+ /** One root `type: 'parallel'` state of the compiled shape. */
2398
+ interface XStateParallelState {
2399
+ readonly stateId: string;
2400
+ readonly regions: readonly XStateParallelRegion[];
2401
+ }
2402
+
2403
+ /**
2404
+ * The parallel profile derived from a machine that declares a parallel
2405
+ * state (DR-067 §1): its parallel states, each wait state's resume targets
2406
+ * — region waits and the root reply wait alike — and, per state id, the
2407
+ * config nodes whose `on` maps an active state contributes.
2408
+ */
2409
+ interface XStateParallelProfile {
2410
+ readonly states: readonly XStateParallelState[];
2411
+ readonly parallelStateByWorkingStateId: ReadonlyMap<
2412
+ string,
2413
+ XStateParallelState
2414
+ >;
2415
+ readonly waitResumeTargets: ReadonlyMap<string, ReadonlySet<string>>;
2416
+ readonly transitionNodesByStateId: ReadonlyMap<
2417
+ string,
2418
+ readonly Record<string, unknown>[]
2419
+ >;
2420
+ }
2421
+
2422
+ /** One registered cohort member awaiting its cohort's settlement. */
2423
+ interface XStateCohortMember {
2424
+ readonly region: XStateParallelRegion;
2425
+ readonly input: PlaybookPlayerInput;
2426
+ readonly roleId: string;
2427
+ readonly playerId: string | undefined;
2428
+ readonly playerKey: string;
2429
+ readonly signal: AbortSignal;
2430
+ readonly effectBoundary: XStateEffectBoundarySeed;
2431
+ readonly run: (
2432
+ callSignal: AbortSignal,
2433
+ cohortCancellation: AbortSignal,
2434
+ preExistingBlock: string | undefined,
2435
+ ) => Promise<PlayerResult>;
2436
+ readonly reportCollision: (callSignal: AbortSignal) => Promise<never>;
2437
+ readonly settled: DeferredValue<PlayerResult>;
2438
+ }
2439
+
2440
+ /** The cohort of one parallel state within one public boundary. */
2441
+ interface PendingCohort {
2442
+ readonly boundarySequence: number;
2443
+ readonly members: Map<string, XStateCohortMember>;
2444
+ started: boolean;
2445
+ }
2446
+
2447
+ function malformedParallelState(
2448
+ label: string,
2449
+ key: string,
2450
+ reason: string,
2451
+ ): Error {
2452
+ return new Error(
2453
+ `${label} parallel state ${key} is not the compiled parallel shape: ${reason}`,
2454
+ );
2455
+ }
2456
+
2457
+ /**
2458
+ * DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
2459
+ * `type: 'parallel'` states of the one shape gears2fsm compiles — each
2460
+ * region a compound state with exactly one player-invoking `playbook.busy`
2461
+ * working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
2462
+ * arms resume that working leaf, and exactly one final leaf, every leaf
2463
+ * carrying a stable `meta.playbook.stateId` distinct from every other
2464
+ * state's, and the parent's `onDone` the join. Any other compound state
2465
+ * keeps the flat-domain rejection. Returns `undefined` for a flat machine.
2466
+ */
2467
+ function parallelProfileFromMachine(
2468
+ machine: AnyStateMachine,
2469
+ label: string,
2470
+ ): XStateParallelProfile | undefined {
2471
+ const config = (machine as unknown as { config?: unknown }).config;
2472
+ if (!isPlainObject(config) || !isPlainObject(config.states)) {
2473
+ return undefined;
2474
+ }
2475
+ const rootStates = config.states;
2476
+ const parallelKeys = Object.entries(rootStates)
2477
+ .filter(
2478
+ ([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel',
2479
+ )
2480
+ .map(([key]) => key);
2481
+ for (const stateDef of Object.values(rootStates)) {
2482
+ if (
2483
+ isPlainObject(stateDef) &&
2484
+ stateDef.type !== 'parallel' &&
2485
+ isPlainObject(stateDef.states) &&
2486
+ Object.keys(stateDef.states).length > 0
2487
+ ) {
2488
+ throw new Error(
2489
+ `${label} declares a compound state; the shared runtime supports only flat single-region FSMs`,
2490
+ );
2491
+ }
2492
+ }
2493
+ if (parallelKeys.length === 0) return undefined;
2494
+
2495
+ const byNodeId = playbookStateIdsByNodeId(config);
2496
+ const seenStateIds = new Set<string>();
2497
+ for (const stateDef of Object.values(rootStates)) {
2498
+ const stateId = playbookStateIdOf(stateDef);
2499
+ if (stateId !== undefined) seenStateIds.add(stateId);
2500
+ }
2501
+ const states: XStateParallelState[] = [];
2502
+ const parallelStateByWorkingStateId = new Map<string, XStateParallelState>();
2503
+ const waitResumeTargets = new Map<string, ReadonlySet<string>>();
2504
+ const transitionNodesByStateId = new Map<
2505
+ string,
2506
+ readonly Record<string, unknown>[]
2507
+ >();
2508
+ for (const key of parallelKeys) {
2509
+ const parent = rootStates[key] as Record<string, unknown>;
2510
+ if (parent.onDone === undefined) {
2511
+ throw malformedParallelState(label, key, 'it declares no onDone join');
2512
+ }
2513
+ if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
2514
+ throw malformedParallelState(
2515
+ label,
2516
+ key,
2517
+ 'it must declare at least two regions',
2518
+ );
2519
+ }
2520
+ const regions: XStateParallelRegion[] = [];
2521
+ const roleIds = new Set<string>();
2522
+ for (const [regionKey, region] of Object.entries(parent.states)) {
2523
+ const regionReason = (reason: string) =>
2524
+ malformedParallelState(label, key, `region ${regionKey} ${reason}`);
2525
+ if (
2526
+ !isPlainObject(region) ||
2527
+ region.type === 'parallel' ||
2528
+ region.type === 'final' ||
2529
+ region.type === 'history' ||
2530
+ !isPlainObject(region.states)
2531
+ ) {
2532
+ throw regionReason('must be a compound state of leaves');
2533
+ }
2534
+ const regionStates = region.states;
2535
+ let working: [string, Record<string, unknown>] | undefined;
2536
+ let wait: [string, Record<string, unknown>] | undefined;
2537
+ let final: [string, Record<string, unknown>] | undefined;
2538
+ for (const [leafKey, leaf] of Object.entries(regionStates)) {
2539
+ if (!isPlainObject(leaf)) {
2540
+ throw regionReason(`leaf ${leafKey} must be a state object`);
2541
+ }
2542
+ if (
2543
+ (isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
2544
+ leaf.type === 'parallel' ||
2545
+ leaf.type === 'history'
2546
+ ) {
2547
+ throw regionReason(`leaf ${leafKey} must be an atomic state`);
2548
+ }
2549
+ const leafStateId = playbookStateIdOf(leaf);
2550
+ if (leafStateId === undefined || leafStateId.trim().length === 0) {
2551
+ throw regionReason(
2552
+ `leaf ${leafKey} must carry a string meta.playbook.stateId`,
2553
+ );
2554
+ }
2555
+ if (seenStateIds.has(leafStateId)) {
2556
+ throw regionReason(
2557
+ `leaf ${leafKey} reuses playbook state id ${leafStateId}`,
2558
+ );
2559
+ }
2560
+ seenStateIds.add(leafStateId);
2561
+ const invoke = leaf.invoke;
2562
+ const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
2563
+ const tags = stateTagsOf(leaf);
2564
+ if (leaf.type === 'final') {
2565
+ if (final !== undefined) throw regionReason('declares two final leaves');
2566
+ final = [leafKey, leaf];
2567
+ } else if (
2568
+ invokes.length === 1 &&
2569
+ isPlainObject(invokes[0]) &&
2570
+ invokes[0].src === 'player' &&
2571
+ tags.includes('playbook.busy')
2572
+ ) {
2573
+ if (working !== undefined) {
2574
+ throw regionReason('declares two working leaves');
2575
+ }
2576
+ working = [leafKey, leaf];
2577
+ } else if (
2578
+ invokes.length === 0 &&
2579
+ tags.includes('playbook.parked') &&
2580
+ isPlainObject(leaf.on) &&
2581
+ leaf.on.BOSS_REPLY !== undefined
2582
+ ) {
2583
+ if (wait !== undefined) throw regionReason('declares two wait leaves');
2584
+ wait = [leafKey, leaf];
2585
+ } else {
2586
+ throw regionReason(
2587
+ `leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`,
2588
+ );
2589
+ }
2590
+ }
2591
+ if (working === undefined || wait === undefined || final === undefined) {
2592
+ throw regionReason(
2593
+ 'must hold exactly one working leaf, one wait leaf, and one final leaf',
2594
+ );
2595
+ }
2596
+ if (region.initial !== working[0]) {
2597
+ throw regionReason('must start in its working leaf');
2598
+ }
2599
+ const workingStateId = playbookStateIdOf(working[1])!;
2600
+ const waitStateId = playbookStateIdOf(wait[1])!;
2601
+ const finalStateId = playbookStateIdOf(final[1])!;
2602
+ const replyTargets = transitionArmTargets(
2603
+ (wait[1].on as Record<string, unknown>).BOSS_REPLY,
2604
+ ).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
2605
+ const regionStateIds = new Set([waitStateId, finalStateId]);
2606
+ if (
2607
+ !replyTargets.includes(workingStateId) ||
2608
+ replyTargets.some((target) => regionStateIds.has(target))
2609
+ ) {
2610
+ throw regionReason(
2611
+ 'wait leaf BOSS_REPLY arms must resume its working leaf',
2612
+ );
2613
+ }
2614
+ const workingMeta = (working[1].meta as Record<string, unknown>)
2615
+ .playbook as Record<string, unknown>;
2616
+ const roleId = workingMeta.role;
2617
+ if (typeof roleId !== 'string' || roleId.trim().length === 0) {
2618
+ throw regionReason('working leaf must declare meta.playbook.role');
2619
+ }
2620
+ if (roleIds.has(roleId)) {
2621
+ throw malformedParallelState(
2622
+ label,
2623
+ key,
2624
+ `role ${roleId} works in more than one region`,
2625
+ );
2626
+ }
2627
+ roleIds.add(roleId);
2628
+ regions.push({
2629
+ key: regionKey,
2630
+ roleId,
2631
+ workingStateId,
2632
+ waitStateId,
2633
+ finalStateId,
2634
+ });
2635
+ waitResumeTargets.set(waitStateId, new Set([workingStateId]));
2636
+ for (const [, leaf] of [working, wait, final]) {
2637
+ transitionNodesByStateId.set(playbookStateIdOf(leaf)!, [region, leaf]);
2638
+ }
2639
+ }
2640
+ const parallelState: XStateParallelState = {
2641
+ stateId: key,
2642
+ regions: Object.freeze(regions),
2643
+ };
2644
+ states.push(parallelState);
2645
+ for (const region of regions) {
2646
+ parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
2647
+ }
2648
+ transitionNodesByStateId.set(key, [parent]);
2649
+ }
2650
+ for (const [key, stateDef] of Object.entries(rootStates)) {
2651
+ if (!isPlainObject(stateDef) || parallelKeys.includes(key)) continue;
2652
+ transitionNodesByStateId.set(key, [stateDef]);
2653
+ }
2654
+ const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
2655
+ if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
2656
+ const bossReply = rootWait.on.BOSS_REPLY;
2657
+ if (bossReply !== undefined) {
2658
+ waitResumeTargets.set(
2659
+ BOSS_REPLY_WAIT_STATE_ID,
2660
+ new Set(transitionTargets(bossReply)),
2661
+ );
2662
+ }
2184
2663
  }
2185
- const bossReply = awaitState.on.BOSS_REPLY;
2186
- if (bossReply === undefined) return new Set();
2187
- return new Set(transitionTargets(bossReply));
2664
+ return Object.freeze({
2665
+ states: Object.freeze(states),
2666
+ parallelStateByWorkingStateId,
2667
+ waitResumeTargets,
2668
+ transitionNodesByStateId,
2669
+ });
2188
2670
  }
2189
2671
 
2190
2672
  // ---------------------------------------------------------------------------
@@ -2997,10 +3479,63 @@ function eventContractPrompt(contract: XStateBossEventSpec): string {
2997
3479
  }`;
2998
3480
  }
2999
3481
 
3482
+ /**
3483
+ * DR-067 §1: the questions pending in a parallel-declaring machine — the
3484
+ * keyed records whose resume state an active wait state (a region wait leaf
3485
+ * or the root reply wait) resumes.
3486
+ */
3487
+ function parallelPendingBossQuestions(
3488
+ profile: XStateParallelProfile,
3489
+ activeStateIds: readonly string[],
3490
+ context: Record<string, unknown>,
3491
+ ): PlaybookPendingBossQuestionContext[] {
3492
+ return keyedPendingBossQuestions(context, (resumeStateId) =>
3493
+ activeStateIds.some(
3494
+ (stateId) =>
3495
+ profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true,
3496
+ ),
3497
+ );
3498
+ }
3499
+
3500
+ /** Event types the root and every active state node of a parallel machine configure. */
3501
+ function parallelConfiguredEventTypes(
3502
+ machine: AnyStateMachine,
3503
+ profile: XStateParallelProfile,
3504
+ activeStateIds: readonly string[],
3505
+ ): ReadonlySet<string> {
3506
+ const configured = new Set<string>();
3507
+ const config = (machine as unknown as { config?: unknown }).config;
3508
+ if (!isPlainObject(config)) return configured;
3509
+ if (isPlainObject(config.on)) {
3510
+ for (const type of Object.keys(config.on)) configured.add(type);
3511
+ }
3512
+ for (const stateId of activeStateIds) {
3513
+ for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
3514
+ if (!isPlainObject(node.on)) continue;
3515
+ for (const type of Object.keys(node.on)) configured.add(type);
3516
+ }
3517
+ }
3518
+ return configured;
3519
+ }
3520
+
3521
+ function activeStateIdsOf(snapshotOrState: unknown): readonly string[] {
3522
+ const listed = (snapshotOrState as { activeStateIds?: unknown } | null)
3523
+ ?.activeStateIds;
3524
+ if (Array.isArray(listed)) {
3525
+ return listed.filter((stateId): stateId is string => typeof stateId === 'string');
3526
+ }
3527
+ try {
3528
+ return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
3529
+ } catch {
3530
+ return [];
3531
+ }
3532
+ }
3533
+
3000
3534
  function makeDefaultClassifyBossText(
3001
3535
  machine: AnyStateMachine,
3002
3536
  entryEvent: { type: string; textField: string } | undefined,
3003
3537
  bossEvents: readonly XStateBossEventSpec[],
3538
+ parallelProfile?: XStateParallelProfile,
3004
3539
  ): (
3005
3540
  text: string,
3006
3541
  ports: PlaybookPorts,
@@ -3020,16 +3555,54 @@ function makeDefaultClassifyBossText(
3020
3555
  // is answered history, so the prompt must not present it as pending —
3021
3556
  // a judge told a question awaits at the failure state is steered toward
3022
3557
  // 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
- );
3558
+ let pendingQuestions: readonly PlaybookPendingBossQuestionContext[];
3559
+ let configuredTypes: ReadonlySet<string>;
3560
+ if (parallelProfile === undefined) {
3561
+ const pending =
3562
+ stateId === BOSS_REPLY_WAIT_STATE_ID
3563
+ ? pendingBossQuestionFromContext(state.context)
3564
+ : undefined;
3565
+ pendingQuestions = pending === undefined ? [] : [pending];
3566
+ configuredTypes = configuredEventTypesForState(machine, stateId);
3567
+ } else {
3568
+ // DR-067 §1: every question pending in any active wait is offered, and
3569
+ // the events of every active state node apply.
3570
+ const activeStateIds = activeStateIdsOf(snapshotOrState);
3571
+ pendingQuestions = parallelPendingBossQuestions(
3572
+ parallelProfile,
3573
+ activeStateIds,
3574
+ state.context,
3575
+ );
3576
+ configuredTypes = parallelConfiguredEventTypes(
3577
+ machine,
3578
+ parallelProfile,
3579
+ activeStateIds,
3580
+ );
3581
+ }
3582
+ // Several pending questions: a reply must name the one it answers, and
3583
+ // the classifier never guesses among them (gears2fsm §Parallel groups).
3584
+ const severalPending = pendingQuestions.length > 1;
3585
+ const applicable = [...contracts.values()]
3586
+ .filter(
3587
+ (contract) =>
3588
+ configuredTypes.has(contract.type) &&
3589
+ (contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0),
3590
+ )
3591
+ .map((contract): XStateBossEventSpec =>
3592
+ contract.type === 'BOSS_REPLY' && severalPending
3593
+ ? {
3594
+ type: 'BOSS_REPLY',
3595
+ fields: {
3596
+ questionId: {
3597
+ source: 'judge',
3598
+ required: true,
3599
+ values: pendingQuestions.map(({ questionId }) => questionId),
3600
+ },
3601
+ answer: { source: 'text', required: true },
3602
+ },
3603
+ }
3604
+ : contract,
3605
+ );
3033
3606
 
3034
3607
  const lines = [
3035
3608
  'Classify the following Boss message into exactly one event.',
@@ -3038,13 +3611,18 @@ function makeDefaultClassifyBossText(
3038
3611
  '',
3039
3612
  `Current state: ${currentState}`,
3040
3613
  ];
3041
- if (pending !== undefined) {
3614
+ for (const pending of pendingQuestions) {
3042
3615
  lines.push(
3043
3616
  `Pending question id: ${pending.questionId}`,
3044
3617
  `Pending asker: ${askerLabel(pending.asker)}`,
3045
3618
  `Pending Boss question: ${pending.question}`,
3046
3619
  );
3047
3620
  }
3621
+ if (severalPending) {
3622
+ lines.push(
3623
+ 'Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.',
3624
+ );
3625
+ }
3048
3626
  lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
3049
3627
  for (const contract of applicable) {
3050
3628
  lines.push(`- ${eventContractPrompt(contract)}`);
@@ -3137,20 +3715,23 @@ function makeDefaultClassifyBossText(
3137
3715
  }
3138
3716
 
3139
3717
  if (eventType === 'BOSS_REPLY') {
3140
- if (pending === undefined) {
3718
+ if (pendingQuestions.length === 0) {
3141
3719
  await ports.emitStatus(
3142
3720
  'Classifier returned BOSS_REPLY without a pending question',
3143
3721
  );
3144
3722
  return undefined;
3145
3723
  }
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;
3724
+ if (!severalPending) {
3725
+ const pending = pendingQuestions[0]!;
3726
+ const questionId = event.questionId;
3727
+ if (questionId !== undefined && questionId !== pending.questionId) {
3728
+ await ports.emitStatus(
3729
+ `Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`,
3730
+ );
3731
+ return undefined;
3732
+ }
3733
+ event.questionId = pending.questionId;
3152
3734
  }
3153
- event.questionId = pending.questionId;
3154
3735
  }
3155
3736
  const can = (snapshotOrState as { can?: unknown } | null)?.can;
3156
3737
  if (
@@ -3187,34 +3768,6 @@ interface TracePosition {
3187
3768
  callId?: string;
3188
3769
  }
3189
3770
 
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
3771
  // PBRT-52: the factory's lookups index states by their root key, and the
3219
3772
  // published playbook identity is `meta.playbook.stateId` — the two must
3220
3773
  // coincide or a machine can advertise a pending question or retry under an
@@ -3309,10 +3862,12 @@ function assertUnfinishedFinalStateIds(
3309
3862
  * the optional parked-session snapshot capability (DR-014) and the retained-
3310
3863
  * snapshot adoption capability (DR-038).
3311
3864
  *
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.
3865
+ * Scope: flat single-region machines — every root state's
3866
+ * `meta.playbook.stateId` equal to its state key, so each snapshot exposes
3867
+ * exactly one playbook state id — plus the parallel profile of DR-067: root
3868
+ * `type: 'parallel'` states of the shape gears2fsm compiles, whose regions
3869
+ * run as one all-`unchanged` repository cohort with keyed pending questions.
3870
+ * A machine declaring a parallel state omits `adopt`.
3316
3871
  */
3317
3872
  export function createXStatePlaybookRuntime<
3318
3873
  TOptions,
@@ -3348,16 +3903,9 @@ export function createXStatePlaybookRuntime<
3348
3903
  `${label} artifacts must not derive concrete player bindings`,
3349
3904
  );
3350
3905
  }
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
- }
3906
+ // DR-067 §1: a flat machine, or one whose only compound states are root
3907
+ // parallel states of the compiled shape; everything else is rejected.
3908
+ const parallelProfile = parallelProfileFromMachine(machine, label);
3361
3909
  assertFlatStateIdentity(machine, label);
3362
3910
  assertUnfinishedFinalStateIds(
3363
3911
  spec.unfinishedFinalStateIds,
@@ -3398,6 +3946,18 @@ export function createXStatePlaybookRuntime<
3398
3946
  const declaredRoleIds = Object.freeze([
3399
3947
  ...new Set([...roleStates.values()].map(({ role }) => role)),
3400
3948
  ]);
3949
+ // DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
3950
+ // each player state, each wait state, and the failure state. A status is
3951
+ // scheduled for each one newly entered, and a status trace names a
3952
+ // `stateId` only while exactly one is active.
3953
+ const bossRelevantStateIds: ReadonlySet<string> | undefined =
3954
+ parallelProfile === undefined
3955
+ ? undefined
3956
+ : new Set([
3957
+ ...roleStates.keys(),
3958
+ ...parallelProfile.waitResumeTargets.keys(),
3959
+ 'failed',
3960
+ ]);
3401
3961
  // PBRT-52: the artifact's own ControlView context projection. Nothing is
3402
3962
  // exported by default, so an FSM context member — including one added
3403
3963
  // after this artifact was linked — is private until named here. The two
@@ -3462,6 +4022,7 @@ export function createXStatePlaybookRuntime<
3462
4022
  machine,
3463
4023
  spec.entryEvent,
3464
4024
  spec.bossEvents ?? [],
4025
+ parallelProfile,
3465
4026
  );
3466
4027
  const classifyBossText: NonNullable<
3467
4028
  XStatePlaybookRuntimeSpec<TOptions>['classifyBossText']
@@ -3485,26 +4046,144 @@ export function createXStatePlaybookRuntime<
3485
4046
  return typeof cwd === 'string' ? cwd : undefined;
3486
4047
  });
3487
4048
 
3488
- const createPlaybookRuntime = function createPlaybookRuntime(
3489
- factoryOptions: unknown,
3490
- ): PlaybookRuntime {
3491
- const construction = configuredOptionsFromFactoryInput(
3492
- factoryOptions,
3493
- label,
4049
+ // DR-067 §1: the questions pending in a state — the singular reply-wait
4050
+ // question of a flat machine, or a parallel machine's keyed questions
4051
+ // whose own wait state is active.
4052
+ const pendingBossQuestionsFor = (
4053
+ state: PlaybookState,
4054
+ context: Record<string, unknown>,
4055
+ ): readonly PlaybookPendingBossQuestionContext[] => {
4056
+ if (parallelProfile === undefined) {
4057
+ const pending = pendingBossQuestionForState(state, context);
4058
+ return pending === undefined ? [] : [pending];
4059
+ }
4060
+ return parallelPendingBossQuestions(
4061
+ parallelProfile,
4062
+ state.activeStateIds,
4063
+ context,
3494
4064
  );
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;
3502
- if (
3503
- !Object.prototype.hasOwnProperty.call(governedPlayerStates, source)
3504
- ) {
3505
- return false;
3506
- }
3507
- const declarations = governedPlayerStates[source];
4065
+ };
4066
+ const singlePendingBossQuestion = (
4067
+ state: PlaybookState,
4068
+ context: Record<string, unknown>,
4069
+ ): PlaybookPendingBossQuestionContext | undefined => {
4070
+ const pending = pendingBossQuestionsFor(state, context);
4071
+ return pending.length === 1 ? pending[0] : undefined;
4072
+ };
4073
+ // DR-067 §1: a status trace names a `stateId` only while exactly one
4074
+ // Boss-relevant state is active; a flat machine's is its one state.
4075
+ const statusTraceStateId = (state: PlaybookState): string | undefined => {
4076
+ if (bossRelevantStateIds === undefined) return state.stateId;
4077
+ const relevant = state.activeStateIds.filter((stateId) =>
4078
+ bossRelevantStateIds.has(stateId),
4079
+ );
4080
+ return relevant.length === 1 ? relevant[0] : undefined;
4081
+ };
4082
+ // DR-067 §1: a parallel machine schedules statuses for each newly entered
4083
+ // Boss-relevant state — a player state's role line, a wait state's
4084
+ // question lines, and the failure line — diffed against the previous
4085
+ // snapshot's active state ids, so a sibling still running is not
4086
+ // announced again.
4087
+ const parallelStatusesFor = (
4088
+ previousState: PlaybookState | undefined,
4089
+ state: PlaybookState,
4090
+ context: Record<string, unknown>,
4091
+ ): ScheduledStatus[] => {
4092
+ const prior = new Set(previousState?.activeStateIds ?? []);
4093
+ const statuses: ScheduledStatus[] = [];
4094
+ for (const stateId of state.activeStateIds) {
4095
+ if (prior.has(stateId) || !bossRelevantStateIds!.has(stateId)) continue;
4096
+ const resumeTargets = parallelProfile!.waitResumeTargets.get(stateId);
4097
+ if (resumeTargets !== undefined) {
4098
+ const questions = keyedPendingBossQuestions(context, (resumeStateId) =>
4099
+ resumeTargets.has(resumeStateId),
4100
+ );
4101
+ if (questions.length === 0) {
4102
+ statuses.push({ message: 'Awaiting Boss reply.' });
4103
+ continue;
4104
+ }
4105
+ for (const pending of questions) {
4106
+ statuses.push(
4107
+ {
4108
+ message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
4109
+ },
4110
+ {
4111
+ message:
4112
+ `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
4113
+ `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
4114
+ },
4115
+ );
4116
+ }
4117
+ continue;
4118
+ }
4119
+ if (stateId === 'failed') {
4120
+ const lastError = normalizeErrorCompact(context.lastError);
4121
+ statuses.push({
4122
+ message: '◆ workflow failed; awaiting Boss recovery.',
4123
+ ...(lastError === undefined
4124
+ ? {}
4125
+ : {
4126
+ data: snapshotJsonValue({ lastError }, 'failed status data'),
4127
+ }),
4128
+ });
4129
+ continue;
4130
+ }
4131
+ const roleState = roleStates.get(stateId);
4132
+ if (roleState !== undefined) {
4133
+ statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
4134
+ }
4135
+ }
4136
+ return statuses;
4137
+ };
4138
+ // DR-067 §1: each parallel state's region roles, in region order, are one
4139
+ // concurrent role set the FSM exports; a host that forwards those sets in
4140
+ // its schema-3 authority is held to them before any agent call.
4141
+ const assertDeclaredCohorts = (hostCapabilities: object): void => {
4142
+ const authority = Object.getOwnPropertyDescriptor(
4143
+ hostCapabilities,
4144
+ 'authority',
4145
+ )?.value;
4146
+ if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
4147
+ return;
4148
+ }
4149
+ const declared = authority.concurrentRoleSets as readonly unknown[];
4150
+ for (const parallelState of parallelProfile!.states) {
4151
+ const roles = parallelState.regions.map(({ roleId }) => roleId);
4152
+ if (
4153
+ !declared.some(
4154
+ (candidate) =>
4155
+ Array.isArray(candidate) &&
4156
+ candidate.length === roles.length &&
4157
+ candidate.every((roleId, index) => roleId === roles[index]),
4158
+ )
4159
+ ) {
4160
+ throw new TypeError(
4161
+ `${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`,
4162
+ );
4163
+ }
4164
+ }
4165
+ };
4166
+
4167
+ const createPlaybookRuntime = function createPlaybookRuntime(
4168
+ factoryOptions: unknown,
4169
+ ): PlaybookRuntime {
4170
+ const construction = configuredOptionsFromFactoryInput(
4171
+ factoryOptions,
4172
+ label,
4173
+ );
4174
+ const configuredOptions = construction.configuredOptions as TOptions;
4175
+ const effectLedgerCapability = construction.effectLedger;
4176
+ const hasGovernedPlayerStates =
4177
+ Object.keys(outcomeAuthority.governedPlayerStates).length > 0;
4178
+ const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer(
4179
+ (source, acceptedOutcome) => {
4180
+ const governedPlayerStates = outcomeAuthority.governedPlayerStates;
4181
+ if (
4182
+ !Object.prototype.hasOwnProperty.call(governedPlayerStates, source)
4183
+ ) {
4184
+ return false;
4185
+ }
4186
+ const declarations = governedPlayerStates[source];
3508
4187
  return (
3509
4188
  declarations !== undefined &&
3510
4189
  Object.prototype.hasOwnProperty.call(
@@ -3519,8 +4198,12 @@ export function createXStatePlaybookRuntime<
3519
4198
  ? repositoryCapabilityFromHostCapabilities(
3520
4199
  construction.hostCapabilities,
3521
4200
  label,
4201
+ parallelProfile !== undefined,
3522
4202
  )
3523
4203
  : undefined;
4204
+ if (parallelProfile !== undefined) {
4205
+ assertDeclaredCohorts(construction.hostCapabilities);
4206
+ }
3524
4207
  const currentEffectLedger = (): PlaybookEffectLedger =>
3525
4208
  assertPlaybookEffectLedger(
3526
4209
  effectLedgerCapability.snapshot(),
@@ -3591,9 +4274,11 @@ export function createXStatePlaybookRuntime<
3591
4274
  // mutable `activeSignal` alone cannot classify a late invocation reason.
3592
4275
  let activeAborts: AbortReasonClassifier | undefined;
3593
4276
  // 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;
4277
+ // its promise actor settles. The next root snapshot/error consumes it so
4278
+ // background settlement emissions retain their owner. A flat machine has
4279
+ // one settling actor at a time, so a later binding replaces an unconsumed
4280
+ // one; a parallel machine's settlements queue in order (DR-067 §1).
4281
+ const actorSettlementAborts: (AbortReasonClassifier | undefined)[] = [];
3597
4282
  let actorSettlementErrorAborts: AbortReasonClassifier | undefined;
3598
4283
  // Exact cancellation observed by an emission owned by the active
3599
4284
  // boundary. Ordinary runs settle from their signal/state; apply also
@@ -3643,11 +4328,29 @@ export function createXStatePlaybookRuntime<
3643
4328
  // each successful result, so it is never stale.
3644
4329
  let pendingResultFailureCause: PlaybookFailureCause | undefined;
3645
4330
  // 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
4331
+ // marker — a string, a frozen error — and for the record a machine keeps
4332
+ // in place of the thrown value, kept by identity for as long as the
3647
4333
  // 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.
4334
+ // publishes the cause decided for exactly that failure, whatever turn
4335
+ // reads it.
3650
4336
  const failureCauses = createFailureCauseRetention();
4337
+ // DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
4338
+ // result, keyed by the result itself, so concurrent region calls never
4339
+ // lend one another the single slot above.
4340
+ const resultFailureCauses = new WeakMap<object, PlaybookFailureCause>();
4341
+ // DR-067 §1: the parallel profile's cohort bookkeeping. Every public
4342
+ // boundary advances the sequence; a parallel state's working leaves run
4343
+ // as one cohort at most once per boundary, and each cohort's semantic
4344
+ // completions — which the host settles concurrently — reconcile one at a
4345
+ // time so their durable correction-budget writes cannot race.
4346
+ let publicBoundarySequence = 0;
4347
+ const consumedCohortBoundaries = new Map<string, number>();
4348
+ const pendingCohorts = new Map<string, PendingCohort>();
4349
+ const cohortCompletionQueue = new PQueue({ concurrency: 1 });
4350
+ // DR-067 §1: every player, judge, and nested call a parallel machine
4351
+ // starts, so a boundary's drain waits for a cancelled sibling whose port
4352
+ // ignores its signal before it settles.
4353
+ const activeBoundaryCalls = new Set<Promise<unknown>>();
3651
4354
  // Previous root-machine state for the inspect-driven telemetry /
3652
4355
  // status emitter. undefined before the first inspect firing.
3653
4356
  let priorState: PlaybookState | undefined;
@@ -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
  );
@@ -5672,6 +6413,371 @@ export function createXStatePlaybookRuntime<
5672
6413
  failedGovernedAttemptId = attemptIds.values().next().value;
5673
6414
  }
5674
6415
 
6416
+ // -----------------------------------------------------------------
6417
+ // DR-067 §1: the parallel profile's repository cohort. The working
6418
+ // leaves of one parallel state entered together run under one
6419
+ // `runCohort` claim, each member an `unchanged` boundary: a member's
6420
+ // result is adjudicated through the one reconciler in serial, a spent
6421
+ // correction rewrites every member's boundary in one acknowledged batch,
6422
+ // members are acknowledged and released in completion order, and a
6423
+ // member's failure cancels its siblings and fails the cohort with the
6424
+ // cause decided first.
6425
+ // -----------------------------------------------------------------
6426
+
6427
+ function trackBoundaryCall<T>(call: Promise<T>): Promise<T> {
6428
+ if (parallelProfile === undefined) return call;
6429
+ activeBoundaryCalls.add(call);
6430
+ const forget = (): void => {
6431
+ activeBoundaryCalls.delete(call);
6432
+ };
6433
+ void call.then(forget, forget);
6434
+ return call;
6435
+ }
6436
+
6437
+ async function drainBoundaryCalls(): Promise<void> {
6438
+ while (activeBoundaryCalls.size > 0) {
6439
+ await Promise.allSettled([...activeBoundaryCalls]);
6440
+ }
6441
+ }
6442
+
6443
+ function cohortParallelStateFor(
6444
+ stateId: string,
6445
+ ): XStateParallelState | undefined {
6446
+ const parallelState =
6447
+ parallelProfile?.parallelStateByWorkingStateId.get(stateId);
6448
+ if (parallelState === undefined) return undefined;
6449
+ if (
6450
+ consumedCohortBoundaries.get(parallelState.stateId) ===
6451
+ publicBoundarySequence
6452
+ ) {
6453
+ return undefined;
6454
+ }
6455
+ const pending = pendingCohorts.get(parallelState.stateId);
6456
+ if (
6457
+ pending !== undefined &&
6458
+ pending.boundarySequence === publicBoundarySequence &&
6459
+ !pending.started
6460
+ ) {
6461
+ return parallelState;
6462
+ }
6463
+ const active = currentState().activeStateIds;
6464
+ return parallelState.regions.every(({ workingStateId }) =>
6465
+ active.includes(workingStateId),
6466
+ )
6467
+ ? parallelState
6468
+ : undefined;
6469
+ }
6470
+
6471
+ function startCohort(
6472
+ parallelState: XStateParallelState,
6473
+ cohort: PendingCohort,
6474
+ ): void {
6475
+ cohort.started = true;
6476
+ consumedCohortBoundaries.set(
6477
+ parallelState.stateId,
6478
+ cohort.boundarySequence,
6479
+ );
6480
+ if (pendingCohorts.get(parallelState.stateId) === cohort) {
6481
+ pendingCohorts.delete(parallelState.stateId);
6482
+ }
6483
+ }
6484
+
6485
+ async function joinCohort(
6486
+ parallelState: XStateParallelState,
6487
+ member: XStateCohortMember,
6488
+ ): Promise<PlayerResult> {
6489
+ let cohort = pendingCohorts.get(parallelState.stateId);
6490
+ if (
6491
+ cohort === undefined ||
6492
+ cohort.boundarySequence !== publicBoundarySequence ||
6493
+ cohort.started
6494
+ ) {
6495
+ cohort = {
6496
+ boundarySequence: publicBoundarySequence,
6497
+ members: new Map(),
6498
+ started: false,
6499
+ };
6500
+ pendingCohorts.set(parallelState.stateId, cohort);
6501
+ }
6502
+ const registered = cohort;
6503
+ if (registered.members.has(member.region.key)) {
6504
+ throw new Error(
6505
+ `${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`,
6506
+ );
6507
+ }
6508
+ registered.members.set(member.region.key, member);
6509
+ // A member whose invocation stops before every sibling registered —
6510
+ // its region exited, or the boundary aborted — leaves no cohort to
6511
+ // run: each registered member settles with its own cancellation and
6512
+ // the boundary's cohort is spent.
6513
+ const withdraw = (): void => {
6514
+ if (registered.started) return;
6515
+ startCohort(parallelState, registered);
6516
+ for (const candidate of registered.members.values()) {
6517
+ candidate.settled.reject(
6518
+ candidate.signal.aborted
6519
+ ? candidate.signal.reason
6520
+ : member.signal.reason,
6521
+ );
6522
+ }
6523
+ };
6524
+ if (member.signal.aborted) {
6525
+ withdraw();
6526
+ } else {
6527
+ member.signal.addEventListener('abort', withdraw, { once: true });
6528
+ }
6529
+ if (
6530
+ !registered.started &&
6531
+ registered.members.size === parallelState.regions.length
6532
+ ) {
6533
+ startCohort(parallelState, registered);
6534
+ void settleCohort(parallelState, registered);
6535
+ }
6536
+ return await member.settled.promise;
6537
+ }
6538
+
6539
+ async function settleCohort(
6540
+ parallelState: XStateParallelState,
6541
+ cohort: PendingCohort,
6542
+ ): Promise<void> {
6543
+ const members = parallelState.regions.map(
6544
+ ({ key }) => cohort.members.get(key)!,
6545
+ );
6546
+ const cancellation = new AbortController();
6547
+ const completionOrder: XStateCohortMember[] = [];
6548
+ const decided = new Map<
6549
+ XStateCohortMember,
6550
+ {
6551
+ readonly error: unknown;
6552
+ readonly cause: PlaybookFailureCause | undefined;
6553
+ readonly sequence: number;
6554
+ }
6555
+ >();
6556
+ let decisionSequence = 0;
6557
+ // DR-063 §2: each member failure is one decision, ordered as it was
6558
+ // decided, with the cause decided for exactly that value at that
6559
+ // moment — so a value a sibling later rethrows cannot rename it.
6560
+ const decide = (
6561
+ member: XStateCohortMember,
6562
+ error: unknown,
6563
+ cause: PlaybookFailureCause | undefined = failureCauses.causeOf(error),
6564
+ ): void => {
6565
+ if (decided.has(member)) return;
6566
+ decided.set(member, { error, cause, sequence: ++decisionSequence });
6567
+ };
6568
+ const isCancellation = (error: unknown): boolean =>
6569
+ cancellation.signal.aborted &&
6570
+ Object.is(error, cancellation.signal.reason) &&
6571
+ !activeAborts?.isAbortReason(error);
6572
+ const operationFor =
6573
+ (member: XStateCohortMember) =>
6574
+ async (
6575
+ context:
6576
+ | { readonly baseline?: PlaybookRepositoryReceipt['baseline'] }
6577
+ | undefined,
6578
+ ): Promise<PlayerResult> => {
6579
+ const callSignal = combineAbortSignals(
6580
+ member.signal,
6581
+ cancellation.signal,
6582
+ );
6583
+ try {
6584
+ if (activePlayerKeys.has(member.playerKey)) {
6585
+ await member.reportCollision(callSignal);
6586
+ }
6587
+ activePlayerKeys.add(member.playerKey);
6588
+ try {
6589
+ const result = await member.run(
6590
+ callSignal,
6591
+ cancellation.signal,
6592
+ effectAuthorizedPreExistingBlock(
6593
+ member.effectBoundary,
6594
+ context?.baseline,
6595
+ ),
6596
+ );
6597
+ if (result.status !== 'ok' && !cancellation.signal.aborted) {
6598
+ // DR-063 §2: the failure built for a non-`ok` member result
6599
+ // carries the cause decided for exactly that result, and its
6600
+ // siblings are cancelled with it.
6601
+ const cause =
6602
+ resultFailureCauses.get(result) ??
6603
+ runtimeDefectCause(
6604
+ `callPlayer status ${JSON.stringify(result.status)}`,
6605
+ );
6606
+ const failure = attachPlaybookFailureCause(
6607
+ markFsmResultFailure(
6608
+ new Error(
6609
+ result.error ??
6610
+ `captainBridge: callPlayer status "${result.status}"`,
6611
+ ),
6612
+ ),
6613
+ cause,
6614
+ );
6615
+ failureCauses.retain(failure, cause);
6616
+ decide(member, failure, cause);
6617
+ cancellation.abort(failure);
6618
+ }
6619
+ return result;
6620
+ } finally {
6621
+ activePlayerKeys.delete(member.playerKey);
6622
+ }
6623
+ } catch (error) {
6624
+ if (!isCancellation(error)) decide(member, error);
6625
+ if (!cancellation.signal.aborted) cancellation.abort(error);
6626
+ throw error;
6627
+ } finally {
6628
+ completionOrder.push(member);
6629
+ }
6630
+ };
6631
+ const completeEffectBoundary = (
6632
+ completion: XStateRepositoryCohortCompletion<PlayerResult>,
6633
+ ): Promise<XStateRepositoryCompletionEvidence> =>
6634
+ cohortCompletionQueue.add(async () => {
6635
+ const member = members.find(
6636
+ ({ roleId }) => roleId === completion.roleId,
6637
+ );
6638
+ if (
6639
+ member === undefined ||
6640
+ completion.boundary.boundaryId !== member.effectBoundary.boundaryId
6641
+ ) {
6642
+ throw new TypeError(
6643
+ `${label} repository cohort completed an undeclared member`,
6644
+ );
6645
+ }
6646
+ const evidence = await completionEvidenceFor(
6647
+ member.input,
6648
+ member.roleId,
6649
+ member.playerId,
6650
+ member.signal,
6651
+ undefined,
6652
+ )(completion);
6653
+ const settlement = governedSettlementsByBoundaryId.get(
6654
+ member.effectBoundary.boundaryId,
6655
+ );
6656
+ if (settlement?.status === 'unresolved') {
6657
+ decide(member, settlement.error);
6658
+ }
6659
+ return evidence;
6660
+ }) as Promise<XStateRepositoryCompletionEvidence>;
6661
+ const runSignal = activeSignal ?? members[0]!.signal;
6662
+ const invocationId = randomUUID();
6663
+ let settled: XStateRepositoryCohortResult<PlayerResult>;
6664
+ try {
6665
+ for (const member of members) member.signal.throwIfAborted();
6666
+ settled = await repositoryCapability!.runCohort!<PlayerResult>({
6667
+ signal: runSignal,
6668
+ invocationId,
6669
+ roleIds: members.map(({ roleId }) => roleId),
6670
+ dispositionsByRole: Object.fromEntries(
6671
+ members.map(({ roleId }) => [roleId, ['unchanged'] as const]),
6672
+ ),
6673
+ effectBoundaries: Object.fromEntries(
6674
+ members.map(({ roleId, effectBoundary }) => [
6675
+ roleId,
6676
+ effectBoundary,
6677
+ ]),
6678
+ ),
6679
+ operations: Object.fromEntries(
6680
+ members.map((member) => [member.roleId, operationFor(member)]),
6681
+ ),
6682
+ completeEffectBoundary,
6683
+ });
6684
+ if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
6685
+ throw new TypeError(
6686
+ `${label} repository cohort changed its invocation identity`,
6687
+ );
6688
+ }
6689
+ } catch (error) {
6690
+ for (const member of members) {
6691
+ governedSettlementsByBoundaryId.delete(
6692
+ member.effectBoundary.boundaryId,
6693
+ );
6694
+ governedCompletionEvidenceByBoundaryId.delete(
6695
+ member.effectBoundary.boundaryId,
6696
+ );
6697
+ refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
6698
+ }
6699
+ if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
6700
+ for (const member of members) member.settled.reject(error);
6701
+ return;
6702
+ }
6703
+ const releaseOrder = [
6704
+ ...completionOrder,
6705
+ ...members.filter((member) => !completionOrder.includes(member)),
6706
+ ];
6707
+ const acknowledged = new Map<XStateCohortMember, PlayerResult>();
6708
+ const failures = new Map<XStateCohortMember, unknown>();
6709
+ for (const member of releaseOrder) {
6710
+ const operation = isPlainObject(settled.operations)
6711
+ ? settled.operations[member.roleId]
6712
+ : undefined;
6713
+ try {
6714
+ const result = acknowledgeGovernedPlayerResult(
6715
+ {
6716
+ operation,
6717
+ receipt: isPlainObject(settled.receipts)
6718
+ ? settled.receipts[member.roleId]
6719
+ : undefined,
6720
+ effectLedger: settled.effectLedger,
6721
+ },
6722
+ member.effectBoundary.boundaryId,
6723
+ 'runCohort',
6724
+ );
6725
+ acknowledged.set(member, result);
6726
+ const settlement = governedPlayerSettlements.get(result);
6727
+ if (settlement?.status === 'unresolved') {
6728
+ failures.set(member, settlement.error);
6729
+ } else if (result.status !== 'ok') {
6730
+ failures.set(member, decided.get(member)?.error);
6731
+ }
6732
+ } catch (error) {
6733
+ failures.set(member, error);
6734
+ const operationReason =
6735
+ isPlainObject(operation) &&
6736
+ operation.status === 'rejected' &&
6737
+ Object.is(operation.reason, error);
6738
+ if (!operationReason) {
6739
+ // An acknowledgement the host could not prove is a control-plane
6740
+ // defect, exactly as on the exclusive path.
6741
+ if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
6742
+ decide(member, error);
6743
+ }
6744
+ } finally {
6745
+ governedSettlementsByBoundaryId.delete(
6746
+ member.effectBoundary.boundaryId,
6747
+ );
6748
+ governedCompletionEvidenceByBoundaryId.delete(
6749
+ member.effectBoundary.boundaryId,
6750
+ );
6751
+ }
6752
+ }
6753
+ if (failures.size === 0) {
6754
+ for (const member of releaseOrder) {
6755
+ member.settled.resolve(acknowledged.get(member)!);
6756
+ }
6757
+ return;
6758
+ }
6759
+ for (const result of acknowledged.values()) {
6760
+ governedPlayerSettlements.delete(result);
6761
+ }
6762
+ const first = [...decided.values()].sort(
6763
+ (left, right) => left.sequence - right.sequence,
6764
+ )[0];
6765
+ const failure =
6766
+ first?.error ??
6767
+ failures.get(releaseOrder.find((member) => failures.has(member))!) ??
6768
+ markFsmResultFailure(
6769
+ new Error(
6770
+ `${label} parallel state ${parallelState.stateId} cohort failed`,
6771
+ ),
6772
+ );
6773
+ const cause = first?.cause ?? failureCauses.causeOf(failure);
6774
+ if (cause !== undefined) {
6775
+ failureCauses.retain(failure, cause);
6776
+ attachPlaybookFailureCause(failure, cause);
6777
+ }
6778
+ for (const member of members) member.settled.reject(failure);
6779
+ }
6780
+
5675
6781
  const boundary: RuntimeBoundaryCalls = {
5676
6782
  async callPlayer(
5677
6783
  input,
@@ -5729,16 +6835,192 @@ export function createXStatePlaybookRuntime<
5729
6835
  };
5730
6836
 
5731
6837
  const playerKey = continuationKey(roleId, playerId);
5732
- if (activePlayerKeys.has(playerKey)) {
6838
+ const runTracedPlayerCall = async (
6839
+ resume: string | false = selectedResume,
6840
+ // DR-062 §4: composed from this call's own baseline observation,
6841
+ // so the prompt the trace records is the prompt the player is sent.
6842
+ preExistingBlock?: string,
6843
+ // DR-067 §1: a cohort member's call signal also carries its
6844
+ // cohort's sibling cancellation.
6845
+ callSignal: AbortSignal = signal,
6846
+ cohortCancellation?: AbortSignal,
6847
+ ): Promise<PlayerResult> => {
6848
+ const identity = callIdentity(resume);
6849
+ // DR-067 §1: a parallel machine classifies aborts per call, so a
6850
+ // trace sink rejecting with a sibling's cancellation is forgiven
6851
+ // for that call alone.
6852
+ const traceAborts =
6853
+ parallelProfile === undefined
6854
+ ? undefined
6855
+ : abortReasonClassifier(callSignal, activeAborts);
6856
+ const withPreExisting = (text: string): string =>
6857
+ preExistingBlock === undefined
6858
+ ? text
6859
+ : `${text}\n\n${preExistingBlock}`;
6860
+ const callPrompt = withPreExisting(
6861
+ resume === false ? freshPrompt : prompt,
6862
+ );
6863
+ const callFreshPrompt = withPreExisting(freshPrompt);
6864
+ await emitCallStarted(
6865
+ 'player.call.started',
6866
+ 'player.call.finished',
6867
+ { ...identity, prompt: callPrompt },
6868
+ position,
6869
+ callSignal,
6870
+ undefined,
6871
+ traceAborts,
6872
+ );
6873
+
6874
+ let rawResult: unknown;
6875
+ try {
6876
+ // An abort may land while the awaited started emission drains
6877
+ // (e.g. fired from the trace sink itself); the host call must
6878
+ // never start after abort, so settle the already-started pair
6879
+ // as `aborted` through the catch below.
6880
+ callSignal.throwIfAborted();
6881
+ rawResult = await requireHostPorts().callPlayer(
6882
+ roleId,
6883
+ callPrompt,
6884
+ callSignal,
6885
+ {
6886
+ resume,
6887
+ ...(callPrompt === callFreshPrompt
6888
+ ? {}
6889
+ : { freshPrompt: callFreshPrompt }),
6890
+ },
6891
+ );
6892
+ // A host promise is not required to honor cancellation. Do not
6893
+ // let a late result mutate continuity or publish a successful
6894
+ // finish.
6895
+ callSignal.throwIfAborted();
6896
+ } catch (error) {
6897
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
6898
+ try {
6899
+ await emitTrace(
6900
+ 'player.call.finished',
6901
+ {
6902
+ ...identity,
6903
+ status: isAbortFailure(error, callSignal)
6904
+ ? 'aborted'
6905
+ : 'error',
6906
+ error: normalizeError(error),
6907
+ },
6908
+ position,
6909
+ traceAborts,
6910
+ );
6911
+ } catch {
6912
+ // The original non-abort port rejection remains authoritative.
6913
+ }
6914
+ // A thrown port call carries no authoritative result, so the
6915
+ // prior token remains available for a later explicit resume.
6916
+ throw decidePlayerCallFailure(
6917
+ error,
6918
+ roleId,
6919
+ playerId,
6920
+ callSignal,
6921
+ cohortCancellation,
6922
+ );
6923
+ }
6924
+
6925
+ let result: PlayerResult;
6926
+ try {
6927
+ result = validatePlayerResult(rawResult);
6928
+ } catch (error) {
6929
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
6930
+ try {
6931
+ await emitTrace(
6932
+ 'player.call.finished',
6933
+ { ...identity, status: 'error', error: normalizeError(error) },
6934
+ position,
6935
+ traceAborts,
6936
+ );
6937
+ } catch {
6938
+ // The malformed host result remains authoritative.
6939
+ }
6940
+ throw decidePlayerCallFailure(
6941
+ error,
6942
+ roleId,
6943
+ playerId,
6944
+ callSignal,
6945
+ cohortCancellation,
6946
+ );
6947
+ }
6948
+
6949
+ try {
6950
+ updatePlayerResume(roleId, playerId, result);
6951
+ } catch (error) {
6952
+ if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
6953
+ try {
6954
+ await emitTrace(
6955
+ 'player.call.finished',
6956
+ { ...identity, status: 'error', error: normalizeError(error) },
6957
+ position,
6958
+ traceAborts,
6959
+ );
6960
+ } catch {
6961
+ // The continuation-store failure remains authoritative.
6962
+ }
6963
+ throw error;
6964
+ }
6965
+
6966
+ await emitTrace(
6967
+ 'player.call.finished',
6968
+ {
6969
+ ...identity,
6970
+ status: result.status,
6971
+ ...(result.finalText !== undefined
6972
+ ? { finalText: result.finalText }
6973
+ : {}),
6974
+ ...(result.error !== undefined
6975
+ ? { error: normalizeError(result.error) }
6976
+ : {}),
6977
+ ...(result.resumeToken !== undefined
6978
+ ? { resumeToken: result.resumeToken }
6979
+ : {}),
6980
+ },
6981
+ position,
6982
+ traceAborts,
6983
+ );
6984
+ // DR-063 §2: the player bridge builds the failure Error for a
6985
+ // non-`ok` result, so the cause is decided here, where the role,
6986
+ // the player, and the reported error are known, and a successful
6987
+ // result clears it so no later failure inherits it.
6988
+ if (result.status === 'ok') {
6989
+ pendingResultFailureCause = undefined;
6990
+ } else if (result.status === 'aborted') {
6991
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
6992
+ } else {
6993
+ pendingResultFailureCause = playerFailureCause({
6994
+ roleId,
6995
+ ...(playerId === undefined ? {} : { playerId }),
6996
+ error:
6997
+ result.error ??
6998
+ `callPlayer status ${JSON.stringify(result.status)}`,
6999
+ });
7000
+ }
7001
+ if (pendingResultFailureCause !== undefined) {
7002
+ resultFailureCauses.set(result, pendingResultFailureCause);
7003
+ }
7004
+ return result;
7005
+ };
7006
+ const reportPlayerKeyCollision = async (
7007
+ callSignal: AbortSignal,
7008
+ ): Promise<never> => {
5733
7009
  const error = new Error(
5734
7010
  `simultaneous calls to player key ${playerKey} are not allowed`,
5735
7011
  );
7012
+ const traceAborts =
7013
+ parallelProfile === undefined
7014
+ ? undefined
7015
+ : abortReasonClassifier(callSignal, activeAborts);
5736
7016
  await emitCallStarted(
5737
7017
  'player.call.started',
5738
7018
  'player.call.finished',
5739
7019
  { ...callIdentity(selectedResume), prompt },
5740
7020
  position,
5741
- signal,
7021
+ callSignal,
7022
+ undefined,
7023
+ traceAborts,
5742
7024
  );
5743
7025
  await emitTrace(
5744
7026
  'player.call.finished',
@@ -5748,147 +7030,63 @@ export function createXStatePlaybookRuntime<
5748
7030
  error: normalizeError(error),
5749
7031
  },
5750
7032
  position,
7033
+ traceAborts,
5751
7034
  );
5752
7035
  throw error;
5753
- }
5754
- activePlayerKeys.add(playerKey);
7036
+ };
5755
7037
 
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,
7038
+ // DR-067 §1: the working leaves of one parallel state entered
7039
+ // together run as one all-`unchanged` repository cohort, at most once
7040
+ // per public boundary; a corrective re-ask and a single region
7041
+ // resuming after its Boss reply take the exclusive path below.
7042
+ const cohortState =
7043
+ deferredContinuation === undefined
7044
+ ? cohortParallelStateFor(input.stateId)
7045
+ : undefined;
7046
+ if (cohortState !== undefined) {
7047
+ const effectBoundary = governedBoundarySeed(
7048
+ input,
7049
+ roleId,
7050
+ callId,
7051
+ turnId,
7052
+ );
7053
+ if (
7054
+ effectBoundary === undefined ||
7055
+ repositoryCapability?.runCohort === undefined
7056
+ ) {
7057
+ throw new Error(
7058
+ `${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`,
5778
7059
  );
7060
+ }
7061
+ activeGovernedBoundarySeen = true;
7062
+ return await joinCohort(cohortState, {
7063
+ region: cohortState.regions.find(
7064
+ ({ workingStateId }) => workingStateId === input.stateId,
7065
+ )!,
7066
+ input,
7067
+ roleId,
7068
+ playerId,
7069
+ playerKey,
7070
+ signal,
7071
+ effectBoundary,
7072
+ run: (callSignal, cohortCancellation, preExistingBlock) =>
7073
+ runTracedPlayerCall(
7074
+ selectedResume,
7075
+ preExistingBlock,
7076
+ callSignal,
7077
+ cohortCancellation,
7078
+ ),
7079
+ reportCollision: reportPlayerKeyCollision,
7080
+ settled: deferredValue<PlayerResult>(),
7081
+ });
7082
+ }
5779
7083
 
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
- };
7084
+ if (activePlayerKeys.has(playerKey)) {
7085
+ await reportPlayerKeyCollision(signal);
7086
+ }
7087
+ activePlayerKeys.add(playerKey);
5891
7088
 
7089
+ try {
5892
7090
  const effectBoundary = governedBoundarySeed(
5893
7091
  input,
5894
7092
  roleId,
@@ -5993,10 +7191,13 @@ export function createXStatePlaybookRuntime<
5993
7191
  }
5994
7192
  },
5995
7193
 
5996
- markPlayerResultFailure(error): Error {
5997
- if (pendingResultFailureCause === undefined) return error;
5998
- failureCauses.retain(error, pendingResultFailureCause);
5999
- return attachPlaybookFailureCause(error, pendingResultFailureCause);
7194
+ markPlayerResultFailure(error, result): Error {
7195
+ const cause =
7196
+ (result === undefined ? undefined : resultFailureCauses.get(result)) ??
7197
+ pendingResultFailureCause;
7198
+ if (cause === undefined) return error;
7199
+ failureCauses.retain(error, cause);
7200
+ return attachPlaybookFailureCause(error, cause);
6000
7201
  },
6001
7202
 
6002
7203
  takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
@@ -6237,6 +7438,16 @@ export function createXStatePlaybookRuntime<
6237
7438
  }) as Promise<CaptainResult>;
6238
7439
  },
6239
7440
  };
7441
+ if (parallelProfile !== undefined) {
7442
+ // DR-067 §1: a parallel machine's boundary drain waits for every player
7443
+ // and judge call it started.
7444
+ const untrackedCallPlayer = boundary.callPlayer;
7445
+ const untrackedCallJudge = boundary.callJudge;
7446
+ boundary.callPlayer = (input, roleId, freshPrompt, signal) =>
7447
+ trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
7448
+ boundary.callJudge = (purpose, stateId, prompt, signal) =>
7449
+ trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
7450
+ }
6240
7451
 
6241
7452
  function playerActor(
6242
7453
  ports: PlaybookPorts,
@@ -6563,8 +7774,12 @@ export function createXStatePlaybookRuntime<
6563
7774
  const nestedBridge = createNestedPlaybookBridge({
6564
7775
  nextCallId: () => `playbook-${++playbookCallSequence}`,
6565
7776
  getBoundarySignal: () => activeSignal,
6566
- callPlaybook: (request, signal) =>
6567
- requireHostPorts().callPlaybook(request, signal),
7777
+ callPlaybook: (request, signal) => {
7778
+ const call = requireHostPorts().callPlaybook(request, signal);
7779
+ return parallelProfile === undefined
7780
+ ? call
7781
+ : trackBoundaryCall(Promise.resolve(call));
7782
+ },
6568
7783
  emitStarted: async (event, aborts) => {
6569
7784
  playbookCallTurnIds.set(event.callId, activeTurnId);
6570
7785
  if (hasGovernedPlayerStates) {
@@ -6615,7 +7830,8 @@ export function createXStatePlaybookRuntime<
6615
7830
  activeAborts = aborts ?? abortReasonClassifier(signal);
6616
7831
  },
6617
7832
  bindActorSettlement: (aborts) => {
6618
- actorSettlementAborts = aborts;
7833
+ if (parallelProfile === undefined) actorSettlementAborts.length = 0;
7834
+ actorSettlementAborts.push(aborts);
6619
7835
  },
6620
7836
  onControlPlaneError: (error, aborts) => {
6621
7837
  // The shared bridge classifies before reporting against its own
@@ -6662,14 +7878,44 @@ export function createXStatePlaybookRuntime<
6662
7878
  };
6663
7879
  }
6664
7880
 
7881
+ // DR-063 §2: the failure that parks the machine is the error its parking
7882
+ // transition carried, but a compiled machine keeps a JSON-safe record of
7883
+ // it as `lastError` (gears2fsm requires JSON-safe context), so neither
7884
+ // that value's identity nor its marker reaches the failed state. The cause
7885
+ // decided for the carried error is retained under the value the machine
7886
+ // kept, so every surface of this turn and of each later one publishes the
7887
+ // runtime's own decision for as long as that failure stands. A kept value
7888
+ // that already answers keeps its cause.
7889
+ function bindParkedFailureCause(
7890
+ context: Record<string, unknown>,
7891
+ event: unknown,
7892
+ ): void {
7893
+ const kept = context.lastError;
7894
+ if (kept === undefined || kept === null) return;
7895
+ if (failureCauses.causeOf(kept) !== undefined) return;
7896
+ const decided = failureCauses.causeOf(actorErrorOf(event));
7897
+ if (decided !== undefined) failureCauses.retain(kept, decided);
7898
+ }
7899
+
6665
7900
  // 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`.
7901
+ // a `player-failed` failure named by role; an abort is `aborted`. A
7902
+ // cancellation the runtime itself issued to a cohort member because its
7903
+ // sibling failed is not this player's failure and decides nothing, so the
7904
+ // originator's cause stands (DR-067 §1, playbook-55).
6667
7905
  function decidePlayerCallFailure(
6668
7906
  error: unknown,
6669
7907
  roleId: string,
6670
7908
  playerId: string | undefined,
6671
7909
  signal: AbortSignal,
7910
+ cohortCancellation?: AbortSignal,
6672
7911
  ): unknown {
7912
+ if (
7913
+ cohortCancellation?.aborted === true &&
7914
+ Object.is(error, cohortCancellation.reason) &&
7915
+ !activeAborts?.isAbortReason(error)
7916
+ ) {
7917
+ return error;
7918
+ }
6673
7919
  const cause = isAbortFailure(error, signal)
6674
7920
  ? ABORTED_FAILURE_CAUSE
6675
7921
  : playerFailureCause({
@@ -6728,12 +7974,21 @@ export function createXStatePlaybookRuntime<
6728
7974
  previousState: previousState ?? null,
6729
7975
  state,
6730
7976
  };
6731
- const pendingBossQuestion = pendingBossQuestionForState(state, context);
6732
- if (
6733
- pendingBossQuestion !== undefined &&
6734
- !hasUnresolvedReconciliation()
6735
- ) {
6736
- payload.pendingBossQuestion = pendingBossQuestion;
7977
+ if (parallelProfile === undefined) {
7978
+ const pendingBossQuestion = pendingBossQuestionForState(state, context);
7979
+ if (
7980
+ pendingBossQuestion !== undefined &&
7981
+ !hasUnresolvedReconciliation()
7982
+ ) {
7983
+ payload.pendingBossQuestion = pendingBossQuestion;
7984
+ }
7985
+ } else if (!hasUnresolvedReconciliation()) {
7986
+ // DR-067 §1: a parallel machine's telemetry carries every question
7987
+ // pending in an active wait, in the plural.
7988
+ const pendingBossQuestions = pendingBossQuestionsFor(state, context);
7989
+ if (pendingBossQuestions.length > 0) {
7990
+ payload.pendingBossQuestions = pendingBossQuestions;
7991
+ }
6737
7992
  }
6738
7993
  if (state.stateId === 'failed') {
6739
7994
  const lastError = failedStateError(context);
@@ -6768,7 +8023,7 @@ export function createXStatePlaybookRuntime<
6768
8023
  message,
6769
8024
  ...(data === undefined ? {} : { data }),
6770
8025
  state,
6771
- ...stateIdentity(state.stateId),
8026
+ ...stateIdentity(statusTraceStateId(state)),
6772
8027
  },
6773
8028
  position,
6774
8029
  ),
@@ -6818,8 +8073,7 @@ export function createXStatePlaybookRuntime<
6818
8073
  function consumeActorSettlementAborts(
6819
8074
  forSnapshot = false,
6820
8075
  ): AbortReasonClassifier | undefined {
6821
- const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
6822
- actorSettlementAborts = undefined;
8076
+ const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
6823
8077
  actorSettlementErrorAborts = undefined;
6824
8078
  if (forSnapshot && aborts !== undefined) {
6825
8079
  // XState can report an errored root through both its inspection
@@ -6891,7 +8145,14 @@ export function createXStatePlaybookRuntime<
6891
8145
  try {
6892
8146
  const snap = inspectionEvent.snapshot;
6893
8147
  const state = normalizePlaybookSnapshot(snap);
6894
- if (state.stateId === undefined) {
8148
+ if (
8149
+ state.stateId === undefined &&
8150
+ !(
8151
+ parallelProfile?.states.some(({ stateId }) =>
8152
+ state.activeStateIds.includes(stateId),
8153
+ ) ?? false
8154
+ )
8155
+ ) {
6895
8156
  throw new Error(
6896
8157
  `${label} root snapshot must expose exactly one playbook state id`,
6897
8158
  );
@@ -6921,6 +8182,9 @@ export function createXStatePlaybookRuntime<
6921
8182
  }
6922
8183
  const context = ((snap as { context?: unknown }).context ??
6923
8184
  {}) as Record<string, unknown>;
8185
+ if (state.stateId === 'failed') {
8186
+ bindParkedFailureCause(context, inspectionEvent.event);
8187
+ }
6924
8188
  if (
6925
8189
  state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
6926
8190
  deferredReconciliationOperationId !== undefined
@@ -6942,7 +8206,9 @@ export function createXStatePlaybookRuntime<
6942
8206
  context,
6943
8207
  );
6944
8208
  const stateStatuses = completeFailedStatuses(
6945
- statusesForState(state, context, inspectionEvent.event),
8209
+ bossRelevantStateIds !== undefined && usesDefaultStatuses
8210
+ ? parallelStatusesFor(previousState, state, context)
8211
+ : statusesForState(state, context, inspectionEvent.event),
6946
8212
  state,
6947
8213
  context,
6948
8214
  );
@@ -7149,13 +8415,17 @@ export function createXStatePlaybookRuntime<
7149
8415
  emissionQueue.clear();
7150
8416
  judgeQueue.clear();
7151
8417
  appliedReceipts.clear();
8418
+ pendingCohorts.clear();
8419
+ consumedCohortBoundaries.clear();
8420
+ activeBoundaryCalls.clear();
8421
+ cohortCompletionQueue.clear();
7152
8422
  actor = undefined;
7153
8423
  session = undefined;
7154
8424
  savedPorts = undefined;
7155
8425
  runtimePorts = undefined;
7156
8426
  activeSignal = undefined;
7157
8427
  activeAborts = undefined;
7158
- actorSettlementAborts = undefined;
8428
+ actorSettlementAborts.length = 0;
7159
8429
  actorSettlementErrorAborts = undefined;
7160
8430
  activeAbortEmission = undefined;
7161
8431
  activeTurnId = undefined;
@@ -7245,12 +8515,12 @@ export function createXStatePlaybookRuntime<
7245
8515
  // context member above sources. A candidate whose event the live
7246
8516
  // snapshot does not accept — or whose payload the runtime can source
7247
8517
  // from neither — is excluded rather than completed with invented text.
8518
+ // The caller has already applied the PBRT-71 replay fence.
7248
8519
  function retryActionFor(
7249
8520
  snapshot: unknown,
7250
8521
  stateId: string | undefined,
7251
8522
  ): DerivedControlAction | undefined {
7252
8523
  if (stateId !== 'failed') return undefined;
7253
- if (!failedAttemptAllowsReplay()) return undefined;
7254
8524
  const retryEvent = retryEventFrom(snapshot);
7255
8525
  if (retryEvent === undefined) return undefined;
7256
8526
  if (!snapshotCan(snapshot, retryEvent)) return undefined;
@@ -7380,6 +8650,15 @@ export function createXStatePlaybookRuntime<
7380
8650
  );
7381
8651
  return derived;
7382
8652
  }
8653
+ // PBRT-71 / DR-040 §4: out of the failure state, a jump restarts the
8654
+ // failed attempt's work exactly as the retry does, so one fence gates
8655
+ // both. Unless every governed boundary of that attempt carries a
8656
+ // complete `unchanged` receipt, neither is advertised — and `apply`,
8657
+ // which revalidates against this list, accepts neither — so no
8658
+ // control can replay a player call whose effect may already exist.
8659
+ if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
8660
+ return derived;
8661
+ }
7383
8662
  const retry = retryActionFor(snapshot, state.stateId);
7384
8663
  if (retry !== undefined) derived.push(retry);
7385
8664
  // Jump entries: resumable targets whose explicit-state-jump event the
@@ -7447,9 +8726,12 @@ export function createXStatePlaybookRuntime<
7447
8726
  // state whose source declares no description publishes none: an id is
7448
8727
  // never promoted into a description by default.
7449
8728
  function stateDescriptionFor(state: PlaybookState): string | undefined {
8729
+ // DR-067 §1: inside a parallel state, the meaning published is the
8730
+ // root state's — the parallel parent's own description.
7450
8731
  const keys = [
7451
8732
  ...(state.stateId === undefined ? [] : [state.stateId]),
7452
8733
  ...(typeof state.value === 'string' ? [state.value] : []),
8734
+ ...(isPlainObject(state.value) ? Object.keys(state.value) : []),
7453
8735
  ...state.activeStateIds,
7454
8736
  ];
7455
8737
  for (const key of keys) {
@@ -7820,7 +9102,7 @@ export function createXStatePlaybookRuntime<
7820
9102
  ?.context ?? {}) as Record<string, unknown>;
7821
9103
  const actual =
7822
9104
  state?.stateId === BOSS_REPLY_WAIT_STATE_ID
7823
- ? pendingBossQuestionFromContext(context)
9105
+ ? singlePendingBossQuestion(state, context)
7824
9106
  : undefined;
7825
9107
  if (!isDeepStrictEqual(actual, expected)) {
7826
9108
  throw new Error(
@@ -8177,7 +9459,7 @@ export function createXStatePlaybookRuntime<
8177
9459
  const state = normalizePlaybookSnapshot(snapshot);
8178
9460
  const context = ((snapshot as { context?: unknown }).context ??
8179
9461
  {}) as Record<string, unknown>;
8180
- const pending = pendingBossQuestionForState(state, context);
9462
+ const pending = singlePendingBossQuestion(state, context);
8181
9463
  if (
8182
9464
  pending === undefined ||
8183
9465
  currentBoundDeferredOperation(pending)?.operationId !== operationId
@@ -8298,10 +9580,9 @@ export function createXStatePlaybookRuntime<
8298
9580
  refreshRetainedEffectReconciliation(effectLedgerMirror);
8299
9581
  syncDeferredReconciliationOverlay();
8300
9582
  refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
8301
- const pending =
8302
- !hasUnresolvedReconciliation()
8303
- ? pendingBossQuestionForState(state, context ?? {})
8304
- : undefined;
9583
+ const pendingQuestions = !hasUnresolvedReconciliation()
9584
+ ? pendingBossQuestionsFor(state, context ?? {})
9585
+ : [];
8305
9586
  const failedEffectAttempt =
8306
9587
  hasGovernedPlayerStates &&
8307
9588
  state.stateId === 'failed' &&
@@ -8327,17 +9608,12 @@ export function createXStatePlaybookRuntime<
8327
9608
  : {}),
8328
9609
  },
8329
9610
  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
- ],
9611
+ pendingBossQuestions: pendingQuestions.map((pending) => ({
9612
+ questionId: pending.questionId,
9613
+ asker: pending.asker,
9614
+ question: pending.question,
9615
+ sourceItem: pending.sourceItem,
9616
+ })),
8341
9617
  effectLedger: effectLedgerMirror,
8342
9618
  ...(retainedEffectSourceSessionId === undefined
8343
9619
  ? {}
@@ -8365,17 +9641,23 @@ export function createXStatePlaybookRuntime<
8365
9641
  },
8366
9642
 
8367
9643
  // 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
9644
+ // engagement identity and counter lineage, exposed separately so a
9645
+ // capability-less runtime can omit it. Runtime-visible preflight
9646
+ // mismatches reject before effects; after preflight the new
8371
9647
  // 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
- },
9648
+ // DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
9649
+ // machine that declares a parallel state omits the member.
9650
+ ...(parallelProfile === undefined
9651
+ ? {
9652
+ async adopt(
9653
+ nextSession: PlaybookSession,
9654
+ snapshot: PlaybookRuntimeSnapshot,
9655
+ context: PlaybookAdoptionContext,
9656
+ ): Promise<void> {
9657
+ await rehydrateSnapshot('adopt', nextSession, snapshot, context);
9658
+ },
9659
+ }
9660
+ : {}),
8379
9661
 
8380
9662
  // DR-029 / PBRT-52: side-effect-free control view over the live
8381
9663
  // snapshot, valid at parked quiescence outside an active boundary.
@@ -8403,10 +9685,9 @@ export function createXStatePlaybookRuntime<
8403
9685
  const state = currentState();
8404
9686
  const context = ((snapshot as { context?: unknown }).context ??
8405
9687
  {}) as Record<string, unknown>;
8406
- const pending =
8407
- !hasUnresolvedReconciliation()
8408
- ? pendingBossQuestionForState(state, context)
8409
- : undefined;
9688
+ const pendingQuestions = !hasUnresolvedReconciliation()
9689
+ ? pendingBossQuestionsFor(state, context)
9690
+ : [];
8410
9691
  const lastError =
8411
9692
  state.stateId === 'failed'
8412
9693
  ? failedStateError(context)
@@ -8422,17 +9703,12 @@ export function createXStatePlaybookRuntime<
8422
9703
  ...(projectedContext !== undefined
8423
9704
  ? { context: projectedContext }
8424
9705
  : {}),
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
- ],
9706
+ pendingQuestions: pendingQuestions.map((pending) => ({
9707
+ questionId: pending.questionId,
9708
+ asker: pending.asker,
9709
+ question: pending.question,
9710
+ sourceItem: pending.sourceItem,
9711
+ })),
8436
9712
  ...(lastError !== undefined ? { lastError } : {}),
8437
9713
  actions: deriveControlActions(snapshot).map(({ action }) => action),
8438
9714
  });
@@ -8501,6 +9777,7 @@ export function createXStatePlaybookRuntime<
8501
9777
  const callId = `apply-${++applyCallSequence}`;
8502
9778
  const position: TracePosition = { turnId, callId };
8503
9779
  activeTurnId = turnId;
9780
+ publicBoundarySequence += 1;
8504
9781
  beginAutomaticReplayBoundary();
8505
9782
  activeSignal = signal;
8506
9783
  activeAborts = abortReasonClassifier(signal);
@@ -8701,6 +9978,7 @@ export function createXStatePlaybookRuntime<
8701
9978
  if (receipt !== undefined && receipt.disposition !== 'rejected') {
8702
9979
  appliedReceipts.set(key, receipt);
8703
9980
  }
9981
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
8704
9982
  try {
8705
9983
  await drainEmissions();
8706
9984
  } catch (error) {
@@ -8804,6 +10082,7 @@ export function createXStatePlaybookRuntime<
8804
10082
  refreshRetainedEffectFenceFromHost();
8805
10083
  const turnId = ++turnSequence;
8806
10084
  activeTurnId = turnId;
10085
+ publicBoundarySequence += 1;
8807
10086
  beginAutomaticReplayBoundary();
8808
10087
  activeSignal = signal;
8809
10088
  activeAborts = abortReasonClassifier(signal);
@@ -8845,7 +10124,7 @@ export function createXStatePlaybookRuntime<
8845
10124
  const stateId = normalizePlaybookSnapshot(snapshot).stateId;
8846
10125
  const snapshotContext = ((snapshot as { context?: unknown })
8847
10126
  .context ?? {}) as Record<string, unknown>;
8848
- deferredPending = pendingBossQuestionForState(
10127
+ deferredPending = singlePendingBossQuestion(
8849
10128
  normalizePlaybookSnapshot(snapshot),
8850
10129
  snapshotContext,
8851
10130
  );
@@ -9012,6 +10291,7 @@ export function createXStatePlaybookRuntime<
9012
10291
  operationError = error;
9013
10292
  }
9014
10293
 
10294
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
9015
10295
  let drainError: unknown;
9016
10296
  try {
9017
10297
  await drainEmissions();
@@ -9125,6 +10405,7 @@ export function createXStatePlaybookRuntime<
9125
10405
  return runResultFor('no-action');
9126
10406
  }
9127
10407
  activeTurnId = playbookCallTurnIds.get(input.callId);
10408
+ publicBoundarySequence += 1;
9128
10409
  bindAutomaticReplayBoundary(
9129
10410
  playbookCallEffectPrefixes.has(input.callId)
9130
10411
  ? playbookCallEffectPrefixes.get(input.callId)
@@ -9169,6 +10450,7 @@ export function createXStatePlaybookRuntime<
9169
10450
  error: normalizeError(input.signal.reason),
9170
10451
  };
9171
10452
  }
10453
+ if (parallelProfile !== undefined) await drainBoundaryCalls();
9172
10454
  let drainError: unknown;
9173
10455
  try {
9174
10456
  await drainEmissions();
@@ -9286,10 +10568,14 @@ export function createXStatePlaybookRuntime<
9286
10568
  emissionQueue.clear();
9287
10569
  judgeQueue.clear();
9288
10570
  appliedReceipts.clear();
10571
+ pendingCohorts.clear();
10572
+ consumedCohortBoundaries.clear();
10573
+ activeBoundaryCalls.clear();
10574
+ cohortCompletionQueue.clear();
9289
10575
  actor = undefined;
9290
10576
  activeSignal = undefined;
9291
10577
  activeAborts = undefined;
9292
- actorSettlementAborts = undefined;
10578
+ actorSettlementAborts.length = 0;
9293
10579
  actorSettlementErrorAborts = undefined;
9294
10580
  activeAbortEmission = undefined;
9295
10581
  activeTurnId = undefined;