@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
@@ -117,8 +117,8 @@ export function playerFailureCause(input) {
117
117
  /**
118
118
  * DR-063 §1: the cause of an unresolved governed settlement. A reconciled
119
119
  * mismatch supplies its own receipt-read cause; the reasons a runtime owns map
120
- * to adjudication, abort, and runtime-defect codes. Shared with DECIDE's
121
- * bespoke runtime, which decides the same reasons.
120
+ * to adjudication, abort, and runtime-defect codes. Exported so linked
121
+ * machinery of an artifact's own decides the same reasons.
122
122
  */
123
123
  export function governedSettlementCause(reason, error, aborted, supplied) {
124
124
  if (supplied !== undefined)
@@ -157,6 +157,24 @@ export function createFailureCauseRetention() {
157
157
  },
158
158
  };
159
159
  }
160
+ /**
161
+ * DR-063 §2: the value an invoked actor's rejection carried into the machine,
162
+ * read from the XState error event that delivered it, or `undefined` for any
163
+ * other event.
164
+ */
165
+ function actorErrorOf(event) {
166
+ if (typeof event !== 'object' || event === null)
167
+ return undefined;
168
+ try {
169
+ const { type, error } = event;
170
+ return typeof type === 'string' && type.startsWith('xstate.error.actor.')
171
+ ? error
172
+ : undefined;
173
+ }
174
+ catch {
175
+ return undefined;
176
+ }
177
+ }
160
178
  // ---------------------------------------------------------------------------
161
179
  // DR-028: both call boundaries treat an `ok` result whose `finalText` is
162
180
  // missing, empty, or whitespace-only under one empty predicate, and that
@@ -242,7 +260,7 @@ function configuredOptionsFromFactoryInput(value, label) {
242
260
  effectLedger: effectLedger,
243
261
  };
244
262
  }
245
- function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
263
+ function repositoryCapabilityFromHostCapabilities(hostCapabilities, label, requireCohort = false) {
246
264
  const descriptor = hostCapabilities === undefined
247
265
  ? undefined
248
266
  : Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
@@ -256,8 +274,13 @@ function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
256
274
  Array.isArray(repository) ||
257
275
  typeof repository.runExclusive !==
258
276
  'function' ||
259
- typeof repository.runDeferred !== 'function') {
260
- throw new TypeError(`${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`);
277
+ typeof repository.runDeferred !==
278
+ 'function' ||
279
+ (requireCohort &&
280
+ typeof repository.runCohort !== 'function')) {
281
+ throw new TypeError(requireCohort
282
+ ? `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive, runCohort, and runDeferred`
283
+ : `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`);
261
284
  }
262
285
  return repository;
263
286
  }
@@ -448,7 +471,7 @@ export function normalizeErrorFull(err) {
448
471
  // slc/link.md §Abort: cancellation is causal identity with the applicable
449
472
  // signal's reason — never an `AbortError` name, never bare signal state. A
450
473
  // distinct failure observed while the signal is aborted stays a non-abort
451
- // control error and takes precedence (mirrors DECIDE's bespoke reference).
474
+ // control error and takes precedence.
452
475
  function isAbortFailure(error, signal) {
453
476
  return signal.aborted && Object.is(error, signal.reason);
454
477
  }
@@ -479,7 +502,32 @@ function pendingBossQuestionForState(state, context) {
479
502
  }
480
503
  /** Read the FSM context's single pending Boss question, when well-formed. */
481
504
  export function pendingBossQuestionFromContext(context) {
482
- const pending = context.pendingBossQuestion;
505
+ return pendingBossQuestionRecord(context.pendingBossQuestion);
506
+ }
507
+ /**
508
+ * DR-067 §1: the keyed form of gears2fsm's Boss-reply suspension. Each
509
+ * well-formed `context.pendingBossQuestions[stateId]` record whose own
510
+ * `questionId` equals its key counts only while a wait state that resumes
511
+ * its `resumeStateId` is active, so an answered question the context
512
+ * retains for the resumed prompt never reads as pending. Ordered by
513
+ * question id.
514
+ */
515
+ function keyedPendingBossQuestions(context, resumesFromActiveWait) {
516
+ const keyed = context.pendingBossQuestions;
517
+ if (!isPlainObject(keyed))
518
+ return [];
519
+ const pending = [];
520
+ for (const [key, value] of Object.entries(keyed)) {
521
+ const record = pendingBossQuestionRecord(value);
522
+ if (record === undefined || record.questionId !== key)
523
+ continue;
524
+ if (!resumesFromActiveWait(record.resumeStateId))
525
+ continue;
526
+ pending.push(record);
527
+ }
528
+ return pending.sort((left, right) => left.questionId.localeCompare(right.questionId));
529
+ }
530
+ function pendingBossQuestionRecord(pending) {
483
531
  if (pending === undefined ||
484
532
  pending === null ||
485
533
  typeof pending !== 'object') {
@@ -851,8 +899,8 @@ export function defaultBuildJudgePrompt(input, finalText) {
851
899
  * so the judge omits it. Rendering the clause verbatim asked the judge for
852
900
  * `question`, `planningResult`, or `evaluatedRevision`, which the reconciler
853
901
  * rejects as a structural error, spending the single correction on a
854
- * self-inflicted defect. Exported so a bespoke linked runtime (DECIDE's
855
- * parallel machinery) renders the identical contract instead of restating it.
902
+ * self-inflicted defect. Exported so linked machinery of an artifact's own
903
+ * renders the identical contract instead of restating it.
856
904
  */
857
905
  export function renderGovernedOutcomeContract(guard, description, outcome) {
858
906
  const { meaning, clause } = splitOutputClause(description);
@@ -1040,7 +1088,7 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
1040
1088
  }
1041
1089
  if (result.status !== 'ok') {
1042
1090
  const failure = new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
1043
- throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
1091
+ throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
1044
1092
  }
1045
1093
  const finalText = result.finalText ?? '';
1046
1094
  if (isEmptyFinalText(finalText)) {
@@ -1220,20 +1268,293 @@ function transitionTargets(transition) {
1220
1268
  }
1221
1269
  return targets;
1222
1270
  }
1223
- /** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
1271
+ function transitionArmTargets(transition) {
1272
+ const arms = Array.isArray(transition) ? transition : [transition];
1273
+ const targets = [];
1274
+ for (const arm of arms) {
1275
+ if (typeof arm === 'string') {
1276
+ targets.push(arm);
1277
+ }
1278
+ else if (isPlainObject(arm) && typeof arm.target === 'string') {
1279
+ targets.push(arm.target);
1280
+ }
1281
+ }
1282
+ return targets;
1283
+ }
1284
+ function playbookStateIdOf(stateDef) {
1285
+ if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
1286
+ return undefined;
1287
+ }
1288
+ const playbook = stateDef.meta.playbook;
1289
+ return isPlainObject(playbook) && typeof playbook.stateId === 'string'
1290
+ ? playbook.stateId
1291
+ : undefined;
1292
+ }
1293
+ function stateTagsOf(stateDef) {
1294
+ const tags = stateDef.tags;
1295
+ if (typeof tags === 'string')
1296
+ return [tags];
1297
+ return Array.isArray(tags)
1298
+ ? tags.filter((tag) => typeof tag === 'string')
1299
+ : [];
1300
+ }
1301
+ /** Every explicit node `id` in the config, mapped to its playbook state id. */
1302
+ function playbookStateIdsByNodeId(config) {
1303
+ const byNodeId = new Map();
1304
+ const visit = (stateDef) => {
1305
+ if (!isPlainObject(stateDef))
1306
+ return;
1307
+ const stateId = playbookStateIdOf(stateDef);
1308
+ if (typeof stateDef.id === 'string' && stateId !== undefined) {
1309
+ byNodeId.set(stateDef.id, stateId);
1310
+ }
1311
+ if (isPlainObject(stateDef.states)) {
1312
+ for (const child of Object.values(stateDef.states))
1313
+ visit(child);
1314
+ }
1315
+ };
1316
+ if (isPlainObject(config) && isPlainObject(config.states)) {
1317
+ for (const child of Object.values(config.states))
1318
+ visit(child);
1319
+ }
1320
+ return byNodeId;
1321
+ }
1322
+ /**
1323
+ * One transition target written inside a parallel region, resolved to the
1324
+ * playbook state id it names: an `#id` reference through the node ids, and a
1325
+ * bare sibling key through the region's own leaves.
1326
+ */
1327
+ function resolveRegionTarget(target, regionStates, byNodeId) {
1328
+ if (target.startsWith('#')) {
1329
+ const nodeId = target.slice(1);
1330
+ return byNodeId.get(nodeId) ?? nodeId;
1331
+ }
1332
+ if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
1333
+ return playbookStateIdOf(regionStates[target]) ?? target;
1334
+ }
1335
+ return target;
1336
+ }
1337
+ /**
1338
+ * Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
1339
+ * the parallel shape gears2fsm compiles (DR-067) — the targets of each
1340
+ * region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
1341
+ */
1224
1342
  export function resumableStateIdsFromMachine(machine) {
1225
1343
  const config = machine.config;
1226
1344
  if (!isPlainObject(config) || !isPlainObject(config.states)) {
1227
1345
  return new Set();
1228
1346
  }
1347
+ const resumable = new Set();
1229
1348
  const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
1230
- if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
1231
- return new Set();
1349
+ if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
1350
+ const bossReply = awaitState.on.BOSS_REPLY;
1351
+ if (bossReply !== undefined) {
1352
+ for (const target of transitionTargets(bossReply))
1353
+ resumable.add(target);
1354
+ }
1232
1355
  }
1233
- const bossReply = awaitState.on.BOSS_REPLY;
1234
- if (bossReply === undefined)
1235
- return new Set();
1236
- return new Set(transitionTargets(bossReply));
1356
+ const byNodeId = playbookStateIdsByNodeId(config);
1357
+ for (const stateDef of Object.values(config.states)) {
1358
+ if (!isPlainObject(stateDef) ||
1359
+ stateDef.type !== 'parallel' ||
1360
+ !isPlainObject(stateDef.states)) {
1361
+ continue;
1362
+ }
1363
+ for (const region of Object.values(stateDef.states)) {
1364
+ if (!isPlainObject(region) || !isPlainObject(region.states))
1365
+ continue;
1366
+ const regionStates = region.states;
1367
+ for (const leaf of Object.values(regionStates)) {
1368
+ if (!isPlainObject(leaf) || !isPlainObject(leaf.on))
1369
+ continue;
1370
+ const bossReply = leaf.on.BOSS_REPLY;
1371
+ if (bossReply === undefined)
1372
+ continue;
1373
+ for (const target of transitionArmTargets(bossReply)) {
1374
+ resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
1375
+ }
1376
+ }
1377
+ }
1378
+ }
1379
+ return resumable;
1380
+ }
1381
+ function malformedParallelState(label, key, reason) {
1382
+ return new Error(`${label} parallel state ${key} is not the compiled parallel shape: ${reason}`);
1383
+ }
1384
+ /**
1385
+ * DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
1386
+ * `type: 'parallel'` states of the one shape gears2fsm compiles — each
1387
+ * region a compound state with exactly one player-invoking `playbook.busy`
1388
+ * working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
1389
+ * arms resume that working leaf, and exactly one final leaf, every leaf
1390
+ * carrying a stable `meta.playbook.stateId` distinct from every other
1391
+ * state's, and the parent's `onDone` the join. Any other compound state
1392
+ * keeps the flat-domain rejection. Returns `undefined` for a flat machine.
1393
+ */
1394
+ function parallelProfileFromMachine(machine, label) {
1395
+ const config = machine.config;
1396
+ if (!isPlainObject(config) || !isPlainObject(config.states)) {
1397
+ return undefined;
1398
+ }
1399
+ const rootStates = config.states;
1400
+ const parallelKeys = Object.entries(rootStates)
1401
+ .filter(([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel')
1402
+ .map(([key]) => key);
1403
+ for (const stateDef of Object.values(rootStates)) {
1404
+ if (isPlainObject(stateDef) &&
1405
+ stateDef.type !== 'parallel' &&
1406
+ isPlainObject(stateDef.states) &&
1407
+ Object.keys(stateDef.states).length > 0) {
1408
+ throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
1409
+ }
1410
+ }
1411
+ if (parallelKeys.length === 0)
1412
+ return undefined;
1413
+ const byNodeId = playbookStateIdsByNodeId(config);
1414
+ const seenStateIds = new Set();
1415
+ for (const stateDef of Object.values(rootStates)) {
1416
+ const stateId = playbookStateIdOf(stateDef);
1417
+ if (stateId !== undefined)
1418
+ seenStateIds.add(stateId);
1419
+ }
1420
+ const states = [];
1421
+ const parallelStateByWorkingStateId = new Map();
1422
+ const waitResumeTargets = new Map();
1423
+ const transitionNodesByStateId = new Map();
1424
+ for (const key of parallelKeys) {
1425
+ const parent = rootStates[key];
1426
+ if (parent.onDone === undefined) {
1427
+ throw malformedParallelState(label, key, 'it declares no onDone join');
1428
+ }
1429
+ if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
1430
+ throw malformedParallelState(label, key, 'it must declare at least two regions');
1431
+ }
1432
+ const regions = [];
1433
+ const roleIds = new Set();
1434
+ for (const [regionKey, region] of Object.entries(parent.states)) {
1435
+ const regionReason = (reason) => malformedParallelState(label, key, `region ${regionKey} ${reason}`);
1436
+ if (!isPlainObject(region) ||
1437
+ region.type === 'parallel' ||
1438
+ region.type === 'final' ||
1439
+ region.type === 'history' ||
1440
+ !isPlainObject(region.states)) {
1441
+ throw regionReason('must be a compound state of leaves');
1442
+ }
1443
+ const regionStates = region.states;
1444
+ let working;
1445
+ let wait;
1446
+ let final;
1447
+ for (const [leafKey, leaf] of Object.entries(regionStates)) {
1448
+ if (!isPlainObject(leaf)) {
1449
+ throw regionReason(`leaf ${leafKey} must be a state object`);
1450
+ }
1451
+ if ((isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
1452
+ leaf.type === 'parallel' ||
1453
+ leaf.type === 'history') {
1454
+ throw regionReason(`leaf ${leafKey} must be an atomic state`);
1455
+ }
1456
+ const leafStateId = playbookStateIdOf(leaf);
1457
+ if (leafStateId === undefined || leafStateId.trim().length === 0) {
1458
+ throw regionReason(`leaf ${leafKey} must carry a string meta.playbook.stateId`);
1459
+ }
1460
+ if (seenStateIds.has(leafStateId)) {
1461
+ throw regionReason(`leaf ${leafKey} reuses playbook state id ${leafStateId}`);
1462
+ }
1463
+ seenStateIds.add(leafStateId);
1464
+ const invoke = leaf.invoke;
1465
+ const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
1466
+ const tags = stateTagsOf(leaf);
1467
+ if (leaf.type === 'final') {
1468
+ if (final !== undefined)
1469
+ throw regionReason('declares two final leaves');
1470
+ final = [leafKey, leaf];
1471
+ }
1472
+ else if (invokes.length === 1 &&
1473
+ isPlainObject(invokes[0]) &&
1474
+ invokes[0].src === 'player' &&
1475
+ tags.includes('playbook.busy')) {
1476
+ if (working !== undefined) {
1477
+ throw regionReason('declares two working leaves');
1478
+ }
1479
+ working = [leafKey, leaf];
1480
+ }
1481
+ else if (invokes.length === 0 &&
1482
+ tags.includes('playbook.parked') &&
1483
+ isPlainObject(leaf.on) &&
1484
+ leaf.on.BOSS_REPLY !== undefined) {
1485
+ if (wait !== undefined)
1486
+ throw regionReason('declares two wait leaves');
1487
+ wait = [leafKey, leaf];
1488
+ }
1489
+ else {
1490
+ throw regionReason(`leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`);
1491
+ }
1492
+ }
1493
+ if (working === undefined || wait === undefined || final === undefined) {
1494
+ throw regionReason('must hold exactly one working leaf, one wait leaf, and one final leaf');
1495
+ }
1496
+ if (region.initial !== working[0]) {
1497
+ throw regionReason('must start in its working leaf');
1498
+ }
1499
+ const workingStateId = playbookStateIdOf(working[1]);
1500
+ const waitStateId = playbookStateIdOf(wait[1]);
1501
+ const finalStateId = playbookStateIdOf(final[1]);
1502
+ const replyTargets = transitionArmTargets(wait[1].on.BOSS_REPLY).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
1503
+ const regionStateIds = new Set([waitStateId, finalStateId]);
1504
+ if (!replyTargets.includes(workingStateId) ||
1505
+ replyTargets.some((target) => regionStateIds.has(target))) {
1506
+ throw regionReason('wait leaf BOSS_REPLY arms must resume its working leaf');
1507
+ }
1508
+ const workingMeta = working[1].meta
1509
+ .playbook;
1510
+ const roleId = workingMeta.role;
1511
+ if (typeof roleId !== 'string' || roleId.trim().length === 0) {
1512
+ throw regionReason('working leaf must declare meta.playbook.role');
1513
+ }
1514
+ if (roleIds.has(roleId)) {
1515
+ throw malformedParallelState(label, key, `role ${roleId} works in more than one region`);
1516
+ }
1517
+ roleIds.add(roleId);
1518
+ regions.push({
1519
+ key: regionKey,
1520
+ roleId,
1521
+ workingStateId,
1522
+ waitStateId,
1523
+ finalStateId,
1524
+ });
1525
+ waitResumeTargets.set(waitStateId, new Set([workingStateId]));
1526
+ for (const [, leaf] of [working, wait, final]) {
1527
+ transitionNodesByStateId.set(playbookStateIdOf(leaf), [region, leaf]);
1528
+ }
1529
+ }
1530
+ const parallelState = {
1531
+ stateId: key,
1532
+ regions: Object.freeze(regions),
1533
+ };
1534
+ states.push(parallelState);
1535
+ for (const region of regions) {
1536
+ parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
1537
+ }
1538
+ transitionNodesByStateId.set(key, [parent]);
1539
+ }
1540
+ for (const [key, stateDef] of Object.entries(rootStates)) {
1541
+ if (!isPlainObject(stateDef) || parallelKeys.includes(key))
1542
+ continue;
1543
+ transitionNodesByStateId.set(key, [stateDef]);
1544
+ }
1545
+ const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
1546
+ if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
1547
+ const bossReply = rootWait.on.BOSS_REPLY;
1548
+ if (bossReply !== undefined) {
1549
+ waitResumeTargets.set(BOSS_REPLY_WAIT_STATE_ID, new Set(transitionTargets(bossReply)));
1550
+ }
1551
+ }
1552
+ return Object.freeze({
1553
+ states: Object.freeze(states),
1554
+ parallelStateByWorkingStateId,
1555
+ waitResumeTargets,
1556
+ transitionNodesByStateId,
1557
+ });
1237
1558
  }
1238
1559
  // ---------------------------------------------------------------------------
1239
1560
  // DR-029 control surface: the FSM's explicit-state-jump event and the
@@ -1840,7 +2161,48 @@ function eventContractPrompt(contract) {
1840
2161
  ]);
1841
2162
  return `{ ${members.join(', ')} }${notes.length === 0 ? '' : ` (${notes.join('; ')})`}`;
1842
2163
  }
1843
- function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
2164
+ /**
2165
+ * DR-067 §1: the questions pending in a parallel-declaring machine — the
2166
+ * keyed records whose resume state an active wait state (a region wait leaf
2167
+ * or the root reply wait) resumes.
2168
+ */
2169
+ function parallelPendingBossQuestions(profile, activeStateIds, context) {
2170
+ return keyedPendingBossQuestions(context, (resumeStateId) => activeStateIds.some((stateId) => profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true));
2171
+ }
2172
+ /** Event types the root and every active state node of a parallel machine configure. */
2173
+ function parallelConfiguredEventTypes(machine, profile, activeStateIds) {
2174
+ const configured = new Set();
2175
+ const config = machine.config;
2176
+ if (!isPlainObject(config))
2177
+ return configured;
2178
+ if (isPlainObject(config.on)) {
2179
+ for (const type of Object.keys(config.on))
2180
+ configured.add(type);
2181
+ }
2182
+ for (const stateId of activeStateIds) {
2183
+ for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
2184
+ if (!isPlainObject(node.on))
2185
+ continue;
2186
+ for (const type of Object.keys(node.on))
2187
+ configured.add(type);
2188
+ }
2189
+ }
2190
+ return configured;
2191
+ }
2192
+ function activeStateIdsOf(snapshotOrState) {
2193
+ const listed = snapshotOrState
2194
+ ?.activeStateIds;
2195
+ if (Array.isArray(listed)) {
2196
+ return listed.filter((stateId) => typeof stateId === 'string');
2197
+ }
2198
+ try {
2199
+ return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
2200
+ }
2201
+ catch {
2202
+ return [];
2203
+ }
2204
+ }
2205
+ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents, parallelProfile) {
1844
2206
  const contracts = defaultBossEventSpecs(machine, entryEvent, bossEvents);
1845
2207
  return async (text, ports, signal, snapshotOrState, boundary) => {
1846
2208
  const trimmed = text.trim();
@@ -1854,12 +2216,41 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1854
2216
  // is answered history, so the prompt must not present it as pending —
1855
2217
  // a judge told a question awaits at the failure state is steered toward
1856
2218
  // a reply it cannot select or toward no action at all.
1857
- const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
1858
- ? pendingBossQuestionFromContext(state.context)
1859
- : undefined;
1860
- const configuredTypes = configuredEventTypesForState(machine, stateId);
1861
- const applicable = [...contracts.values()].filter((contract) => configuredTypes.has(contract.type) &&
1862
- (contract.type !== 'BOSS_REPLY' || pending !== undefined));
2219
+ let pendingQuestions;
2220
+ let configuredTypes;
2221
+ if (parallelProfile === undefined) {
2222
+ const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
2223
+ ? pendingBossQuestionFromContext(state.context)
2224
+ : undefined;
2225
+ pendingQuestions = pending === undefined ? [] : [pending];
2226
+ configuredTypes = configuredEventTypesForState(machine, stateId);
2227
+ }
2228
+ else {
2229
+ // DR-067 §1: every question pending in any active wait is offered, and
2230
+ // the events of every active state node apply.
2231
+ const activeStateIds = activeStateIdsOf(snapshotOrState);
2232
+ pendingQuestions = parallelPendingBossQuestions(parallelProfile, activeStateIds, state.context);
2233
+ configuredTypes = parallelConfiguredEventTypes(machine, parallelProfile, activeStateIds);
2234
+ }
2235
+ // Several pending questions: a reply must name the one it answers, and
2236
+ // the classifier never guesses among them (gears2fsm §Parallel groups).
2237
+ const severalPending = pendingQuestions.length > 1;
2238
+ const applicable = [...contracts.values()]
2239
+ .filter((contract) => configuredTypes.has(contract.type) &&
2240
+ (contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0))
2241
+ .map((contract) => contract.type === 'BOSS_REPLY' && severalPending
2242
+ ? {
2243
+ type: 'BOSS_REPLY',
2244
+ fields: {
2245
+ questionId: {
2246
+ source: 'judge',
2247
+ required: true,
2248
+ values: pendingQuestions.map(({ questionId }) => questionId),
2249
+ },
2250
+ answer: { source: 'text', required: true },
2251
+ },
2252
+ }
2253
+ : contract);
1863
2254
  const lines = [
1864
2255
  'Classify the following Boss message into exactly one event.',
1865
2256
  'Respond with one exact flat JSON object. Do not add fields that are not shown.',
@@ -1867,9 +2258,12 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1867
2258
  '',
1868
2259
  `Current state: ${currentState}`,
1869
2260
  ];
1870
- if (pending !== undefined) {
2261
+ for (const pending of pendingQuestions) {
1871
2262
  lines.push(`Pending question id: ${pending.questionId}`, `Pending asker: ${askerLabel(pending.asker)}`, `Pending Boss question: ${pending.question}`);
1872
2263
  }
2264
+ if (severalPending) {
2265
+ lines.push('Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.');
2266
+ }
1873
2267
  lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
1874
2268
  for (const contract of applicable) {
1875
2269
  lines.push(`- ${eventContractPrompt(contract)}`);
@@ -1940,16 +2334,19 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1940
2334
  event[field] = value;
1941
2335
  }
1942
2336
  if (eventType === 'BOSS_REPLY') {
1943
- if (pending === undefined) {
2337
+ if (pendingQuestions.length === 0) {
1944
2338
  await ports.emitStatus('Classifier returned BOSS_REPLY without a pending question');
1945
2339
  return undefined;
1946
2340
  }
1947
- const questionId = event.questionId;
1948
- if (questionId !== undefined && questionId !== pending.questionId) {
1949
- await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`);
1950
- return undefined;
2341
+ if (!severalPending) {
2342
+ const pending = pendingQuestions[0];
2343
+ const questionId = event.questionId;
2344
+ if (questionId !== undefined && questionId !== pending.questionId) {
2345
+ await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`);
2346
+ return undefined;
2347
+ }
2348
+ event.questionId = pending.questionId;
1951
2349
  }
1952
- event.questionId = pending.questionId;
1953
2350
  }
1954
2351
  const can = snapshotOrState?.can;
1955
2352
  if (typeof can === 'function' &&
@@ -1960,33 +2357,6 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1960
2357
  return event;
1961
2358
  };
1962
2359
  }
1963
- function machineDeclaresParallelState(machine) {
1964
- const visit = (stateDef) => {
1965
- if (!isPlainObject(stateDef))
1966
- return false;
1967
- if (stateDef.type === 'parallel')
1968
- return true;
1969
- if (!isPlainObject(stateDef.states))
1970
- return false;
1971
- return Object.values(stateDef.states).some(visit);
1972
- };
1973
- return visit(machine.config);
1974
- }
1975
- // PBRT-52: the factory's domain is FLAT single-region machines — every
1976
- // state a direct child of the root, so each snapshot exposes exactly one
1977
- // playbook state id and every state-keyed lookup (deterministic entries,
1978
- // retry, reply-wait pendingness, configured events, descriptions) indexes
1979
- // one unambiguous identity. A compound child would be accepted and then
1980
- // silently misbehave on all of those gates, so it is rejected up front
1981
- // exactly like a parallel region.
1982
- function machineDeclaresNestedState(machine) {
1983
- const config = machine.config;
1984
- if (!isPlainObject(config) || !isPlainObject(config.states))
1985
- return false;
1986
- return Object.values(config.states).some((stateDef) => isPlainObject(stateDef) &&
1987
- isPlainObject(stateDef.states) &&
1988
- Object.keys(stateDef.states).length > 0);
1989
- }
1990
2360
  // PBRT-52: the factory's lookups index states by their root key, and the
1991
2361
  // published playbook identity is `meta.playbook.stateId` — the two must
1992
2362
  // coincide or a machine can advertise a pending question or retry under an
@@ -2065,12 +2435,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2065
2435
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
2066
2436
  throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
2067
2437
  }
2068
- if (machineDeclaresParallelState(machine)) {
2069
- throw new Error(`${label} uses a parallel state; the shared runtime supports only single-region FSMs`);
2070
- }
2071
- if (machineDeclaresNestedState(machine)) {
2072
- throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
2073
- }
2438
+ // DR-067 §1: a flat machine, or one whose only compound states are root
2439
+ // parallel states of the compiled shape; everything else is rejected.
2440
+ const parallelProfile = parallelProfileFromMachine(machine, label);
2074
2441
  assertFlatStateIdentity(machine, label);
2075
2442
  assertUnfinishedFinalStateIds(spec.unfinishedFinalStateIds, machine, label);
2076
2443
  const retainedGenerationMetadata = spec.unfinishedFinalStateIds === undefined
@@ -2098,6 +2465,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
2098
2465
  const declaredRoleIds = Object.freeze([
2099
2466
  ...new Set([...roleStates.values()].map(({ role }) => role)),
2100
2467
  ]);
2468
+ // DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
2469
+ // each player state, each wait state, and the failure state. A status is
2470
+ // scheduled for each one newly entered, and a status trace names a
2471
+ // `stateId` only while exactly one is active.
2472
+ const bossRelevantStateIds = parallelProfile === undefined
2473
+ ? undefined
2474
+ : new Set([
2475
+ ...roleStates.keys(),
2476
+ ...parallelProfile.waitResumeTargets.keys(),
2477
+ 'failed',
2478
+ ]);
2101
2479
  // PBRT-52: the artifact's own ControlView context projection. Nothing is
2102
2480
  // exported by default, so an FSM context member — including one added
2103
2481
  // after this artifact was linked — is private until named here. The two
@@ -2136,7 +2514,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2136
2514
  // supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
2137
2515
  // fail factory construction whether or not this spec overrides the
2138
2516
  // classifier that would have consumed the contracts.
2139
- const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? []);
2517
+ const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? [], parallelProfile);
2140
2518
  const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
2141
2519
  const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
2142
2520
  makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
@@ -2151,6 +2529,93 @@ export function createXStatePlaybookRuntime(machine, spec) {
2151
2529
  const cwd = options?.cwd;
2152
2530
  return typeof cwd === 'string' ? cwd : undefined;
2153
2531
  });
2532
+ // DR-067 §1: the questions pending in a state — the singular reply-wait
2533
+ // question of a flat machine, or a parallel machine's keyed questions
2534
+ // whose own wait state is active.
2535
+ const pendingBossQuestionsFor = (state, context) => {
2536
+ if (parallelProfile === undefined) {
2537
+ const pending = pendingBossQuestionForState(state, context);
2538
+ return pending === undefined ? [] : [pending];
2539
+ }
2540
+ return parallelPendingBossQuestions(parallelProfile, state.activeStateIds, context);
2541
+ };
2542
+ const singlePendingBossQuestion = (state, context) => {
2543
+ const pending = pendingBossQuestionsFor(state, context);
2544
+ return pending.length === 1 ? pending[0] : undefined;
2545
+ };
2546
+ // DR-067 §1: a status trace names a `stateId` only while exactly one
2547
+ // Boss-relevant state is active; a flat machine's is its one state.
2548
+ const statusTraceStateId = (state) => {
2549
+ if (bossRelevantStateIds === undefined)
2550
+ return state.stateId;
2551
+ const relevant = state.activeStateIds.filter((stateId) => bossRelevantStateIds.has(stateId));
2552
+ return relevant.length === 1 ? relevant[0] : undefined;
2553
+ };
2554
+ // DR-067 §1: a parallel machine schedules statuses for each newly entered
2555
+ // Boss-relevant state — a player state's role line, a wait state's
2556
+ // question lines, and the failure line — diffed against the previous
2557
+ // snapshot's active state ids, so a sibling still running is not
2558
+ // announced again.
2559
+ const parallelStatusesFor = (previousState, state, context) => {
2560
+ const prior = new Set(previousState?.activeStateIds ?? []);
2561
+ const statuses = [];
2562
+ for (const stateId of state.activeStateIds) {
2563
+ if (prior.has(stateId) || !bossRelevantStateIds.has(stateId))
2564
+ continue;
2565
+ const resumeTargets = parallelProfile.waitResumeTargets.get(stateId);
2566
+ if (resumeTargets !== undefined) {
2567
+ const questions = keyedPendingBossQuestions(context, (resumeStateId) => resumeTargets.has(resumeStateId));
2568
+ if (questions.length === 0) {
2569
+ statuses.push({ message: 'Awaiting Boss reply.' });
2570
+ continue;
2571
+ }
2572
+ for (const pending of questions) {
2573
+ statuses.push({
2574
+ message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
2575
+ }, {
2576
+ message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
2577
+ `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
2578
+ });
2579
+ }
2580
+ continue;
2581
+ }
2582
+ if (stateId === 'failed') {
2583
+ const lastError = normalizeErrorCompact(context.lastError);
2584
+ statuses.push({
2585
+ message: '◆ workflow failed; awaiting Boss recovery.',
2586
+ ...(lastError === undefined
2587
+ ? {}
2588
+ : {
2589
+ data: snapshotJsonValue({ lastError }, 'failed status data'),
2590
+ }),
2591
+ });
2592
+ continue;
2593
+ }
2594
+ const roleState = roleStates.get(stateId);
2595
+ if (roleState !== undefined) {
2596
+ statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
2597
+ }
2598
+ }
2599
+ return statuses;
2600
+ };
2601
+ // DR-067 §1: each parallel state's region roles, in region order, are one
2602
+ // concurrent role set the FSM exports; a host that forwards those sets in
2603
+ // its schema-3 authority is held to them before any agent call.
2604
+ const assertDeclaredCohorts = (hostCapabilities) => {
2605
+ const authority = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority')?.value;
2606
+ if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
2607
+ return;
2608
+ }
2609
+ const declared = authority.concurrentRoleSets;
2610
+ for (const parallelState of parallelProfile.states) {
2611
+ const roles = parallelState.regions.map(({ roleId }) => roleId);
2612
+ if (!declared.some((candidate) => Array.isArray(candidate) &&
2613
+ candidate.length === roles.length &&
2614
+ candidate.every((roleId, index) => roleId === roles[index]))) {
2615
+ throw new TypeError(`${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`);
2616
+ }
2617
+ }
2618
+ };
2154
2619
  const createPlaybookRuntime = function createPlaybookRuntime(factoryOptions) {
2155
2620
  const construction = configuredOptionsFromFactoryInput(factoryOptions, label);
2156
2621
  const configuredOptions = construction.configuredOptions;
@@ -2166,8 +2631,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2166
2631
  Object.prototype.hasOwnProperty.call(declarations, acceptedOutcome));
2167
2632
  });
2168
2633
  const repositoryCapability = hasGovernedPlayerStates
2169
- ? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label)
2634
+ ? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label, parallelProfile !== undefined)
2170
2635
  : undefined;
2636
+ if (parallelProfile !== undefined) {
2637
+ assertDeclaredCohorts(construction.hostCapabilities);
2638
+ }
2171
2639
  const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
2172
2640
  let effectLedgerMirror = currentEffectLedger();
2173
2641
  let retainedEffectSourceSessionId;
@@ -2202,9 +2670,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2202
2670
  // mutable `activeSignal` alone cannot classify a late invocation reason.
2203
2671
  let activeAborts;
2204
2672
  // The bridge binds the provenance of a child result immediately before
2205
- // its promise actor settles. The next root snapshot/error consumes this
2206
- // one-shot so background settlement emissions retain their owner.
2207
- let actorSettlementAborts;
2673
+ // its promise actor settles. The next root snapshot/error consumes it so
2674
+ // background settlement emissions retain their owner. A flat machine has
2675
+ // one settling actor at a time, so a later binding replaces an unconsumed
2676
+ // one; a parallel machine's settlements queue in order (DR-067 §1).
2677
+ const actorSettlementAborts = [];
2208
2678
  let actorSettlementErrorAborts;
2209
2679
  // Exact cancellation observed by an emission owned by the active
2210
2680
  // boundary. Ordinary runs settle from their signal/state; apply also
@@ -2235,11 +2705,29 @@ export function createXStatePlaybookRuntime(machine, spec) {
2235
2705
  // each successful result, so it is never stale.
2236
2706
  let pendingResultFailureCause;
2237
2707
  // DR-063 §2: the causes decided for thrown values that cannot carry the
2238
- // marker — a string, a frozen error — kept by identity for as long as the
2708
+ // marker — a string, a frozen error — and for the record a machine keeps
2709
+ // in place of the thrown value, kept by identity for as long as the
2239
2710
  // failure stands, so every surface that reads the FSM's own `lastError`
2240
- // publishes the cause decided for exactly that value, whatever turn reads
2241
- // it.
2711
+ // publishes the cause decided for exactly that failure, whatever turn
2712
+ // reads it.
2242
2713
  const failureCauses = createFailureCauseRetention();
2714
+ // DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
2715
+ // result, keyed by the result itself, so concurrent region calls never
2716
+ // lend one another the single slot above.
2717
+ const resultFailureCauses = new WeakMap();
2718
+ // DR-067 §1: the parallel profile's cohort bookkeeping. Every public
2719
+ // boundary advances the sequence; a parallel state's working leaves run
2720
+ // as one cohort at most once per boundary, and each cohort's semantic
2721
+ // completions — which the host settles concurrently — reconcile one at a
2722
+ // time so their durable correction-budget writes cannot race.
2723
+ let publicBoundarySequence = 0;
2724
+ const consumedCohortBoundaries = new Map();
2725
+ const pendingCohorts = new Map();
2726
+ const cohortCompletionQueue = new PQueue({ concurrency: 1 });
2727
+ // DR-067 §1: every player, judge, and nested call a parallel machine
2728
+ // starts, so a boundary's drain waits for a cancelled sibling whose port
2729
+ // ignores its signal before it settles.
2730
+ const activeBoundaryCalls = new Set();
2243
2731
  // Previous root-machine state for the inspect-driven telemetry /
2244
2732
  // status emitter. undefined before the first inspect firing.
2245
2733
  let priorState;
@@ -3015,7 +3503,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3015
3503
  ...(descriptor !== undefined
3016
3504
  ? {
3017
3505
  state: descriptor,
3018
- ...stateIdentity(descriptor.stateId),
3506
+ ...stateIdentity(statusTraceStateId(descriptor)),
3019
3507
  }
3020
3508
  : {}),
3021
3509
  }, activeTurnId !== undefined ? { turnId: activeTurnId } : {});
@@ -3048,9 +3536,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
3048
3536
  // its finish carries the receipt disposition and none of the
3049
3537
  // start-only fields, so it passes its own canonical pre-acceptance
3050
3538
  // base (slc/link.md §Playbook trace).
3051
- finishIdentity = identity) {
3539
+ finishIdentity = identity,
3540
+ // DR-067 §1: the per-call abort classification a parallel machine's
3541
+ // player pairs enqueue under; absent, the active boundary's.
3542
+ aborts) {
3052
3543
  try {
3053
- await emitTrace(startedType, identity, position);
3544
+ await emitTrace(startedType, identity, position, aborts);
3054
3545
  }
3055
3546
  catch (error) {
3056
3547
  if (!isAbortFailure(error, signal))
@@ -3060,7 +3551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3060
3551
  ...finishIdentity,
3061
3552
  status: isAbortFailure(error, signal) ? 'aborted' : 'error',
3062
3553
  error: normalizeError(error),
3063
- }, position);
3554
+ }, position, aborts);
3064
3555
  }
3065
3556
  catch {
3066
3557
  // Preserve the start failure after one best-effort finish attempt.
@@ -3199,11 +3690,32 @@ export function createXStatePlaybookRuntime(machine, spec) {
3199
3690
  ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
3200
3691
  correctionBudget: { limit: 1, spent: true },
3201
3692
  };
3693
+ // DR-067 §1 / PBRT-69: a cohort's shared receipt becomes visible for
3694
+ // every member in one ledger revision — publishing only this member's
3695
+ // spend would leave the invalid half-complete cohort the ledger admits
3696
+ // nowhere.
3697
+ const cohortMembers = current.cohortId === undefined
3698
+ ? [current]
3699
+ : currentLedger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
3700
+ const replacements = cohortMembers.map((member) => {
3701
+ if (member.boundaryId === current.boundaryId) {
3702
+ return { expected: current, next };
3703
+ }
3704
+ if (member.physicalReceipt !== undefined &&
3705
+ !isDeepStrictEqual(member.physicalReceipt, receipt)) {
3706
+ throw new TypeError(`${label} correction budget cohort conflicts with its shared repository receipt`);
3707
+ }
3708
+ return {
3709
+ expected: member,
3710
+ next: {
3711
+ ...member,
3712
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
3713
+ physicalReceipt: receipt,
3714
+ },
3715
+ };
3716
+ });
3202
3717
  const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
3203
- {
3204
- kind: 'replace-boundaries',
3205
- replacements: [{ expected: current, next }],
3206
- },
3718
+ { kind: 'replace-boundaries', replacements },
3207
3719
  ]), `${label} semantic correction budget acknowledgement`);
3208
3720
  effectLedgerMirror = acknowledged;
3209
3721
  refreshRetainedEffectReconciliation(acknowledged);
@@ -3508,7 +4020,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3508
4020
  governedOutput = undefined;
3509
4021
  }
3510
4022
  }
3511
- else if (source === 'runExclusive' && value.deferredStatus !== undefined) {
4023
+ else if (source !== 'runDeferred' && value.deferredStatus !== undefined) {
3512
4024
  throw new TypeError(`${label} non-deferred settlement returned a deferred binding status`);
3513
4025
  }
3514
4026
  if (governedSettlement !== undefined) {
@@ -3643,6 +4155,269 @@ export function createXStatePlaybookRuntime(machine, spec) {
3643
4155
  : activeEffectLedgerPrefixSequence;
3644
4156
  failedGovernedAttemptId = attemptIds.values().next().value;
3645
4157
  }
4158
+ // -----------------------------------------------------------------
4159
+ // DR-067 §1: the parallel profile's repository cohort. The working
4160
+ // leaves of one parallel state entered together run under one
4161
+ // `runCohort` claim, each member an `unchanged` boundary: a member's
4162
+ // result is adjudicated through the one reconciler in serial, a spent
4163
+ // correction rewrites every member's boundary in one acknowledged batch,
4164
+ // members are acknowledged and released in completion order, and a
4165
+ // member's failure cancels its siblings and fails the cohort with the
4166
+ // cause decided first.
4167
+ // -----------------------------------------------------------------
4168
+ function trackBoundaryCall(call) {
4169
+ if (parallelProfile === undefined)
4170
+ return call;
4171
+ activeBoundaryCalls.add(call);
4172
+ const forget = () => {
4173
+ activeBoundaryCalls.delete(call);
4174
+ };
4175
+ void call.then(forget, forget);
4176
+ return call;
4177
+ }
4178
+ async function drainBoundaryCalls() {
4179
+ while (activeBoundaryCalls.size > 0) {
4180
+ await Promise.allSettled([...activeBoundaryCalls]);
4181
+ }
4182
+ }
4183
+ function cohortParallelStateFor(stateId) {
4184
+ const parallelState = parallelProfile?.parallelStateByWorkingStateId.get(stateId);
4185
+ if (parallelState === undefined)
4186
+ return undefined;
4187
+ if (consumedCohortBoundaries.get(parallelState.stateId) ===
4188
+ publicBoundarySequence) {
4189
+ return undefined;
4190
+ }
4191
+ const pending = pendingCohorts.get(parallelState.stateId);
4192
+ if (pending !== undefined &&
4193
+ pending.boundarySequence === publicBoundarySequence &&
4194
+ !pending.started) {
4195
+ return parallelState;
4196
+ }
4197
+ const active = currentState().activeStateIds;
4198
+ return parallelState.regions.every(({ workingStateId }) => active.includes(workingStateId))
4199
+ ? parallelState
4200
+ : undefined;
4201
+ }
4202
+ function startCohort(parallelState, cohort) {
4203
+ cohort.started = true;
4204
+ consumedCohortBoundaries.set(parallelState.stateId, cohort.boundarySequence);
4205
+ if (pendingCohorts.get(parallelState.stateId) === cohort) {
4206
+ pendingCohorts.delete(parallelState.stateId);
4207
+ }
4208
+ }
4209
+ async function joinCohort(parallelState, member) {
4210
+ let cohort = pendingCohorts.get(parallelState.stateId);
4211
+ if (cohort === undefined ||
4212
+ cohort.boundarySequence !== publicBoundarySequence ||
4213
+ cohort.started) {
4214
+ cohort = {
4215
+ boundarySequence: publicBoundarySequence,
4216
+ members: new Map(),
4217
+ started: false,
4218
+ };
4219
+ pendingCohorts.set(parallelState.stateId, cohort);
4220
+ }
4221
+ const registered = cohort;
4222
+ if (registered.members.has(member.region.key)) {
4223
+ throw new Error(`${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`);
4224
+ }
4225
+ registered.members.set(member.region.key, member);
4226
+ // A member whose invocation stops before every sibling registered —
4227
+ // its region exited, or the boundary aborted — leaves no cohort to
4228
+ // run: each registered member settles with its own cancellation and
4229
+ // the boundary's cohort is spent.
4230
+ const withdraw = () => {
4231
+ if (registered.started)
4232
+ return;
4233
+ startCohort(parallelState, registered);
4234
+ for (const candidate of registered.members.values()) {
4235
+ candidate.settled.reject(candidate.signal.aborted
4236
+ ? candidate.signal.reason
4237
+ : member.signal.reason);
4238
+ }
4239
+ };
4240
+ if (member.signal.aborted) {
4241
+ withdraw();
4242
+ }
4243
+ else {
4244
+ member.signal.addEventListener('abort', withdraw, { once: true });
4245
+ }
4246
+ if (!registered.started &&
4247
+ registered.members.size === parallelState.regions.length) {
4248
+ startCohort(parallelState, registered);
4249
+ void settleCohort(parallelState, registered);
4250
+ }
4251
+ return await member.settled.promise;
4252
+ }
4253
+ async function settleCohort(parallelState, cohort) {
4254
+ const members = parallelState.regions.map(({ key }) => cohort.members.get(key));
4255
+ const cancellation = new AbortController();
4256
+ const completionOrder = [];
4257
+ const decided = new Map();
4258
+ let decisionSequence = 0;
4259
+ // DR-063 §2: each member failure is one decision, ordered as it was
4260
+ // decided, with the cause decided for exactly that value at that
4261
+ // moment — so a value a sibling later rethrows cannot rename it.
4262
+ const decide = (member, error, cause = failureCauses.causeOf(error)) => {
4263
+ if (decided.has(member))
4264
+ return;
4265
+ decided.set(member, { error, cause, sequence: ++decisionSequence });
4266
+ };
4267
+ const isCancellation = (error) => cancellation.signal.aborted &&
4268
+ Object.is(error, cancellation.signal.reason) &&
4269
+ !activeAborts?.isAbortReason(error);
4270
+ const operationFor = (member) => async (context) => {
4271
+ const callSignal = combineAbortSignals(member.signal, cancellation.signal);
4272
+ try {
4273
+ if (activePlayerKeys.has(member.playerKey)) {
4274
+ await member.reportCollision(callSignal);
4275
+ }
4276
+ activePlayerKeys.add(member.playerKey);
4277
+ try {
4278
+ const result = await member.run(callSignal, cancellation.signal, effectAuthorizedPreExistingBlock(member.effectBoundary, context?.baseline));
4279
+ if (result.status !== 'ok' && !cancellation.signal.aborted) {
4280
+ // DR-063 §2: the failure built for a non-`ok` member result
4281
+ // carries the cause decided for exactly that result, and its
4282
+ // siblings are cancelled with it.
4283
+ const cause = resultFailureCauses.get(result) ??
4284
+ runtimeDefectCause(`callPlayer status ${JSON.stringify(result.status)}`);
4285
+ const failure = attachPlaybookFailureCause(markFsmResultFailure(new Error(result.error ??
4286
+ `captainBridge: callPlayer status "${result.status}"`)), cause);
4287
+ failureCauses.retain(failure, cause);
4288
+ decide(member, failure, cause);
4289
+ cancellation.abort(failure);
4290
+ }
4291
+ return result;
4292
+ }
4293
+ finally {
4294
+ activePlayerKeys.delete(member.playerKey);
4295
+ }
4296
+ }
4297
+ catch (error) {
4298
+ if (!isCancellation(error))
4299
+ decide(member, error);
4300
+ if (!cancellation.signal.aborted)
4301
+ cancellation.abort(error);
4302
+ throw error;
4303
+ }
4304
+ finally {
4305
+ completionOrder.push(member);
4306
+ }
4307
+ };
4308
+ const completeEffectBoundary = (completion) => cohortCompletionQueue.add(async () => {
4309
+ const member = members.find(({ roleId }) => roleId === completion.roleId);
4310
+ if (member === undefined ||
4311
+ completion.boundary.boundaryId !== member.effectBoundary.boundaryId) {
4312
+ throw new TypeError(`${label} repository cohort completed an undeclared member`);
4313
+ }
4314
+ const evidence = await completionEvidenceFor(member.input, member.roleId, member.playerId, member.signal, undefined)(completion);
4315
+ const settlement = governedSettlementsByBoundaryId.get(member.effectBoundary.boundaryId);
4316
+ if (settlement?.status === 'unresolved') {
4317
+ decide(member, settlement.error);
4318
+ }
4319
+ return evidence;
4320
+ });
4321
+ const runSignal = activeSignal ?? members[0].signal;
4322
+ const invocationId = randomUUID();
4323
+ let settled;
4324
+ try {
4325
+ for (const member of members)
4326
+ member.signal.throwIfAborted();
4327
+ settled = await repositoryCapability.runCohort({
4328
+ signal: runSignal,
4329
+ invocationId,
4330
+ roleIds: members.map(({ roleId }) => roleId),
4331
+ dispositionsByRole: Object.fromEntries(members.map(({ roleId }) => [roleId, ['unchanged']])),
4332
+ effectBoundaries: Object.fromEntries(members.map(({ roleId, effectBoundary }) => [
4333
+ roleId,
4334
+ effectBoundary,
4335
+ ])),
4336
+ operations: Object.fromEntries(members.map((member) => [member.roleId, operationFor(member)])),
4337
+ completeEffectBoundary,
4338
+ });
4339
+ if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
4340
+ throw new TypeError(`${label} repository cohort changed its invocation identity`);
4341
+ }
4342
+ }
4343
+ catch (error) {
4344
+ for (const member of members) {
4345
+ governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
4346
+ governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
4347
+ refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
4348
+ }
4349
+ if (!isAbortFailure(error, runSignal))
4350
+ controlPlaneError ??= error;
4351
+ for (const member of members)
4352
+ member.settled.reject(error);
4353
+ return;
4354
+ }
4355
+ const releaseOrder = [
4356
+ ...completionOrder,
4357
+ ...members.filter((member) => !completionOrder.includes(member)),
4358
+ ];
4359
+ const acknowledged = new Map();
4360
+ const failures = new Map();
4361
+ for (const member of releaseOrder) {
4362
+ const operation = isPlainObject(settled.operations)
4363
+ ? settled.operations[member.roleId]
4364
+ : undefined;
4365
+ try {
4366
+ const result = acknowledgeGovernedPlayerResult({
4367
+ operation,
4368
+ receipt: isPlainObject(settled.receipts)
4369
+ ? settled.receipts[member.roleId]
4370
+ : undefined,
4371
+ effectLedger: settled.effectLedger,
4372
+ }, member.effectBoundary.boundaryId, 'runCohort');
4373
+ acknowledged.set(member, result);
4374
+ const settlement = governedPlayerSettlements.get(result);
4375
+ if (settlement?.status === 'unresolved') {
4376
+ failures.set(member, settlement.error);
4377
+ }
4378
+ else if (result.status !== 'ok') {
4379
+ failures.set(member, decided.get(member)?.error);
4380
+ }
4381
+ }
4382
+ catch (error) {
4383
+ failures.set(member, error);
4384
+ const operationReason = isPlainObject(operation) &&
4385
+ operation.status === 'rejected' &&
4386
+ Object.is(operation.reason, error);
4387
+ if (!operationReason) {
4388
+ // An acknowledgement the host could not prove is a control-plane
4389
+ // defect, exactly as on the exclusive path.
4390
+ if (!isAbortFailure(error, runSignal))
4391
+ controlPlaneError ??= error;
4392
+ decide(member, error);
4393
+ }
4394
+ }
4395
+ finally {
4396
+ governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
4397
+ governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
4398
+ }
4399
+ }
4400
+ if (failures.size === 0) {
4401
+ for (const member of releaseOrder) {
4402
+ member.settled.resolve(acknowledged.get(member));
4403
+ }
4404
+ return;
4405
+ }
4406
+ for (const result of acknowledged.values()) {
4407
+ governedPlayerSettlements.delete(result);
4408
+ }
4409
+ const first = [...decided.values()].sort((left, right) => left.sequence - right.sequence)[0];
4410
+ const failure = first?.error ??
4411
+ failures.get(releaseOrder.find((member) => failures.has(member))) ??
4412
+ markFsmResultFailure(new Error(`${label} parallel state ${parallelState.stateId} cohort failed`));
4413
+ const cause = first?.cause ?? failureCauses.causeOf(failure);
4414
+ if (cause !== undefined) {
4415
+ failureCauses.retain(failure, cause);
4416
+ attachPlaybookFailureCause(failure, cause);
4417
+ }
4418
+ for (const member of members)
4419
+ member.settled.reject(failure);
4420
+ }
3646
4421
  const boundary = {
3647
4422
  async callPlayer(input, roleId, freshPrompt, signal) {
3648
4423
  // State-entry telemetry/status must precede the call they describe.
@@ -3687,126 +4462,173 @@ export function createXStatePlaybookRuntime(machine, spec) {
3687
4462
  callId,
3688
4463
  };
3689
4464
  const playerKey = continuationKey(roleId, playerId);
3690
- if (activePlayerKeys.has(playerKey)) {
3691
- const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
3692
- await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, signal);
3693
- await emitTrace('player.call.finished', {
3694
- ...callIdentity(selectedResume),
3695
- status: 'error',
3696
- error: normalizeError(error),
3697
- }, position);
3698
- throw error;
3699
- }
3700
- activePlayerKeys.add(playerKey);
3701
- try {
3702
- const runTracedPlayerCall = async (resume = selectedResume,
3703
- // DR-062 §4: composed from this call's own baseline observation,
3704
- // so the prompt the trace records is the prompt the player is sent.
3705
- preExistingBlock) => {
3706
- const identity = callIdentity(resume);
3707
- const withPreExisting = (text) => preExistingBlock === undefined
3708
- ? text
3709
- : `${text}\n\n${preExistingBlock}`;
3710
- const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
3711
- const callFreshPrompt = withPreExisting(freshPrompt);
3712
- await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, signal);
3713
- let rawResult;
4465
+ const runTracedPlayerCall = async (resume = selectedResume,
4466
+ // DR-062 §4: composed from this call's own baseline observation,
4467
+ // so the prompt the trace records is the prompt the player is sent.
4468
+ preExistingBlock,
4469
+ // DR-067 §1: a cohort member's call signal also carries its
4470
+ // cohort's sibling cancellation.
4471
+ callSignal = signal, cohortCancellation) => {
4472
+ const identity = callIdentity(resume);
4473
+ // DR-067 §1: a parallel machine classifies aborts per call, so a
4474
+ // trace sink rejecting with a sibling's cancellation is forgiven
4475
+ // for that call alone.
4476
+ const traceAborts = parallelProfile === undefined
4477
+ ? undefined
4478
+ : abortReasonClassifier(callSignal, activeAborts);
4479
+ const withPreExisting = (text) => preExistingBlock === undefined
4480
+ ? text
4481
+ : `${text}\n\n${preExistingBlock}`;
4482
+ const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
4483
+ const callFreshPrompt = withPreExisting(freshPrompt);
4484
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, callSignal, undefined, traceAborts);
4485
+ let rawResult;
4486
+ try {
4487
+ // An abort may land while the awaited started emission drains
4488
+ // (e.g. fired from the trace sink itself); the host call must
4489
+ // never start after abort, so settle the already-started pair
4490
+ // as `aborted` through the catch below.
4491
+ callSignal.throwIfAborted();
4492
+ rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, callSignal, {
4493
+ resume,
4494
+ ...(callPrompt === callFreshPrompt
4495
+ ? {}
4496
+ : { freshPrompt: callFreshPrompt }),
4497
+ });
4498
+ // A host promise is not required to honor cancellation. Do not
4499
+ // let a late result mutate continuity or publish a successful
4500
+ // finish.
4501
+ callSignal.throwIfAborted();
4502
+ }
4503
+ catch (error) {
4504
+ if (!isAbortFailure(error, callSignal))
4505
+ controlPlaneError ??= error;
3714
4506
  try {
3715
- // An abort may land while the awaited started emission drains
3716
- // (e.g. fired from the trace sink itself); the host call must
3717
- // never start after abort, so settle the already-started pair
3718
- // as `aborted` through the catch below.
3719
- signal.throwIfAborted();
3720
- rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, signal, {
3721
- resume,
3722
- ...(callPrompt === callFreshPrompt
3723
- ? {}
3724
- : { freshPrompt: callFreshPrompt }),
3725
- });
3726
- // A host promise is not required to honor cancellation. Do not
3727
- // let a late result mutate continuity or publish a successful
3728
- // finish.
3729
- signal.throwIfAborted();
4507
+ await emitTrace('player.call.finished', {
4508
+ ...identity,
4509
+ status: isAbortFailure(error, callSignal)
4510
+ ? 'aborted'
4511
+ : 'error',
4512
+ error: normalizeError(error),
4513
+ }, position, traceAborts);
3730
4514
  }
3731
- catch (error) {
3732
- if (!isAbortFailure(error, signal))
3733
- controlPlaneError ??= error;
3734
- try {
3735
- await emitTrace('player.call.finished', {
3736
- ...identity,
3737
- status: isAbortFailure(error, signal) ? 'aborted' : 'error',
3738
- error: normalizeError(error),
3739
- }, position);
3740
- }
3741
- catch {
3742
- // The original non-abort port rejection remains authoritative.
3743
- }
3744
- // A thrown port call carries no authoritative result, so the
3745
- // prior token remains available for a later explicit resume.
3746
- throw decidePlayerCallFailure(error, roleId, playerId, signal);
4515
+ catch {
4516
+ // The original non-abort port rejection remains authoritative.
3747
4517
  }
3748
- let result;
4518
+ // A thrown port call carries no authoritative result, so the
4519
+ // prior token remains available for a later explicit resume.
4520
+ throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
4521
+ }
4522
+ let result;
4523
+ try {
4524
+ result = validatePlayerResult(rawResult);
4525
+ }
4526
+ catch (error) {
4527
+ if (!isAbortFailure(error, callSignal))
4528
+ controlPlaneError ??= error;
3749
4529
  try {
3750
- result = validatePlayerResult(rawResult);
4530
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
3751
4531
  }
3752
- catch (error) {
3753
- if (!isAbortFailure(error, signal))
3754
- controlPlaneError ??= error;
3755
- try {
3756
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3757
- }
3758
- catch {
3759
- // The malformed host result remains authoritative.
3760
- }
3761
- throw decidePlayerCallFailure(error, roleId, playerId, signal);
4532
+ catch {
4533
+ // The malformed host result remains authoritative.
3762
4534
  }
4535
+ throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
4536
+ }
4537
+ try {
4538
+ updatePlayerResume(roleId, playerId, result);
4539
+ }
4540
+ catch (error) {
4541
+ if (!isAbortFailure(error, callSignal))
4542
+ controlPlaneError ??= error;
3763
4543
  try {
3764
- updatePlayerResume(roleId, playerId, result);
3765
- }
3766
- catch (error) {
3767
- if (!isAbortFailure(error, signal))
3768
- controlPlaneError ??= error;
3769
- try {
3770
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3771
- }
3772
- catch {
3773
- // The continuation-store failure remains authoritative.
3774
- }
3775
- throw error;
3776
- }
3777
- await emitTrace('player.call.finished', {
3778
- ...identity,
3779
- status: result.status,
3780
- ...(result.finalText !== undefined
3781
- ? { finalText: result.finalText }
3782
- : {}),
3783
- ...(result.error !== undefined
3784
- ? { error: normalizeError(result.error) }
3785
- : {}),
3786
- ...(result.resumeToken !== undefined
3787
- ? { resumeToken: result.resumeToken }
3788
- : {}),
3789
- }, position);
3790
- // DR-063 §2: the player bridge builds the failure Error for a
3791
- // non-`ok` result, so the cause is decided here, where the role,
3792
- // the player, and the reported error are known, and a successful
3793
- // result clears it so no later failure inherits it.
3794
- if (result.status === 'ok') {
3795
- pendingResultFailureCause = undefined;
4544
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
3796
4545
  }
3797
- else if (result.status === 'aborted') {
3798
- pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
3799
- }
3800
- else {
3801
- pendingResultFailureCause = playerFailureCause({
3802
- roleId,
3803
- ...(playerId === undefined ? {} : { playerId }),
3804
- error: result.error ??
3805
- `callPlayer status ${JSON.stringify(result.status)}`,
3806
- });
4546
+ catch {
4547
+ // The continuation-store failure remains authoritative.
3807
4548
  }
3808
- return result;
3809
- };
4549
+ throw error;
4550
+ }
4551
+ await emitTrace('player.call.finished', {
4552
+ ...identity,
4553
+ status: result.status,
4554
+ ...(result.finalText !== undefined
4555
+ ? { finalText: result.finalText }
4556
+ : {}),
4557
+ ...(result.error !== undefined
4558
+ ? { error: normalizeError(result.error) }
4559
+ : {}),
4560
+ ...(result.resumeToken !== undefined
4561
+ ? { resumeToken: result.resumeToken }
4562
+ : {}),
4563
+ }, position, traceAborts);
4564
+ // DR-063 §2: the player bridge builds the failure Error for a
4565
+ // non-`ok` result, so the cause is decided here, where the role,
4566
+ // the player, and the reported error are known, and a successful
4567
+ // result clears it so no later failure inherits it.
4568
+ if (result.status === 'ok') {
4569
+ pendingResultFailureCause = undefined;
4570
+ }
4571
+ else if (result.status === 'aborted') {
4572
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
4573
+ }
4574
+ else {
4575
+ pendingResultFailureCause = playerFailureCause({
4576
+ roleId,
4577
+ ...(playerId === undefined ? {} : { playerId }),
4578
+ error: result.error ??
4579
+ `callPlayer status ${JSON.stringify(result.status)}`,
4580
+ });
4581
+ }
4582
+ if (pendingResultFailureCause !== undefined) {
4583
+ resultFailureCauses.set(result, pendingResultFailureCause);
4584
+ }
4585
+ return result;
4586
+ };
4587
+ const reportPlayerKeyCollision = async (callSignal) => {
4588
+ const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
4589
+ const traceAborts = parallelProfile === undefined
4590
+ ? undefined
4591
+ : abortReasonClassifier(callSignal, activeAborts);
4592
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, callSignal, undefined, traceAborts);
4593
+ await emitTrace('player.call.finished', {
4594
+ ...callIdentity(selectedResume),
4595
+ status: 'error',
4596
+ error: normalizeError(error),
4597
+ }, position, traceAborts);
4598
+ throw error;
4599
+ };
4600
+ // DR-067 §1: the working leaves of one parallel state entered
4601
+ // together run as one all-`unchanged` repository cohort, at most once
4602
+ // per public boundary; a corrective re-ask and a single region
4603
+ // resuming after its Boss reply take the exclusive path below.
4604
+ const cohortState = deferredContinuation === undefined
4605
+ ? cohortParallelStateFor(input.stateId)
4606
+ : undefined;
4607
+ if (cohortState !== undefined) {
4608
+ const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
4609
+ if (effectBoundary === undefined ||
4610
+ repositoryCapability?.runCohort === undefined) {
4611
+ throw new Error(`${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`);
4612
+ }
4613
+ activeGovernedBoundarySeen = true;
4614
+ return await joinCohort(cohortState, {
4615
+ region: cohortState.regions.find(({ workingStateId }) => workingStateId === input.stateId),
4616
+ input,
4617
+ roleId,
4618
+ playerId,
4619
+ playerKey,
4620
+ signal,
4621
+ effectBoundary,
4622
+ run: (callSignal, cohortCancellation, preExistingBlock) => runTracedPlayerCall(selectedResume, preExistingBlock, callSignal, cohortCancellation),
4623
+ reportCollision: reportPlayerKeyCollision,
4624
+ settled: deferredValue(),
4625
+ });
4626
+ }
4627
+ if (activePlayerKeys.has(playerKey)) {
4628
+ await reportPlayerKeyCollision(signal);
4629
+ }
4630
+ activePlayerKeys.add(playerKey);
4631
+ try {
3810
4632
  const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
3811
4633
  if (deferredContinuation !== undefined) {
3812
4634
  if (effectBoundary === undefined ||
@@ -3875,11 +4697,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
3875
4697
  activePlayerKeys.delete(playerKey);
3876
4698
  }
3877
4699
  },
3878
- markPlayerResultFailure(error) {
3879
- if (pendingResultFailureCause === undefined)
4700
+ markPlayerResultFailure(error, result) {
4701
+ const cause = (result === undefined ? undefined : resultFailureCauses.get(result)) ??
4702
+ pendingResultFailureCause;
4703
+ if (cause === undefined)
3880
4704
  return error;
3881
- failureCauses.retain(error, pendingResultFailureCause);
3882
- return attachPlaybookFailureCause(error, pendingResultFailureCause);
4705
+ failureCauses.retain(error, cause);
4706
+ return attachPlaybookFailureCause(error, cause);
3883
4707
  },
3884
4708
  takeGovernedPlayerOutput(result) {
3885
4709
  const settlement = governedPlayerSettlements.get(result);
@@ -4067,6 +4891,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
4067
4891
  });
4068
4892
  },
4069
4893
  };
4894
+ if (parallelProfile !== undefined) {
4895
+ // DR-067 §1: a parallel machine's boundary drain waits for every player
4896
+ // and judge call it started.
4897
+ const untrackedCallPlayer = boundary.callPlayer;
4898
+ const untrackedCallJudge = boundary.callJudge;
4899
+ boundary.callPlayer = (input, roleId, freshPrompt, signal) => trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
4900
+ boundary.callJudge = (purpose, stateId, prompt, signal) => trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
4901
+ }
4070
4902
  function playerActor(ports) {
4071
4903
  return createPlayerBridge({
4072
4904
  resolveRoleId: requireRoleId,
@@ -4335,7 +5167,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
4335
5167
  const nestedBridge = createNestedPlaybookBridge({
4336
5168
  nextCallId: () => `playbook-${++playbookCallSequence}`,
4337
5169
  getBoundarySignal: () => activeSignal,
4338
- callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
5170
+ callPlaybook: (request, signal) => {
5171
+ const call = requireHostPorts().callPlaybook(request, signal);
5172
+ return parallelProfile === undefined
5173
+ ? call
5174
+ : trackBoundaryCall(Promise.resolve(call));
5175
+ },
4339
5176
  emitStarted: async (event, aborts) => {
4340
5177
  playbookCallTurnIds.set(event.callId, activeTurnId);
4341
5178
  if (hasGovernedPlayerStates) {
@@ -4374,7 +5211,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4374
5211
  activeAborts = aborts ?? abortReasonClassifier(signal);
4375
5212
  },
4376
5213
  bindActorSettlement: (aborts) => {
4377
- actorSettlementAborts = aborts;
5214
+ if (parallelProfile === undefined)
5215
+ actorSettlementAborts.length = 0;
5216
+ actorSettlementAborts.push(aborts);
4378
5217
  },
4379
5218
  onControlPlaneError: (error, aborts) => {
4380
5219
  // The shared bridge classifies before reporting against its own
@@ -4412,9 +5251,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
4412
5251
  runtimeDefectCause(normalized.message),
4413
5252
  };
4414
5253
  }
5254
+ // DR-063 §2: the failure that parks the machine is the error its parking
5255
+ // transition carried, but a compiled machine keeps a JSON-safe record of
5256
+ // it as `lastError` (gears2fsm requires JSON-safe context), so neither
5257
+ // that value's identity nor its marker reaches the failed state. The cause
5258
+ // decided for the carried error is retained under the value the machine
5259
+ // kept, so every surface of this turn and of each later one publishes the
5260
+ // runtime's own decision for as long as that failure stands. A kept value
5261
+ // that already answers keeps its cause.
5262
+ function bindParkedFailureCause(context, event) {
5263
+ const kept = context.lastError;
5264
+ if (kept === undefined || kept === null)
5265
+ return;
5266
+ if (failureCauses.causeOf(kept) !== undefined)
5267
+ return;
5268
+ const decided = failureCauses.causeOf(actorErrorOf(event));
5269
+ if (decided !== undefined)
5270
+ failureCauses.retain(kept, decided);
5271
+ }
4415
5272
  // DR-063 §2: a rejected player port or a result the runtime cannot read is
4416
- // a `player-failed` failure named by role; an abort is `aborted`.
4417
- function decidePlayerCallFailure(error, roleId, playerId, signal) {
5273
+ // a `player-failed` failure named by role; an abort is `aborted`. A
5274
+ // cancellation the runtime itself issued to a cohort member because its
5275
+ // sibling failed is not this player's failure and decides nothing, so the
5276
+ // originator's cause stands (DR-067 §1, playbook-55).
5277
+ function decidePlayerCallFailure(error, roleId, playerId, signal, cohortCancellation) {
5278
+ if (cohortCancellation?.aborted === true &&
5279
+ Object.is(error, cohortCancellation.reason) &&
5280
+ !activeAborts?.isAbortReason(error)) {
5281
+ return error;
5282
+ }
4418
5283
  const cause = isAbortFailure(error, signal)
4419
5284
  ? ABORTED_FAILURE_CAUSE
4420
5285
  : playerFailureCause({
@@ -4459,10 +5324,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
4459
5324
  previousState: previousState ?? null,
4460
5325
  state,
4461
5326
  };
4462
- const pendingBossQuestion = pendingBossQuestionForState(state, context);
4463
- if (pendingBossQuestion !== undefined &&
4464
- !hasUnresolvedReconciliation()) {
4465
- payload.pendingBossQuestion = pendingBossQuestion;
5327
+ if (parallelProfile === undefined) {
5328
+ const pendingBossQuestion = pendingBossQuestionForState(state, context);
5329
+ if (pendingBossQuestion !== undefined &&
5330
+ !hasUnresolvedReconciliation()) {
5331
+ payload.pendingBossQuestion = pendingBossQuestion;
5332
+ }
5333
+ }
5334
+ else if (!hasUnresolvedReconciliation()) {
5335
+ // DR-067 §1: a parallel machine's telemetry carries every question
5336
+ // pending in an active wait, in the plural.
5337
+ const pendingBossQuestions = pendingBossQuestionsFor(state, context);
5338
+ if (pendingBossQuestions.length > 0) {
5339
+ payload.pendingBossQuestions = pendingBossQuestions;
5340
+ }
4466
5341
  }
4467
5342
  if (state.stateId === 'failed') {
4468
5343
  const lastError = failedStateError(context);
@@ -4482,7 +5357,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4482
5357
  message,
4483
5358
  ...(data === undefined ? {} : { data }),
4484
5359
  state,
4485
- ...stateIdentity(state.stateId),
5360
+ ...stateIdentity(statusTraceStateId(state)),
4486
5361
  }, position),
4487
5362
  }));
4488
5363
  void enqueueEmission(async () => {
@@ -4523,8 +5398,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4523
5398
  controlPlaneError ??= error;
4524
5399
  }
4525
5400
  function consumeActorSettlementAborts(forSnapshot = false) {
4526
- const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
4527
- actorSettlementAborts = undefined;
5401
+ const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
4528
5402
  actorSettlementErrorAborts = undefined;
4529
5403
  if (forSnapshot && aborts !== undefined) {
4530
5404
  // XState can report an errored root through both its inspection
@@ -4599,7 +5473,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
4599
5473
  try {
4600
5474
  const snap = inspectionEvent.snapshot;
4601
5475
  const state = normalizePlaybookSnapshot(snap);
4602
- if (state.stateId === undefined) {
5476
+ if (state.stateId === undefined &&
5477
+ !(parallelProfile?.states.some(({ stateId }) => state.activeStateIds.includes(stateId)) ?? false)) {
4603
5478
  throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
4604
5479
  }
4605
5480
  const previousState = priorState;
@@ -4624,6 +5499,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4624
5499
  }
4625
5500
  const context = (snap.context ??
4626
5501
  {});
5502
+ if (state.stateId === 'failed') {
5503
+ bindParkedFailureCause(context, inspectionEvent.event);
5504
+ }
4627
5505
  if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
4628
5506
  deferredReconciliationOperationId !== undefined) {
4629
5507
  priorState = state;
@@ -4635,7 +5513,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4635
5513
  validateBoundQuestionProjection();
4636
5514
  }
4637
5515
  const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
4638
- const stateStatuses = completeFailedStatuses(statusesForState(state, context, inspectionEvent.event), state, context);
5516
+ const stateStatuses = completeFailedStatuses(bossRelevantStateIds !== undefined && usesDefaultStatuses
5517
+ ? parallelStatusesFor(previousState, state, context)
5518
+ : statusesForState(state, context, inspectionEvent.event), state, context);
4639
5519
  const outcomeStatuses = usesDefaultStatuses
4640
5520
  ? acceptedOutcomes.map(({ acceptedOutcome }) => ({
4641
5521
  message: `→ ${acceptedOutcome}`,
@@ -4816,13 +5696,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
4816
5696
  emissionQueue.clear();
4817
5697
  judgeQueue.clear();
4818
5698
  appliedReceipts.clear();
5699
+ pendingCohorts.clear();
5700
+ consumedCohortBoundaries.clear();
5701
+ activeBoundaryCalls.clear();
5702
+ cohortCompletionQueue.clear();
4819
5703
  actor = undefined;
4820
5704
  session = undefined;
4821
5705
  savedPorts = undefined;
4822
5706
  runtimePorts = undefined;
4823
5707
  activeSignal = undefined;
4824
5708
  activeAborts = undefined;
4825
- actorSettlementAborts = undefined;
5709
+ actorSettlementAborts.length = 0;
4826
5710
  actorSettlementErrorAborts = undefined;
4827
5711
  activeAbortEmission = undefined;
4828
5712
  activeTurnId = undefined;
@@ -4897,11 +5781,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
4897
5781
  // context member above sources. A candidate whose event the live
4898
5782
  // snapshot does not accept — or whose payload the runtime can source
4899
5783
  // from neither — is excluded rather than completed with invented text.
5784
+ // The caller has already applied the PBRT-71 replay fence.
4900
5785
  function retryActionFor(snapshot, stateId) {
4901
5786
  if (stateId !== 'failed')
4902
5787
  return undefined;
4903
- if (!failedAttemptAllowsReplay())
4904
- return undefined;
4905
5788
  const retryEvent = retryEventFrom(snapshot);
4906
5789
  if (retryEvent === undefined)
4907
5790
  return undefined;
@@ -5012,6 +5895,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
5012
5895
  });
5013
5896
  return derived;
5014
5897
  }
5898
+ // PBRT-71 / DR-040 §4: out of the failure state, a jump restarts the
5899
+ // failed attempt's work exactly as the retry does, so one fence gates
5900
+ // both. Unless every governed boundary of that attempt carries a
5901
+ // complete `unchanged` receipt, neither is advertised — and `apply`,
5902
+ // which revalidates against this list, accepts neither — so no
5903
+ // control can replay a player call whose effect may already exist.
5904
+ if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
5905
+ return derived;
5906
+ }
5015
5907
  const retry = retryActionFor(snapshot, state.stateId);
5016
5908
  if (retry !== undefined)
5017
5909
  derived.push(retry);
@@ -5077,9 +5969,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
5077
5969
  // state whose source declares no description publishes none: an id is
5078
5970
  // never promoted into a description by default.
5079
5971
  function stateDescriptionFor(state) {
5972
+ // DR-067 §1: inside a parallel state, the meaning published is the
5973
+ // root state's — the parallel parent's own description.
5080
5974
  const keys = [
5081
5975
  ...(state.stateId === undefined ? [] : [state.stateId]),
5082
5976
  ...(typeof state.value === 'string' ? [state.value] : []),
5977
+ ...(isPlainObject(state.value) ? Object.keys(state.value) : []),
5083
5978
  ...state.activeStateIds,
5084
5979
  ];
5085
5980
  for (const key of keys) {
@@ -5363,7 +6258,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5363
6258
  const context = (snapshot
5364
6259
  ?.context ?? {});
5365
6260
  const actual = state?.stateId === BOSS_REPLY_WAIT_STATE_ID
5366
- ? pendingBossQuestionFromContext(context)
6261
+ ? singlePendingBossQuestion(state, context)
5367
6262
  : undefined;
5368
6263
  if (!isDeepStrictEqual(actual, expected)) {
5369
6264
  throw new Error(`${label} deferred FSM question does not equal its durable binding`);
@@ -5622,7 +6517,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5622
6517
  const state = normalizePlaybookSnapshot(snapshot);
5623
6518
  const context = (snapshot.context ??
5624
6519
  {});
5625
- const pending = pendingBossQuestionForState(state, context);
6520
+ const pending = singlePendingBossQuestion(state, context);
5626
6521
  if (pending === undefined ||
5627
6522
  currentBoundDeferredOperation(pending)?.operationId !== operationId) {
5628
6523
  throw new TypeError(`${label} restored deferred wait does not equal its FSM question`);
@@ -5732,9 +6627,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
5732
6627
  refreshRetainedEffectReconciliation(effectLedgerMirror);
5733
6628
  syncDeferredReconciliationOverlay();
5734
6629
  refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5735
- const pending = !hasUnresolvedReconciliation()
5736
- ? pendingBossQuestionForState(state, context ?? {})
5737
- : undefined;
6630
+ const pendingQuestions = !hasUnresolvedReconciliation()
6631
+ ? pendingBossQuestionsFor(state, context ?? {})
6632
+ : [];
5738
6633
  const failedEffectAttempt = hasGovernedPlayerStates &&
5739
6634
  state.stateId === 'failed' &&
5740
6635
  failedAttemptMatchesCurrentLedger(effectLedgerMirror)
@@ -5759,16 +6654,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
5759
6654
  : {}),
5760
6655
  },
5761
6656
  state,
5762
- pendingBossQuestions: pending === undefined
5763
- ? []
5764
- : [
5765
- {
5766
- questionId: pending.questionId,
5767
- asker: pending.asker,
5768
- question: pending.question,
5769
- sourceItem: pending.sourceItem,
5770
- },
5771
- ],
6657
+ pendingBossQuestions: pendingQuestions.map((pending) => ({
6658
+ questionId: pending.questionId,
6659
+ asker: pending.asker,
6660
+ question: pending.question,
6661
+ sourceItem: pending.sourceItem,
6662
+ })),
5772
6663
  effectLedger: effectLedgerMirror,
5773
6664
  ...(retainedEffectSourceSessionId === undefined
5774
6665
  ? {}
@@ -5791,13 +6682,19 @@ export function createXStatePlaybookRuntime(machine, spec) {
5791
6682
  await rehydrateSnapshot('restore', nextSession, snapshot);
5792
6683
  },
5793
6684
  // DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
5794
- // engagement identity and counter lineage, exposed separately so
5795
- // capability-less bespoke runtimes can omit it. Runtime-visible
5796
- // preflight mismatches reject before effects; after preflight the new
6685
+ // engagement identity and counter lineage, exposed separately so a
6686
+ // capability-less runtime can omit it. Runtime-visible preflight
6687
+ // mismatches reject before effects; after preflight the new
5797
6688
  // session.started boundary owns failed-start cleanup just like init.
5798
- async adopt(nextSession, snapshot, context) {
5799
- await rehydrateSnapshot('adopt', nextSession, snapshot, context);
5800
- },
6689
+ // DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
6690
+ // machine that declares a parallel state omits the member.
6691
+ ...(parallelProfile === undefined
6692
+ ? {
6693
+ async adopt(nextSession, snapshot, context) {
6694
+ await rehydrateSnapshot('adopt', nextSession, snapshot, context);
6695
+ },
6696
+ }
6697
+ : {}),
5801
6698
  // DR-029 / PBRT-52: side-effect-free control view over the live
5802
6699
  // snapshot, valid at parked quiescence outside an active boundary.
5803
6700
  // The view is detached and frozen; producing it emits nothing and
@@ -5818,9 +6715,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
5818
6715
  const state = currentState();
5819
6716
  const context = (snapshot.context ??
5820
6717
  {});
5821
- const pending = !hasUnresolvedReconciliation()
5822
- ? pendingBossQuestionForState(state, context)
5823
- : undefined;
6718
+ const pendingQuestions = !hasUnresolvedReconciliation()
6719
+ ? pendingBossQuestionsFor(state, context)
6720
+ : [];
5824
6721
  const lastError = state.stateId === 'failed'
5825
6722
  ? failedStateError(context)
5826
6723
  : normalizeErrorFull(context.lastError);
@@ -5834,16 +6731,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
5834
6731
  ...(projectedContext !== undefined
5835
6732
  ? { context: projectedContext }
5836
6733
  : {}),
5837
- pendingQuestions: pending === undefined
5838
- ? []
5839
- : [
5840
- {
5841
- questionId: pending.questionId,
5842
- asker: pending.asker,
5843
- question: pending.question,
5844
- sourceItem: pending.sourceItem,
5845
- },
5846
- ],
6734
+ pendingQuestions: pendingQuestions.map((pending) => ({
6735
+ questionId: pending.questionId,
6736
+ asker: pending.asker,
6737
+ question: pending.question,
6738
+ sourceItem: pending.sourceItem,
6739
+ })),
5847
6740
  ...(lastError !== undefined ? { lastError } : {}),
5848
6741
  actions: deriveControlActions(snapshot).map(({ action }) => action),
5849
6742
  });
@@ -5893,6 +6786,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5893
6786
  const callId = `apply-${++applyCallSequence}`;
5894
6787
  const position = { turnId, callId };
5895
6788
  activeTurnId = turnId;
6789
+ publicBoundarySequence += 1;
5896
6790
  beginAutomaticReplayBoundary();
5897
6791
  activeSignal = signal;
5898
6792
  activeAborts = abortReasonClassifier(signal);
@@ -6064,6 +6958,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6064
6958
  if (receipt !== undefined && receipt.disposition !== 'rejected') {
6065
6959
  appliedReceipts.set(key, receipt);
6066
6960
  }
6961
+ if (parallelProfile !== undefined)
6962
+ await drainBoundaryCalls();
6067
6963
  try {
6068
6964
  await drainEmissions();
6069
6965
  }
@@ -6157,6 +7053,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6157
7053
  refreshRetainedEffectFenceFromHost();
6158
7054
  const turnId = ++turnSequence;
6159
7055
  activeTurnId = turnId;
7056
+ publicBoundarySequence += 1;
6160
7057
  beginAutomaticReplayBoundary();
6161
7058
  activeSignal = signal;
6162
7059
  activeAborts = abortReasonClassifier(signal);
@@ -6196,7 +7093,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6196
7093
  const stateId = normalizePlaybookSnapshot(snapshot).stateId;
6197
7094
  const snapshotContext = (snapshot
6198
7095
  .context ?? {});
6199
- deferredPending = pendingBossQuestionForState(normalizePlaybookSnapshot(snapshot), snapshotContext);
7096
+ deferredPending = singlePendingBossQuestion(normalizePlaybookSnapshot(snapshot), snapshotContext);
6200
7097
  pendingRequiresDeferredBinding =
6201
7098
  deferredPending !== undefined &&
6202
7099
  outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred' &&
@@ -6327,6 +7224,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6327
7224
  catch (error) {
6328
7225
  operationError = error;
6329
7226
  }
7227
+ if (parallelProfile !== undefined)
7228
+ await drainBoundaryCalls();
6330
7229
  let drainError;
6331
7230
  try {
6332
7231
  await drainEmissions();
@@ -6415,6 +7314,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6415
7314
  return runResultFor('no-action');
6416
7315
  }
6417
7316
  activeTurnId = playbookCallTurnIds.get(input.callId);
7317
+ publicBoundarySequence += 1;
6418
7318
  bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
6419
7319
  ? playbookCallEffectPrefixes.get(input.callId)
6420
7320
  : hasGovernedPlayerStates
@@ -6457,6 +7357,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6457
7357
  error: normalizeError(input.signal.reason),
6458
7358
  };
6459
7359
  }
7360
+ if (parallelProfile !== undefined)
7361
+ await drainBoundaryCalls();
6460
7362
  let drainError;
6461
7363
  try {
6462
7364
  await drainEmissions();
@@ -6570,10 +7472,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
6570
7472
  emissionQueue.clear();
6571
7473
  judgeQueue.clear();
6572
7474
  appliedReceipts.clear();
7475
+ pendingCohorts.clear();
7476
+ consumedCohortBoundaries.clear();
7477
+ activeBoundaryCalls.clear();
7478
+ cohortCompletionQueue.clear();
6573
7479
  actor = undefined;
6574
7480
  activeSignal = undefined;
6575
7481
  activeAborts = undefined;
6576
- actorSettlementAborts = undefined;
7482
+ actorSettlementAborts.length = 0;
6577
7483
  actorSettlementErrorAborts = undefined;
6578
7484
  activeAbortEmission = undefined;
6579
7485
  activeTurnId = undefined;