@sublang/playbook 15.1.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +2 -0
  2. package/docs/cli.md +36 -18
  3. package/docs/configuration.md +7 -5
  4. package/docs/embedding.md +28 -7
  5. package/package.json +4 -4
  6. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  7. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  8. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  9. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  10. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  11. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  12. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  13. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  14. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  15. package/reference/sdlc/captain.md +19 -10
  16. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +6 -2
  17. package/reference/sdlc/captain.playbook/captain.fsm.js +19 -7
  18. package/reference/sdlc/captain.playbook/captain.fsm.ts +25 -10
  19. package/reference/sdlc/captain.playbook/captain.gears.md +16 -8
  20. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +1 -1
  21. package/reference/sdlc/captain.playbook/captain.playbook.js +3 -2
  22. package/reference/sdlc/captain.playbook/captain.playbook.ts +4 -3
  23. package/reference/sdlc/code.playbook/bin/interactive-session.js +14 -1
  24. package/reference/sdlc/code.playbook/bin/portable-codec.js +3 -0
  25. package/reference/sdlc/code.playbook/bin/repository-effects.js +1 -0
  26. package/reference/sdlc/code.playbook/bin/run.js +128 -85
  27. package/reference/sdlc/code.playbook/bin/session-host.js +40 -8
  28. package/reference/sdlc/code.playbook/bin/session-store.js +162 -34
  29. package/reference/sdlc/code.playbook/code.fsm.d.ts +146 -93
  30. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  31. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  32. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  33. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  34. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  35. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  36. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  37. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  38. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  39. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  40. package/reference/sdlc/code.playbook/host-capabilities.d.ts +3 -0
  41. package/reference/sdlc/code.playbook/playbook-captain.d.ts +74 -2
  42. package/reference/sdlc/code.playbook/playbook-captain.js +1548 -468
  43. package/reference/sdlc/code.playbook/playbook-captain.ts +1993 -573
  44. package/reference/sdlc/code.playbook/session-host.d.ts +4 -1
  45. package/reference/sdlc/code.playbook/session-store.d.ts +17 -0
  46. package/reference/sdlc/code.playbook/session-store.js +1 -0
  47. package/reference/sdlc/decide.md +1 -0
  48. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +273 -157
  49. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  50. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  51. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  52. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -59
  53. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3696
  54. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5244
  55. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  56. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  57. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  58. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +116 -134
  59. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  60. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  61. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  62. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  63. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  64. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  65. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  66. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  67. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +95 -88
  68. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  69. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  70. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  71. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  72. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  73. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  74. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  75. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  76. package/reference/sdlc/review.md +3 -3
  77. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  78. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  79. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  80. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  81. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  82. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  83. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  84. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  85. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  86. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  87. package/slc/gears2fsm.md +77 -8
  88. package/slc/link.md +127 -60
  89. package/slc/materialize-link.mjs +51 -13
  90. package/slc/optimize.md +4 -1
  91. package/slc/prefix-prompts.mjs +29 -28
  92. package/slc/prefix.md +27 -24
  93. package/slc/slc.pin-inputs.json +11 -2
  94. package/slc/text2gears.md +71 -11
  95. package/src/runtime.d.ts +46 -2
  96. package/src/runtime.ts +42 -2
  97. package/src/xstate-playbook-runtime.d.ts +63 -23
  98. package/src/xstate-playbook-runtime.js +1637 -475
  99. package/src/xstate-playbook-runtime.ts +2105 -532
  100. package/src/xstate-runtime.d.ts +3 -4
  101. package/src/xstate-runtime.js +52 -0
  102. package/src/xstate-runtime.ts +59 -4
@@ -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);
@@ -918,6 +966,7 @@ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
918
966
  '```',
919
967
  '',
920
968
  'Pick exactly one declared `guard` and reply with exactly that outcome\'s reply shape below: `guard` plus its semantic-owned fields and nothing else.',
969
+ 'If no declared outcome matches the player output, instead reply exactly {"blocked":"explain the missing outcome or contradictory requirements"}. Never force a match or invent a guard. A blocked reply pauses the playbook for Boss; it is not completion.',
921
970
  'A field listed as runtime-supplied is owned by presentation, effect, or runtime evidence; the runtime fills it itself, and a reply that includes one is structurally invalid.',
922
971
  '',
923
972
  ];
@@ -997,7 +1046,7 @@ function validateBossReplyOutput(input, output, resumableStateIds) {
997
1046
  }
998
1047
  }
999
1048
  export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onControlPlaneError) {
1000
- return fromPromise(async ({ input, signal }) => {
1049
+ const execute = async ({ input, signal }) => {
1001
1050
  const activeSignal = combineAbortSignals(signal, getActiveSignal?.());
1002
1051
  let roleId;
1003
1052
  let prompt;
@@ -1040,7 +1089,7 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
1040
1089
  }
1041
1090
  if (result.status !== 'ok') {
1042
1091
  const failure = new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
1043
- throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
1092
+ throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
1044
1093
  }
1045
1094
  const finalText = result.finalText ?? '';
1046
1095
  if (isEmptyFinalText(finalText)) {
@@ -1065,7 +1114,9 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
1065
1114
  }
1066
1115
  throw error;
1067
1116
  }
1068
- });
1117
+ };
1118
+ return fromPromise((args) => spec.run
1119
+ ? spec.run(args.input, args.signal, () => execute(args)) : execute(args));
1069
1120
  }
1070
1121
  // ---------------------------------------------------------------------------
1071
1122
  // Direct-Captain adjudication (slc/link.md §Captain adjudication). The judge
@@ -1220,20 +1271,293 @@ function transitionTargets(transition) {
1220
1271
  }
1221
1272
  return targets;
1222
1273
  }
1223
- /** Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions. */
1274
+ function transitionArmTargets(transition) {
1275
+ const arms = Array.isArray(transition) ? transition : [transition];
1276
+ const targets = [];
1277
+ for (const arm of arms) {
1278
+ if (typeof arm === 'string') {
1279
+ targets.push(arm);
1280
+ }
1281
+ else if (isPlainObject(arm) && typeof arm.target === 'string') {
1282
+ targets.push(arm.target);
1283
+ }
1284
+ }
1285
+ return targets;
1286
+ }
1287
+ function playbookStateIdOf(stateDef) {
1288
+ if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
1289
+ return undefined;
1290
+ }
1291
+ const playbook = stateDef.meta.playbook;
1292
+ return isPlainObject(playbook) && typeof playbook.stateId === 'string'
1293
+ ? playbook.stateId
1294
+ : undefined;
1295
+ }
1296
+ function stateTagsOf(stateDef) {
1297
+ const tags = stateDef.tags;
1298
+ if (typeof tags === 'string')
1299
+ return [tags];
1300
+ return Array.isArray(tags)
1301
+ ? tags.filter((tag) => typeof tag === 'string')
1302
+ : [];
1303
+ }
1304
+ /** Every explicit node `id` in the config, mapped to its playbook state id. */
1305
+ function playbookStateIdsByNodeId(config) {
1306
+ const byNodeId = new Map();
1307
+ const visit = (stateDef) => {
1308
+ if (!isPlainObject(stateDef))
1309
+ return;
1310
+ const stateId = playbookStateIdOf(stateDef);
1311
+ if (typeof stateDef.id === 'string' && stateId !== undefined) {
1312
+ byNodeId.set(stateDef.id, stateId);
1313
+ }
1314
+ if (isPlainObject(stateDef.states)) {
1315
+ for (const child of Object.values(stateDef.states))
1316
+ visit(child);
1317
+ }
1318
+ };
1319
+ if (isPlainObject(config) && isPlainObject(config.states)) {
1320
+ for (const child of Object.values(config.states))
1321
+ visit(child);
1322
+ }
1323
+ return byNodeId;
1324
+ }
1325
+ /**
1326
+ * One transition target written inside a parallel region, resolved to the
1327
+ * playbook state id it names: an `#id` reference through the node ids, and a
1328
+ * bare sibling key through the region's own leaves.
1329
+ */
1330
+ function resolveRegionTarget(target, regionStates, byNodeId) {
1331
+ if (target.startsWith('#')) {
1332
+ const nodeId = target.slice(1);
1333
+ return byNodeId.get(nodeId) ?? nodeId;
1334
+ }
1335
+ if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
1336
+ return playbookStateIdOf(regionStates[target]) ?? target;
1337
+ }
1338
+ return target;
1339
+ }
1340
+ /**
1341
+ * Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
1342
+ * the parallel shape gears2fsm compiles (DR-067) — the targets of each
1343
+ * region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
1344
+ */
1224
1345
  export function resumableStateIdsFromMachine(machine) {
1225
1346
  const config = machine.config;
1226
1347
  if (!isPlainObject(config) || !isPlainObject(config.states)) {
1227
1348
  return new Set();
1228
1349
  }
1350
+ const resumable = new Set();
1229
1351
  const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
1230
- if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
1231
- return new Set();
1352
+ if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
1353
+ const bossReply = awaitState.on.BOSS_REPLY;
1354
+ if (bossReply !== undefined) {
1355
+ for (const target of transitionTargets(bossReply))
1356
+ resumable.add(target);
1357
+ }
1232
1358
  }
1233
- const bossReply = awaitState.on.BOSS_REPLY;
1234
- if (bossReply === undefined)
1235
- return new Set();
1236
- return new Set(transitionTargets(bossReply));
1359
+ const byNodeId = playbookStateIdsByNodeId(config);
1360
+ for (const stateDef of Object.values(config.states)) {
1361
+ if (!isPlainObject(stateDef) ||
1362
+ stateDef.type !== 'parallel' ||
1363
+ !isPlainObject(stateDef.states)) {
1364
+ continue;
1365
+ }
1366
+ for (const region of Object.values(stateDef.states)) {
1367
+ if (!isPlainObject(region) || !isPlainObject(region.states))
1368
+ continue;
1369
+ const regionStates = region.states;
1370
+ for (const leaf of Object.values(regionStates)) {
1371
+ if (!isPlainObject(leaf) || !isPlainObject(leaf.on))
1372
+ continue;
1373
+ const bossReply = leaf.on.BOSS_REPLY;
1374
+ if (bossReply === undefined)
1375
+ continue;
1376
+ for (const target of transitionArmTargets(bossReply)) {
1377
+ resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
1378
+ }
1379
+ }
1380
+ }
1381
+ }
1382
+ return resumable;
1383
+ }
1384
+ function malformedParallelState(label, key, reason) {
1385
+ return new Error(`${label} parallel state ${key} is not the compiled parallel shape: ${reason}`);
1386
+ }
1387
+ /**
1388
+ * DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
1389
+ * `type: 'parallel'` states of the one shape gears2fsm compiles — each
1390
+ * region a compound state with exactly one player-invoking `playbook.busy`
1391
+ * working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
1392
+ * arms resume that working leaf, and exactly one final leaf, every leaf
1393
+ * carrying a stable `meta.playbook.stateId` distinct from every other
1394
+ * state's, and the parent's `onDone` the join. Any other compound state
1395
+ * keeps the flat-domain rejection. Returns `undefined` for a flat machine.
1396
+ */
1397
+ function parallelProfileFromMachine(machine, label) {
1398
+ const config = machine.config;
1399
+ if (!isPlainObject(config) || !isPlainObject(config.states)) {
1400
+ return undefined;
1401
+ }
1402
+ const rootStates = config.states;
1403
+ const parallelKeys = Object.entries(rootStates)
1404
+ .filter(([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel')
1405
+ .map(([key]) => key);
1406
+ for (const stateDef of Object.values(rootStates)) {
1407
+ if (isPlainObject(stateDef) &&
1408
+ stateDef.type !== 'parallel' &&
1409
+ isPlainObject(stateDef.states) &&
1410
+ Object.keys(stateDef.states).length > 0) {
1411
+ throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
1412
+ }
1413
+ }
1414
+ if (parallelKeys.length === 0)
1415
+ return undefined;
1416
+ const byNodeId = playbookStateIdsByNodeId(config);
1417
+ const seenStateIds = new Set();
1418
+ for (const stateDef of Object.values(rootStates)) {
1419
+ const stateId = playbookStateIdOf(stateDef);
1420
+ if (stateId !== undefined)
1421
+ seenStateIds.add(stateId);
1422
+ }
1423
+ const states = [];
1424
+ const parallelStateByWorkingStateId = new Map();
1425
+ const waitResumeTargets = new Map();
1426
+ const transitionNodesByStateId = new Map();
1427
+ for (const key of parallelKeys) {
1428
+ const parent = rootStates[key];
1429
+ if (parent.onDone === undefined) {
1430
+ throw malformedParallelState(label, key, 'it declares no onDone join');
1431
+ }
1432
+ if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
1433
+ throw malformedParallelState(label, key, 'it must declare at least two regions');
1434
+ }
1435
+ const regions = [];
1436
+ const roleIds = new Set();
1437
+ for (const [regionKey, region] of Object.entries(parent.states)) {
1438
+ const regionReason = (reason) => malformedParallelState(label, key, `region ${regionKey} ${reason}`);
1439
+ if (!isPlainObject(region) ||
1440
+ region.type === 'parallel' ||
1441
+ region.type === 'final' ||
1442
+ region.type === 'history' ||
1443
+ !isPlainObject(region.states)) {
1444
+ throw regionReason('must be a compound state of leaves');
1445
+ }
1446
+ const regionStates = region.states;
1447
+ let working;
1448
+ let wait;
1449
+ let final;
1450
+ for (const [leafKey, leaf] of Object.entries(regionStates)) {
1451
+ if (!isPlainObject(leaf)) {
1452
+ throw regionReason(`leaf ${leafKey} must be a state object`);
1453
+ }
1454
+ if ((isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
1455
+ leaf.type === 'parallel' ||
1456
+ leaf.type === 'history') {
1457
+ throw regionReason(`leaf ${leafKey} must be an atomic state`);
1458
+ }
1459
+ const leafStateId = playbookStateIdOf(leaf);
1460
+ if (leafStateId === undefined || leafStateId.trim().length === 0) {
1461
+ throw regionReason(`leaf ${leafKey} must carry a string meta.playbook.stateId`);
1462
+ }
1463
+ if (seenStateIds.has(leafStateId)) {
1464
+ throw regionReason(`leaf ${leafKey} reuses playbook state id ${leafStateId}`);
1465
+ }
1466
+ seenStateIds.add(leafStateId);
1467
+ const invoke = leaf.invoke;
1468
+ const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
1469
+ const tags = stateTagsOf(leaf);
1470
+ if (leaf.type === 'final') {
1471
+ if (final !== undefined)
1472
+ throw regionReason('declares two final leaves');
1473
+ final = [leafKey, leaf];
1474
+ }
1475
+ else if (invokes.length === 1 &&
1476
+ isPlainObject(invokes[0]) &&
1477
+ invokes[0].src === 'player' &&
1478
+ tags.includes('playbook.busy')) {
1479
+ if (working !== undefined) {
1480
+ throw regionReason('declares two working leaves');
1481
+ }
1482
+ working = [leafKey, leaf];
1483
+ }
1484
+ else if (invokes.length === 0 &&
1485
+ tags.includes('playbook.parked') &&
1486
+ isPlainObject(leaf.on) &&
1487
+ leaf.on.BOSS_REPLY !== undefined) {
1488
+ if (wait !== undefined)
1489
+ throw regionReason('declares two wait leaves');
1490
+ wait = [leafKey, leaf];
1491
+ }
1492
+ else {
1493
+ throw regionReason(`leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`);
1494
+ }
1495
+ }
1496
+ if (working === undefined || wait === undefined || final === undefined) {
1497
+ throw regionReason('must hold exactly one working leaf, one wait leaf, and one final leaf');
1498
+ }
1499
+ if (region.initial !== working[0]) {
1500
+ throw regionReason('must start in its working leaf');
1501
+ }
1502
+ const workingStateId = playbookStateIdOf(working[1]);
1503
+ const waitStateId = playbookStateIdOf(wait[1]);
1504
+ const finalStateId = playbookStateIdOf(final[1]);
1505
+ const replyTargets = transitionArmTargets(wait[1].on.BOSS_REPLY).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
1506
+ const regionStateIds = new Set([waitStateId, finalStateId]);
1507
+ if (!replyTargets.includes(workingStateId) ||
1508
+ replyTargets.some((target) => regionStateIds.has(target))) {
1509
+ throw regionReason('wait leaf BOSS_REPLY arms must resume its working leaf');
1510
+ }
1511
+ const workingMeta = working[1].meta
1512
+ .playbook;
1513
+ const roleId = workingMeta.role;
1514
+ if (typeof roleId !== 'string' || roleId.trim().length === 0) {
1515
+ throw regionReason('working leaf must declare meta.playbook.role');
1516
+ }
1517
+ if (roleIds.has(roleId)) {
1518
+ throw malformedParallelState(label, key, `role ${roleId} works in more than one region`);
1519
+ }
1520
+ roleIds.add(roleId);
1521
+ regions.push({
1522
+ key: regionKey,
1523
+ roleId,
1524
+ workingStateId,
1525
+ waitStateId,
1526
+ finalStateId,
1527
+ });
1528
+ waitResumeTargets.set(waitStateId, new Set([workingStateId]));
1529
+ for (const [, leaf] of [working, wait, final]) {
1530
+ transitionNodesByStateId.set(playbookStateIdOf(leaf), [region, leaf]);
1531
+ }
1532
+ }
1533
+ const parallelState = {
1534
+ stateId: key,
1535
+ regions: Object.freeze(regions),
1536
+ };
1537
+ states.push(parallelState);
1538
+ for (const region of regions) {
1539
+ parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
1540
+ }
1541
+ transitionNodesByStateId.set(key, [parent]);
1542
+ }
1543
+ for (const [key, stateDef] of Object.entries(rootStates)) {
1544
+ if (!isPlainObject(stateDef) || parallelKeys.includes(key))
1545
+ continue;
1546
+ transitionNodesByStateId.set(key, [stateDef]);
1547
+ }
1548
+ const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
1549
+ if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
1550
+ const bossReply = rootWait.on.BOSS_REPLY;
1551
+ if (bossReply !== undefined) {
1552
+ waitResumeTargets.set(BOSS_REPLY_WAIT_STATE_ID, new Set(transitionTargets(bossReply)));
1553
+ }
1554
+ }
1555
+ return Object.freeze({
1556
+ states: Object.freeze(states),
1557
+ parallelStateByWorkingStateId,
1558
+ waitResumeTargets,
1559
+ transitionNodesByStateId,
1560
+ });
1237
1561
  }
1238
1562
  // ---------------------------------------------------------------------------
1239
1563
  // DR-029 control surface: the FSM's explicit-state-jump event and the
@@ -1654,10 +1978,11 @@ function makeDefaultStatusesForState(roleStates) {
1654
1978
  }
1655
1979
  return [
1656
1980
  ...statuses,
1657
- { message: `${askerLabel(pending.asker)} asks: ${pending.question}` },
1981
+ { message: `${askerLabel(pending.asker)} asks: ${pending.question}`, data: { kind: 'boss-question' } },
1658
1982
  {
1659
1983
  message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
1660
1984
  `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
1985
+ data: { kind: 'boss-question' },
1661
1986
  },
1662
1987
  ];
1663
1988
  }
@@ -1840,7 +2165,48 @@ function eventContractPrompt(contract) {
1840
2165
  ]);
1841
2166
  return `{ ${members.join(', ')} }${notes.length === 0 ? '' : ` (${notes.join('; ')})`}`;
1842
2167
  }
1843
- function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
2168
+ /**
2169
+ * DR-067 §1: the questions pending in a parallel-declaring machine — the
2170
+ * keyed records whose resume state an active wait state (a region wait leaf
2171
+ * or the root reply wait) resumes.
2172
+ */
2173
+ function parallelPendingBossQuestions(profile, activeStateIds, context) {
2174
+ return keyedPendingBossQuestions(context, (resumeStateId) => activeStateIds.some((stateId) => profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true));
2175
+ }
2176
+ /** Event types the root and every active state node of a parallel machine configure. */
2177
+ function parallelConfiguredEventTypes(machine, profile, activeStateIds) {
2178
+ const configured = new Set();
2179
+ const config = machine.config;
2180
+ if (!isPlainObject(config))
2181
+ return configured;
2182
+ if (isPlainObject(config.on)) {
2183
+ for (const type of Object.keys(config.on))
2184
+ configured.add(type);
2185
+ }
2186
+ for (const stateId of activeStateIds) {
2187
+ for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
2188
+ if (!isPlainObject(node.on))
2189
+ continue;
2190
+ for (const type of Object.keys(node.on))
2191
+ configured.add(type);
2192
+ }
2193
+ }
2194
+ return configured;
2195
+ }
2196
+ function activeStateIdsOf(snapshotOrState) {
2197
+ const listed = snapshotOrState
2198
+ ?.activeStateIds;
2199
+ if (Array.isArray(listed)) {
2200
+ return listed.filter((stateId) => typeof stateId === 'string');
2201
+ }
2202
+ try {
2203
+ return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
2204
+ }
2205
+ catch {
2206
+ return [];
2207
+ }
2208
+ }
2209
+ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents, parallelProfile) {
1844
2210
  const contracts = defaultBossEventSpecs(machine, entryEvent, bossEvents);
1845
2211
  return async (text, ports, signal, snapshotOrState, boundary) => {
1846
2212
  const trimmed = text.trim();
@@ -1854,12 +2220,41 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1854
2220
  // is answered history, so the prompt must not present it as pending —
1855
2221
  // a judge told a question awaits at the failure state is steered toward
1856
2222
  // 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));
2223
+ let pendingQuestions;
2224
+ let configuredTypes;
2225
+ if (parallelProfile === undefined) {
2226
+ const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
2227
+ ? pendingBossQuestionFromContext(state.context)
2228
+ : undefined;
2229
+ pendingQuestions = pending === undefined ? [] : [pending];
2230
+ configuredTypes = configuredEventTypesForState(machine, stateId);
2231
+ }
2232
+ else {
2233
+ // DR-067 §1: every question pending in any active wait is offered, and
2234
+ // the events of every active state node apply.
2235
+ const activeStateIds = activeStateIdsOf(snapshotOrState);
2236
+ pendingQuestions = parallelPendingBossQuestions(parallelProfile, activeStateIds, state.context);
2237
+ configuredTypes = parallelConfiguredEventTypes(machine, parallelProfile, activeStateIds);
2238
+ }
2239
+ // Several pending questions: a reply must name the one it answers, and
2240
+ // the classifier never guesses among them (gears2fsm §Parallel groups).
2241
+ const severalPending = pendingQuestions.length > 1;
2242
+ const applicable = [...contracts.values()]
2243
+ .filter((contract) => configuredTypes.has(contract.type) &&
2244
+ (contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0))
2245
+ .map((contract) => contract.type === 'BOSS_REPLY' && severalPending
2246
+ ? {
2247
+ type: 'BOSS_REPLY',
2248
+ fields: {
2249
+ questionId: {
2250
+ source: 'judge',
2251
+ required: true,
2252
+ values: pendingQuestions.map(({ questionId }) => questionId),
2253
+ },
2254
+ answer: { source: 'text', required: true },
2255
+ },
2256
+ }
2257
+ : contract);
1863
2258
  const lines = [
1864
2259
  'Classify the following Boss message into exactly one event.',
1865
2260
  'Respond with one exact flat JSON object. Do not add fields that are not shown.',
@@ -1867,9 +2262,12 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1867
2262
  '',
1868
2263
  `Current state: ${currentState}`,
1869
2264
  ];
1870
- if (pending !== undefined) {
2265
+ for (const pending of pendingQuestions) {
1871
2266
  lines.push(`Pending question id: ${pending.questionId}`, `Pending asker: ${askerLabel(pending.asker)}`, `Pending Boss question: ${pending.question}`);
1872
2267
  }
2268
+ if (severalPending) {
2269
+ lines.push('Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.');
2270
+ }
1873
2271
  lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
1874
2272
  for (const contract of applicable) {
1875
2273
  lines.push(`- ${eventContractPrompt(contract)}`);
@@ -1940,16 +2338,19 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1940
2338
  event[field] = value;
1941
2339
  }
1942
2340
  if (eventType === 'BOSS_REPLY') {
1943
- if (pending === undefined) {
2341
+ if (pendingQuestions.length === 0) {
1944
2342
  await ports.emitStatus('Classifier returned BOSS_REPLY without a pending question');
1945
2343
  return undefined;
1946
2344
  }
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;
2345
+ if (!severalPending) {
2346
+ const pending = pendingQuestions[0];
2347
+ const questionId = event.questionId;
2348
+ if (questionId !== undefined && questionId !== pending.questionId) {
2349
+ await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`);
2350
+ return undefined;
2351
+ }
2352
+ event.questionId = pending.questionId;
1951
2353
  }
1952
- event.questionId = pending.questionId;
1953
2354
  }
1954
2355
  const can = snapshotOrState?.can;
1955
2356
  if (typeof can === 'function' &&
@@ -1960,33 +2361,6 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1960
2361
  return event;
1961
2362
  };
1962
2363
  }
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
2364
  // PBRT-52: the factory's lookups index states by their root key, and the
1991
2365
  // published playbook identity is `meta.playbook.stateId` — the two must
1992
2366
  // coincide or a machine can advertise a pending question or retry under an
@@ -2065,12 +2439,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2065
2439
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
2066
2440
  throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
2067
2441
  }
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
- }
2442
+ // DR-067 §1: a flat machine, or one whose only compound states are root
2443
+ // parallel states of the compiled shape; everything else is rejected.
2444
+ const parallelProfile = parallelProfileFromMachine(machine, label);
2074
2445
  assertFlatStateIdentity(machine, label);
2075
2446
  assertUnfinishedFinalStateIds(spec.unfinishedFinalStateIds, machine, label);
2076
2447
  const retainedGenerationMetadata = spec.unfinishedFinalStateIds === undefined
@@ -2098,6 +2469,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
2098
2469
  const declaredRoleIds = Object.freeze([
2099
2470
  ...new Set([...roleStates.values()].map(({ role }) => role)),
2100
2471
  ]);
2472
+ // DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
2473
+ // each player state, each wait state, and the failure state. A status is
2474
+ // scheduled for each one newly entered, and a status trace names a
2475
+ // `stateId` only while exactly one is active.
2476
+ const bossRelevantStateIds = parallelProfile === undefined
2477
+ ? undefined
2478
+ : new Set([
2479
+ ...roleStates.keys(),
2480
+ ...parallelProfile.waitResumeTargets.keys(),
2481
+ 'failed',
2482
+ ]);
2101
2483
  // PBRT-52: the artifact's own ControlView context projection. Nothing is
2102
2484
  // exported by default, so an FSM context member — including one added
2103
2485
  // after this artifact was linked — is private until named here. The two
@@ -2136,7 +2518,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2136
2518
  // supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
2137
2519
  // fail factory construction whether or not this spec overrides the
2138
2520
  // classifier that would have consumed the contracts.
2139
- const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? []);
2521
+ const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? [], parallelProfile);
2140
2522
  const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
2141
2523
  const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
2142
2524
  makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
@@ -2151,6 +2533,95 @@ export function createXStatePlaybookRuntime(machine, spec) {
2151
2533
  const cwd = options?.cwd;
2152
2534
  return typeof cwd === 'string' ? cwd : undefined;
2153
2535
  });
2536
+ // DR-067 §1: the questions pending in a state — the singular reply-wait
2537
+ // question of a flat machine, or a parallel machine's keyed questions
2538
+ // whose own wait state is active.
2539
+ const pendingBossQuestionsFor = (state, context) => {
2540
+ if (parallelProfile === undefined) {
2541
+ const pending = pendingBossQuestionForState(state, context);
2542
+ return pending === undefined ? [] : [pending];
2543
+ }
2544
+ return parallelPendingBossQuestions(parallelProfile, state.activeStateIds, context);
2545
+ };
2546
+ const singlePendingBossQuestion = (state, context) => {
2547
+ const pending = pendingBossQuestionsFor(state, context);
2548
+ return pending.length === 1 ? pending[0] : undefined;
2549
+ };
2550
+ // DR-067 §1: a status trace names a `stateId` only while exactly one
2551
+ // Boss-relevant state is active; a flat machine's is its one state.
2552
+ const statusTraceStateId = (state) => {
2553
+ if (bossRelevantStateIds === undefined)
2554
+ return state.stateId;
2555
+ const relevant = state.activeStateIds.filter((stateId) => bossRelevantStateIds.has(stateId));
2556
+ return relevant.length === 1 ? relevant[0] : undefined;
2557
+ };
2558
+ // DR-067 §1: a parallel machine schedules statuses for each newly entered
2559
+ // Boss-relevant state — a player state's role line, a wait state's
2560
+ // question lines, and the failure line — diffed against the previous
2561
+ // snapshot's active state ids, so a sibling still running is not
2562
+ // announced again.
2563
+ const parallelStatusesFor = (previousState, state, context) => {
2564
+ const prior = new Set(previousState?.activeStateIds ?? []);
2565
+ const statuses = [];
2566
+ for (const stateId of state.activeStateIds) {
2567
+ if (prior.has(stateId) || !bossRelevantStateIds.has(stateId))
2568
+ continue;
2569
+ const resumeTargets = parallelProfile.waitResumeTargets.get(stateId);
2570
+ if (resumeTargets !== undefined) {
2571
+ const questions = keyedPendingBossQuestions(context, (resumeStateId) => resumeTargets.has(resumeStateId));
2572
+ if (questions.length === 0) {
2573
+ statuses.push({ message: 'Awaiting Boss reply.' });
2574
+ continue;
2575
+ }
2576
+ for (const pending of questions) {
2577
+ statuses.push({
2578
+ message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
2579
+ data: { kind: 'boss-question' },
2580
+ }, {
2581
+ message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
2582
+ `${askerLabel(pending.asker)} · ${pending.sourceItem}`,
2583
+ data: { kind: 'boss-question' },
2584
+ });
2585
+ }
2586
+ continue;
2587
+ }
2588
+ if (stateId === 'failed') {
2589
+ const lastError = normalizeErrorCompact(context.lastError);
2590
+ statuses.push({
2591
+ message: '◆ workflow failed; awaiting Boss recovery.',
2592
+ ...(lastError === undefined
2593
+ ? {}
2594
+ : {
2595
+ data: snapshotJsonValue({ lastError }, 'failed status data'),
2596
+ }),
2597
+ });
2598
+ continue;
2599
+ }
2600
+ const roleState = roleStates.get(stateId);
2601
+ if (roleState !== undefined) {
2602
+ statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
2603
+ }
2604
+ }
2605
+ return statuses;
2606
+ };
2607
+ // DR-067 §1: each parallel state's region roles, in region order, are one
2608
+ // concurrent role set the FSM exports; a host that forwards those sets in
2609
+ // its schema-3 authority is held to them before any agent call.
2610
+ const assertDeclaredCohorts = (hostCapabilities) => {
2611
+ const authority = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority')?.value;
2612
+ if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
2613
+ return;
2614
+ }
2615
+ const declared = authority.concurrentRoleSets;
2616
+ for (const parallelState of parallelProfile.states) {
2617
+ const roles = parallelState.regions.map(({ roleId }) => roleId);
2618
+ if (!declared.some((candidate) => Array.isArray(candidate) &&
2619
+ candidate.length === roles.length &&
2620
+ candidate.every((roleId, index) => roleId === roles[index]))) {
2621
+ throw new TypeError(`${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`);
2622
+ }
2623
+ }
2624
+ };
2154
2625
  const createPlaybookRuntime = function createPlaybookRuntime(factoryOptions) {
2155
2626
  const construction = configuredOptionsFromFactoryInput(factoryOptions, label);
2156
2627
  const configuredOptions = construction.configuredOptions;
@@ -2166,10 +2637,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
2166
2637
  Object.prototype.hasOwnProperty.call(declarations, acceptedOutcome));
2167
2638
  });
2168
2639
  const repositoryCapability = hasGovernedPlayerStates
2169
- ? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label)
2640
+ ? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label, parallelProfile !== undefined)
2170
2641
  : undefined;
2642
+ if (parallelProfile !== undefined) {
2643
+ assertDeclaredCohorts(construction.hostCapabilities);
2644
+ }
2171
2645
  const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
2172
2646
  let effectLedgerMirror = currentEffectLedger();
2647
+ let recoveryCheckpoint;
2173
2648
  let retainedEffectSourceSessionId;
2174
2649
  let retainedEffectReconciliation;
2175
2650
  let retainedEffectReconciliationRequired = false;
@@ -2202,9 +2677,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2202
2677
  // mutable `activeSignal` alone cannot classify a late invocation reason.
2203
2678
  let activeAborts;
2204
2679
  // 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;
2680
+ // its promise actor settles. The next root snapshot/error consumes it so
2681
+ // background settlement emissions retain their owner. A flat machine has
2682
+ // one settling actor at a time, so a later binding replaces an unconsumed
2683
+ // one; a parallel machine's settlements queue in order (DR-067 §1).
2684
+ const actorSettlementAborts = [];
2208
2685
  let actorSettlementErrorAborts;
2209
2686
  // Exact cancellation observed by an emission owned by the active
2210
2687
  // boundary. Ordinary runs settle from their signal/state; apply also
@@ -2235,11 +2712,29 @@ export function createXStatePlaybookRuntime(machine, spec) {
2235
2712
  // each successful result, so it is never stale.
2236
2713
  let pendingResultFailureCause;
2237
2714
  // 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
2715
+ // marker — a string, a frozen error — and for the record a machine keeps
2716
+ // in place of the thrown value, kept by identity for as long as the
2239
2717
  // 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.
2718
+ // publishes the cause decided for exactly that failure, whatever turn
2719
+ // reads it.
2242
2720
  const failureCauses = createFailureCauseRetention();
2721
+ // DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
2722
+ // result, keyed by the result itself, so concurrent region calls never
2723
+ // lend one another the single slot above.
2724
+ const resultFailureCauses = new WeakMap();
2725
+ // DR-067 §1: the parallel profile's cohort bookkeeping. Every public
2726
+ // boundary advances the sequence; a parallel state's working leaves run
2727
+ // as one cohort at most once per boundary, and each cohort's semantic
2728
+ // completions — which the host settles concurrently — reconcile one at a
2729
+ // time so their durable correction-budget writes cannot race.
2730
+ let publicBoundarySequence = 0;
2731
+ const consumedCohortBoundaries = new Map();
2732
+ const pendingCohorts = new Map();
2733
+ const cohortCompletionQueue = new PQueue({ concurrency: 1 });
2734
+ // DR-067 §1: every player, judge, and nested call a parallel machine
2735
+ // starts, so a boundary's drain waits for a cancelled sibling whose port
2736
+ // ignores its signal before it settles.
2737
+ const activeBoundaryCalls = new Set();
2243
2738
  // Previous root-machine state for the inspect-driven telemetry /
2244
2739
  // status emitter. undefined before the first inspect firing.
2245
2740
  let priorState;
@@ -2251,12 +2746,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
2251
2746
  let playbookCallSequence = 0;
2252
2747
  let captainCallSequence = 0;
2253
2748
  let applyCallSequence = 0;
2254
- // DR-029: the last event a public Boss boundary sent into the
2255
- // machine — classified, deterministic entry, or Boss reply — kept with
2256
- // its recorded payload so a failure-state retry action can replay the
2257
- // event that drove the run into `failed`. Process-local: the durable
2258
- // runtime snapshot does not persist it (PBRT-50).
2259
- let lastBossEvent;
2260
2749
  // DR-029: process-local at-most-once `apply` execution — the accepted receipt
2261
2750
  // recorded for each idempotency key, returned verbatim on a repeated
2262
2751
  // key. A key whose call settled `rejected` or threw before reaching
@@ -2431,7 +2920,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2431
2920
  // cumulative receipt from its original baseline, not by its own step.
2432
2921
  function boundaryExcludesEffect(boundary) {
2433
2922
  return (boundary.logicalOperationId === undefined &&
2434
- boundary.physicalReceipt?.classification === 'unchanged');
2923
+ (boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined));
2435
2924
  }
2436
2925
  function boundaryNeedsSemanticReconciliation(candidate, ledger) {
2437
2926
  if (!runtimeBoundaryIsOwned(candidate))
@@ -2486,7 +2975,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2486
2975
  const candidate = reconstructedGovernedPrefixSequence === undefined
2487
2976
  ? owned.at(-1)
2488
2977
  : owned.find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
2489
- if (candidate === undefined || candidate.sourceStateId !== state.stateId) {
2978
+ if (candidate === undefined || candidate.restored !== undefined || candidate.sourceStateId !== state.stateId) {
2490
2979
  return;
2491
2980
  }
2492
2981
  const persisted = persistedBoundaryReconciliation(candidate, ledger);
@@ -3015,7 +3504,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3015
3504
  ...(descriptor !== undefined
3016
3505
  ? {
3017
3506
  state: descriptor,
3018
- ...stateIdentity(descriptor.stateId),
3507
+ ...stateIdentity(statusTraceStateId(descriptor)),
3019
3508
  }
3020
3509
  : {}),
3021
3510
  }, activeTurnId !== undefined ? { turnId: activeTurnId } : {});
@@ -3048,9 +3537,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
3048
3537
  // its finish carries the receipt disposition and none of the
3049
3538
  // start-only fields, so it passes its own canonical pre-acceptance
3050
3539
  // base (slc/link.md §Playbook trace).
3051
- finishIdentity = identity) {
3540
+ finishIdentity = identity,
3541
+ // DR-067 §1: the per-call abort classification a parallel machine's
3542
+ // player pairs enqueue under; absent, the active boundary's.
3543
+ aborts) {
3052
3544
  try {
3053
- await emitTrace(startedType, identity, position);
3545
+ await emitTrace(startedType, identity, position, aborts);
3054
3546
  }
3055
3547
  catch (error) {
3056
3548
  if (!isAbortFailure(error, signal))
@@ -3060,7 +3552,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3060
3552
  ...finishIdentity,
3061
3553
  status: isAbortFailure(error, signal) ? 'aborted' : 'error',
3062
3554
  error: normalizeError(error),
3063
- }, position);
3555
+ }, position, aborts);
3064
3556
  }
3065
3557
  catch {
3066
3558
  // Preserve the start failure after one best-effort finish attempt.
@@ -3199,11 +3691,32 @@ export function createXStatePlaybookRuntime(machine, spec) {
3199
3691
  ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
3200
3692
  correctionBudget: { limit: 1, spent: true },
3201
3693
  };
3694
+ // DR-067 §1 / PBRT-69: a cohort's shared receipt becomes visible for
3695
+ // every member in one ledger revision — publishing only this member's
3696
+ // spend would leave the invalid half-complete cohort the ledger admits
3697
+ // nowhere.
3698
+ const cohortMembers = current.cohortId === undefined
3699
+ ? [current]
3700
+ : currentLedger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
3701
+ const replacements = cohortMembers.map((member) => {
3702
+ if (member.boundaryId === current.boundaryId) {
3703
+ return { expected: current, next };
3704
+ }
3705
+ if (member.physicalReceipt !== undefined &&
3706
+ !isDeepStrictEqual(member.physicalReceipt, receipt)) {
3707
+ throw new TypeError(`${label} correction budget cohort conflicts with its shared repository receipt`);
3708
+ }
3709
+ return {
3710
+ expected: member,
3711
+ next: {
3712
+ ...member,
3713
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
3714
+ physicalReceipt: receipt,
3715
+ },
3716
+ };
3717
+ });
3202
3718
  const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
3203
- {
3204
- kind: 'replace-boundaries',
3205
- replacements: [{ expected: current, next }],
3206
- },
3719
+ { kind: 'replace-boundaries', replacements },
3207
3720
  ]), `${label} semantic correction budget acknowledgement`);
3208
3721
  effectLedgerMirror = acknowledged;
3209
3722
  refreshRetainedEffectReconciliation(acknowledged);
@@ -3508,7 +4021,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3508
4021
  governedOutput = undefined;
3509
4022
  }
3510
4023
  }
3511
- else if (source === 'runExclusive' && value.deferredStatus !== undefined) {
4024
+ else if (source !== 'runDeferred' && value.deferredStatus !== undefined) {
3512
4025
  throw new TypeError(`${label} non-deferred settlement returned a deferred binding status`);
3513
4026
  }
3514
4027
  if (governedSettlement !== undefined) {
@@ -3578,47 +4091,397 @@ export function createXStatePlaybookRuntime(machine, spec) {
3578
4091
  return (boundaries.length > 0 &&
3579
4092
  boundaries.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged'));
3580
4093
  }
3581
- function captureEffectLedgerPrefixSequence() {
3582
- if (!hasGovernedPlayerStates)
3583
- return undefined;
4094
+ let acceptedStepResult;
4095
+ let requiredRecoveryBaseline;
4096
+ function captureRecoveryCheckpoint(stateId, prompt, boundaryPrefix) {
4097
+ if (requiredRecoveryBaseline !== undefined)
4098
+ return;
4099
+ // Capture after the entry emissions drain: the invocation is now a
4100
+ // real active child in XState's persisted snapshot, before host work.
4101
+ const id = recoveryCheckpoint?.stateId === stateId ? recoveryCheckpoint.id : undefined;
4102
+ recoveryCheckpoint = undefined;
4103
+ if (!actor || currentState().stateId !== stateId)
4104
+ return;
3584
4105
  try {
3585
4106
  const current = currentEffectLedger();
3586
- effectLedgerMirror = current;
3587
- refreshRetainedEffectReconciliation(current);
3588
- return current.boundaries.at(-1)?.sequence ?? 0;
4107
+ recoveryCheckpoint = deepFreeze({
4108
+ stateId,
4109
+ prompt,
4110
+ ...(id === undefined ? {} : { id }),
4111
+ machine: detachPersistedMachineSnapshot(actor.getPersistedSnapshot()),
4112
+ boundaryPrefix: boundaryPrefix ?? current.boundaries.length,
4113
+ });
3589
4114
  }
3590
4115
  catch {
3591
- return undefined;
4116
+ // An in-memory-only context is still executable; it simply cannot
4117
+ // promise a durable step retry. Keep the legacy control behavior.
3592
4118
  }
3593
4119
  }
3594
- function bindAutomaticReplayBoundary(prefixSequence) {
3595
- activeGovernedBoundarySeen = false;
3596
- activeGovernedAttemptId = undefined;
3597
- activeEffectLedgerPrefixSequence = prefixSequence;
4120
+ function stepReplayIsSafe() {
4121
+ if (!recoveryCheckpoint || hasUnresolvedReconciliation())
4122
+ return false;
4123
+ const current = currentEffectLedger();
4124
+ return current.boundaries
4125
+ .slice(recoveryCheckpoint.boundaryPrefix)
4126
+ .every((boundary) => runtimeBoundaryIsOwned(boundary) &&
4127
+ boundary.logicalOperationId === undefined &&
4128
+ (boundary.physicalReceipt?.classification === 'unchanged' || boundary.restored !== undefined));
4129
+ }
4130
+ function repairableReadOnlyBoundary() {
4131
+ if (!recoveryCheckpoint || !repositoryCapability?.observe || !repositoryCapability.acquire ||
4132
+ retainedEffectReconciliationRequired || deferredReconciliationOperationId !== undefined)
4133
+ return undefined;
4134
+ const suffix = currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix);
4135
+ if (suffix.length !== 1)
4136
+ return undefined;
4137
+ const saved = suffix[0];
4138
+ if (!runtimeBoundaryIsOwned(saved) || saved.restored !== undefined ||
4139
+ saved.sourceStateId !== recoveryCheckpoint.stateId || saved.cohortId !== undefined ||
4140
+ saved.logicalOperationId !== undefined || saved.dispositions.some((item) => item !== 'unchanged') ||
4141
+ saved.physicalReceipt === undefined || saved.after?.head !== saved.baseline.head ||
4142
+ saved.physicalReceipt.classification === 'unchanged' ||
4143
+ [...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId))
4144
+ return undefined;
4145
+ return saved;
3598
4146
  }
3599
- function beginAutomaticReplayBoundary() {
3600
- bindAutomaticReplayBoundary(captureEffectLedgerPrefixSequence());
4147
+ async function confirmReadOnlyRestoration(saved, signal) {
4148
+ const claim = await repositoryCapability.acquire({ signal });
4149
+ try {
4150
+ await claim.assertOwner();
4151
+ const restored = snapshotJsonValue(await repositoryCapability.observe());
4152
+ if (!isDeepStrictEqual(restored, saved.baseline)) {
4153
+ throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
4154
+ }
4155
+ signal.throwIfAborted();
4156
+ effectLedgerMirror = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
4157
+ { kind: 'replace-boundaries', replacements: [{ expected: saved, next: { ...saved, restored } }] },
4158
+ ]));
4159
+ if (!isDeepStrictEqual(effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === saved.boundaryId)?.restored, restored)) {
4160
+ throw new Error(`${label} repository restoration was not durably acknowledged`);
4161
+ }
4162
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
4163
+ }
4164
+ finally {
4165
+ await claim.release();
4166
+ }
3601
4167
  }
3602
- function latchFailedGovernedAttempt() {
3603
- if (!hasGovernedPlayerStates) {
3604
- failedGovernedAttemptUnknown = false;
3605
- failedEffectBoundaryPrefix = undefined;
3606
- failedGovernedAttemptId = undefined;
3607
- return;
4168
+ // A completed commit with saved presentation needs only the missing
4169
+ // tool-free judgment. Never repeat the player to recover that evidence.
4170
+ function recoverableJudgment() {
4171
+ if (!recoveryCheckpoint || recoveryCheckpoint.result !== undefined || recoveryCheckpoint.delivered ||
4172
+ retainedEffectReconciliationRequired ||
4173
+ deferredReconciliationOperationId !== undefined)
4174
+ return undefined;
4175
+ const suffix = currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix);
4176
+ if (suffix.length !== 1)
4177
+ return undefined;
4178
+ const saved = suffix[0];
4179
+ if (!runtimeBoundaryIsOwned(saved) ||
4180
+ saved.sourceStateId !== recoveryCheckpoint.stateId ||
4181
+ saved.logicalOperationId !== undefined ||
4182
+ saved.correctionBudget.spent ||
4183
+ saved.physicalReceipt?.classification !== 'one-descendant-commit' ||
4184
+ !saved.finalText?.trim() ||
4185
+ governedOutcomesForBoundary(saved) === undefined ||
4186
+ (saved.semanticCandidate !== undefined &&
4187
+ persistedBoundaryReconciliation(saved, currentEffectLedger())
4188
+ ?.reconciliation.status !== 'resolved') ||
4189
+ [...unresolvedSemanticBoundaryIds].some((id) => id !== saved.boundaryId))
4190
+ return undefined;
4191
+ return saved;
4192
+ }
4193
+ async function recoverJudgment(saved, signal) {
4194
+ const checkpoint = recoveryCheckpoint;
4195
+ const persisted = checkpoint.machine;
4196
+ const input = Object.values(persisted.children)[0];
4197
+ const actorInput = input.snapshot.input;
4198
+ const outcomes = governedOutcomesForBoundary(saved);
4199
+ const candidate = saved.semanticCandidate ??
4200
+ parseGovernedSemanticCandidate(await boundary.callJudge('player-output-adjudication', saved.sourceStateId, buildGovernedJudgePrompt(actorInput, saved.finalText, outcomes), signal));
4201
+ const result = reconcilePlaybookSemanticEvidence({
4202
+ outcomes,
4203
+ semanticCandidate: candidate,
4204
+ finalText: saved.finalText,
4205
+ receipt: saved.physicalReceipt,
4206
+ });
4207
+ signal.throwIfAborted();
4208
+ const next = {
4209
+ ...saved,
4210
+ semanticCandidate: snapshotJsonValue(result.evidence.semanticCandidate),
4211
+ };
4212
+ const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
4213
+ {
4214
+ kind: 'replace-boundaries',
4215
+ replacements: [{ expected: saved, next }],
4216
+ },
4217
+ ]));
4218
+ if (!isDeepStrictEqual(acknowledged.boundaries.find((b) => b.boundaryId === saved.boundaryId), next)) {
4219
+ throw new Error(`${label} recovered judgment was not acknowledged exactly`);
3608
4220
  }
3609
- if (activeEffectLedgerPrefixSequence === undefined) {
3610
- failedGovernedAttemptUnknown = true;
3611
- failedEffectBoundaryPrefix = undefined;
3612
- failedGovernedAttemptId = undefined;
3613
- return;
4221
+ effectLedgerMirror = acknowledged;
4222
+ refreshUnresolvedSemanticReconciliation(acknowledged);
4223
+ if (result.status !== 'resolved') {
4224
+ const error = attachPlaybookFailureCause(new Error(result.status === 'unresolved' && 'reason' in result.cause.evidence && result.cause.evidence.reason
4225
+ ? String(result.cause.evidence.reason)
4226
+ : `${label} saved result could not be reconciled; the playbook remains parked`), result.status === 'unresolved' ? result.cause : runtimeDefectCause('Saved result still needs a Boss answer'));
4227
+ const paused = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
4228
+ stopActor();
4229
+ actor = buildActor(runtimePorts, {
4230
+ ...paused, context: { ...paused.context, lastError: snapshotJsonValue(normalizeErrorFull(error)) },
4231
+ });
4232
+ suppressInspectionEmissions = false;
4233
+ actor.start();
4234
+ throw error;
3614
4235
  }
3615
- let current;
4236
+ await resumeCheckpoint({ ...checkpoint, result: snapshotJsonValue(result.output) });
4237
+ }
4238
+ async function resumeCheckpoint(checkpoint) {
4239
+ acceptedStepResult = checkpoint;
3616
4240
  try {
3617
- current = currentEffectLedger();
3618
- effectLedgerMirror = current;
3619
- refreshRetainedEffectReconciliation(current);
3620
- }
3621
- catch {
4241
+ stopActor();
4242
+ actor = buildActor(runtimePorts, checkpoint.machine);
4243
+ suppressInspectionEmissions = false;
4244
+ actor.start();
4245
+ await waitForPlaybookQuiescence(actor, { pendingCalls: nestedBridge });
4246
+ }
4247
+ finally {
4248
+ acceptedStepResult = undefined;
4249
+ }
4250
+ }
4251
+ function exportRuntimeSnapshot(checkpoint) {
4252
+ if (!actor || !session || disposed || disposalPromise !== undefined) {
4253
+ return undefined;
4254
+ }
4255
+ if (activeSignal !== undefined && checkpoint === undefined)
4256
+ return undefined;
4257
+ if (deferredSettlementClosure !== undefined)
4258
+ return undefined;
4259
+ const pendingCall = checkpoint?.child ?? nestedBridge.getPendingCall();
4260
+ const bridgeSuspendedCall = checkpoint?.child ? nestedBridge.checkpointCall(checkpoint.child) : nestedBridge.getSuspendedCall();
4261
+ if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
4262
+ return undefined;
4263
+ }
4264
+ let suspendedCall;
4265
+ if (bridgeSuspendedCall !== undefined) {
4266
+ if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
4267
+ pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
4268
+ pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
4269
+ return undefined;
4270
+ }
4271
+ if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
4272
+ return undefined;
4273
+ }
4274
+ const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
4275
+ if (bridgeSuspendedCall.turnId !== undefined &&
4276
+ bridgeSuspendedCall.turnId !== turnId) {
4277
+ return undefined;
4278
+ }
4279
+ suspendedCall = {
4280
+ ...bridgeSuspendedCall,
4281
+ ...(turnId === undefined ? {} : { turnId }),
4282
+ ...(hasGovernedPlayerStates
4283
+ ? {
4284
+ effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
4285
+ }
4286
+ : {}),
4287
+ };
4288
+ }
4289
+ const interrupted = checkpoint?.interrupted && recoveryCheckpoint && machine.root.states.failed;
4290
+ const state = interrupted
4291
+ ? normalizePlaybookSnapshot(machine.resolveState({ value: 'failed', context: {} }))
4292
+ : checkpoint?.child ? normalizePlaybookSnapshot(actor.getSnapshot(), { pendingCall }) : currentState();
4293
+ if (state.status !== 'active' || !state.quiescent)
4294
+ return undefined;
4295
+ const context = actor.getSnapshot()
4296
+ .context;
4297
+ // DR-063 §2: the failed state's `lastError` is persisted as every live
4298
+ // surface publishes it, completed from the runtime's own cause slot, so
4299
+ // a thrown value that refused the cause still explains itself after a
4300
+ // restart.
4301
+ const machineSnapshot = detachPersistedMachineSnapshot(interrupted ? { ...recoveryCheckpoint.machine, value: 'failed', children: {} } : actor.getPersistedSnapshot(), state.stateId === 'failed'
4302
+ ? { lastError: interrupted ? { name: 'InterruptedStep', message: 'The process stopped during this step. Review the recorded work before choosing how to continue.', cause: runtimeDefectCause('The process stopped during this step. Review the recorded work before choosing how to continue.') } : failedStateError(context ?? {}) }
4303
+ : {});
4304
+ effectLedgerMirror = currentEffectLedger();
4305
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
4306
+ syncDeferredReconciliationOverlay();
4307
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
4308
+ const pendingQuestions = !hasUnresolvedReconciliation()
4309
+ ? pendingBossQuestionsFor(state, context ?? {})
4310
+ : [];
4311
+ const interruptedPrefix = interrupted ? activeEffectLedgerPrefixSequence : undefined;
4312
+ const interruptedAttempts = new Set(effectLedgerMirror.boundaries.filter(({ sequence }) => interruptedPrefix !== undefined && sequence > interruptedPrefix).map(({ attemptId }) => attemptId));
4313
+ const failedEffectAttempt = interrupted && interruptedPrefix !== undefined && interruptedAttempts.size <= 1
4314
+ ? { boundaryPrefix: interruptedPrefix, attemptId: interruptedAttempts.values().next().value ?? null }
4315
+ :
4316
+ hasGovernedPlayerStates &&
4317
+ !interrupted && state.stateId === 'failed' &&
4318
+ failedAttemptMatchesCurrentLedger(effectLedgerMirror)
4319
+ ? {
4320
+ boundaryPrefix: failedEffectBoundaryPrefix,
4321
+ attemptId: failedGovernedAttemptId ?? null,
4322
+ }
4323
+ : undefined;
4324
+ return {
4325
+ schemaVersion: 4,
4326
+ playbookId: session.playbookId,
4327
+ machine: machineSnapshot,
4328
+ roleResumeTokens: snapshotRoleResumeTokens(),
4329
+ sequences: {
4330
+ trace: traceSequence,
4331
+ turn: turnSequence,
4332
+ judgeCall: judgeCallSequence,
4333
+ playerCall: playerCallSequence,
4334
+ playbookCall: playbookCallSequence,
4335
+ ...(declaredActors.has('captain')
4336
+ ? { captainCall: captainCallSequence }
4337
+ : {}),
4338
+ },
4339
+ state,
4340
+ pendingBossQuestions: pendingQuestions.map((pending) => ({
4341
+ questionId: pending.questionId,
4342
+ asker: pending.asker,
4343
+ question: pending.question,
4344
+ sourceItem: pending.sourceItem,
4345
+ })),
4346
+ effectLedger: effectLedgerMirror,
4347
+ ...(retainedEffectSourceSessionId === undefined
4348
+ ? {}
4349
+ : { retainedEffectSourceSessionId }),
4350
+ ...(retainedEffectReconciliation === undefined
4351
+ ? {}
4352
+ : { retainedEffectReconciliation }),
4353
+ ...(failedEffectAttempt === undefined
4354
+ ? {}
4355
+ : { failedEffectAttempt }),
4356
+ ...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
4357
+ ? { recoveryCheckpoint } : {}),
4358
+ ...(suspendedCall === undefined ? {} : { suspendedCall }),
4359
+ };
4360
+ }
4361
+ async function runJournalledStep(kind, input, signal, execute) {
4362
+ const saved = acceptedStepResult;
4363
+ let output;
4364
+ if (saved?.stateId === input.stateId && saved.result !== undefined) {
4365
+ acceptedStepResult = undefined;
4366
+ recoveryCheckpoint = saved;
4367
+ output = snapshotJsonValue(saved.result);
4368
+ }
4369
+ else {
4370
+ const record = savedPorts?.recordStep;
4371
+ await drainEmissions();
4372
+ signal.throwIfAborted();
4373
+ try {
4374
+ captureRecoveryCheckpoint(input.stateId, 'command' in input ? input.command : 'role' in input ? composeBoundPlayerPrompt(input) : composeCaptainPrompt(input));
4375
+ }
4376
+ catch (error) {
4377
+ if (!isAbortFailure(error, signal))
4378
+ controlPlaneError ??= error;
4379
+ throw error;
4380
+ }
4381
+ if (record) {
4382
+ const id = randomUUID();
4383
+ if (recoveryCheckpoint)
4384
+ recoveryCheckpoint = { ...recoveryCheckpoint, id };
4385
+ const step = { id, kind, stateId: input.stateId };
4386
+ await record(step, exportRuntimeSnapshot({ interrupted: true }));
4387
+ signal.throwIfAborted();
4388
+ const result = snapshotJsonValue(await execute());
4389
+ // Keep the known output if its save loses acknowledgement. A drained
4390
+ // failure can then preserve it without treating the call as unexecuted.
4391
+ if (recoveryCheckpoint?.id === id)
4392
+ recoveryCheckpoint = { ...recoveryCheckpoint, result };
4393
+ await record({ ...step, result });
4394
+ output = result;
4395
+ }
4396
+ else {
4397
+ output = await execute();
4398
+ }
4399
+ }
4400
+ if (recoveryCheckpoint?.stateId === input.stateId) {
4401
+ const { result: _result, ...start } = recoveryCheckpoint;
4402
+ recoveryCheckpoint = { ...start, delivered: true };
4403
+ }
4404
+ return output;
4405
+ }
4406
+ function validateRecoveryCheckpoint(checkpoint) {
4407
+ if (!checkpoint)
4408
+ return;
4409
+ const node = machine.root.states[checkpoint.stateId];
4410
+ const persisted = checkpoint.machine;
4411
+ const children = persisted.children;
4412
+ if (!node ||
4413
+ node.invoke.length !== 1 ||
4414
+ !['player', 'captain', 'script'].includes(String(node.invoke[0].src)) ||
4415
+ !isPlainObject(children) ||
4416
+ Object.keys(children).length !== 1) {
4417
+ throw new TypeError(`${label} recovery checkpoint does not name one current invocation`);
4418
+ }
4419
+ const child = children[node.invoke[0].id];
4420
+ if (!child ||
4421
+ child.src !== node.invoke[0].src ||
4422
+ !isPlainObject(child.snapshot) ||
4423
+ child.snapshot.status !== 'active' ||
4424
+ !isPlainObject(child.snapshot.input) ||
4425
+ child.snapshot.input.stateId !== checkpoint.stateId) {
4426
+ throw new TypeError(`${label} recovery checkpoint invocation is incompatible`);
4427
+ }
4428
+ if (checkpoint.result !== undefined) {
4429
+ const output = checkpoint.result;
4430
+ const input = child.snapshot.input;
4431
+ if (!isPlainObject(output) || typeof output.guard !== 'string' ||
4432
+ !isPlainObject(input.result) || !Object.hasOwn(input.result, output.guard) ||
4433
+ (node.invoke[0].src === 'script' &&
4434
+ (!Number.isInteger(output.exitStatus) || Object.keys(output).sort().join(',') !== 'exitStatus,guard' ||
4435
+ output.guard !== Object.keys(input.result)[output.exitStatus === 0 ? 0 : Math.min(1, Object.keys(input.result).length - 1)]))) {
4436
+ throw new TypeError(`${label} saved step result is incompatible with its invocation`);
4437
+ }
4438
+ for (const field of extractFields(String(input.result[output.guard]))) {
4439
+ if (output[field] === undefined || output[field] === null)
4440
+ throw new TypeError(`${label} saved step result is missing ${field}`);
4441
+ }
4442
+ }
4443
+ }
4444
+ function captureEffectLedgerPrefixSequence() {
4445
+ if (!hasGovernedPlayerStates)
4446
+ return undefined;
4447
+ try {
4448
+ const current = currentEffectLedger();
4449
+ effectLedgerMirror = current;
4450
+ refreshRetainedEffectReconciliation(current);
4451
+ return current.boundaries.at(-1)?.sequence ?? 0;
4452
+ }
4453
+ catch {
4454
+ return undefined;
4455
+ }
4456
+ }
4457
+ function bindAutomaticReplayBoundary(prefixSequence) {
4458
+ activeGovernedBoundarySeen = false;
4459
+ activeGovernedAttemptId = undefined;
4460
+ activeEffectLedgerPrefixSequence = prefixSequence;
4461
+ }
4462
+ function beginAutomaticReplayBoundary() {
4463
+ bindAutomaticReplayBoundary(captureEffectLedgerPrefixSequence());
4464
+ }
4465
+ function latchFailedGovernedAttempt() {
4466
+ if (!hasGovernedPlayerStates) {
4467
+ failedGovernedAttemptUnknown = false;
4468
+ failedEffectBoundaryPrefix = undefined;
4469
+ failedGovernedAttemptId = undefined;
4470
+ return;
4471
+ }
4472
+ if (activeEffectLedgerPrefixSequence === undefined) {
4473
+ failedGovernedAttemptUnknown = true;
4474
+ failedEffectBoundaryPrefix = undefined;
4475
+ failedGovernedAttemptId = undefined;
4476
+ return;
4477
+ }
4478
+ let current;
4479
+ try {
4480
+ current = currentEffectLedger();
4481
+ effectLedgerMirror = current;
4482
+ refreshRetainedEffectReconciliation(current);
4483
+ }
4484
+ catch {
3622
4485
  failedGovernedAttemptUnknown = true;
3623
4486
  failedEffectBoundaryPrefix = undefined;
3624
4487
  failedGovernedAttemptId = undefined;
@@ -3643,6 +4506,269 @@ export function createXStatePlaybookRuntime(machine, spec) {
3643
4506
  : activeEffectLedgerPrefixSequence;
3644
4507
  failedGovernedAttemptId = attemptIds.values().next().value;
3645
4508
  }
4509
+ // -----------------------------------------------------------------
4510
+ // DR-067 §1: the parallel profile's repository cohort. The working
4511
+ // leaves of one parallel state entered together run under one
4512
+ // `runCohort` claim, each member an `unchanged` boundary: a member's
4513
+ // result is adjudicated through the one reconciler in serial, a spent
4514
+ // correction rewrites every member's boundary in one acknowledged batch,
4515
+ // members are acknowledged and released in completion order, and a
4516
+ // member's failure cancels its siblings and fails the cohort with the
4517
+ // cause decided first.
4518
+ // -----------------------------------------------------------------
4519
+ function trackBoundaryCall(call) {
4520
+ if (parallelProfile === undefined)
4521
+ return call;
4522
+ activeBoundaryCalls.add(call);
4523
+ const forget = () => {
4524
+ activeBoundaryCalls.delete(call);
4525
+ };
4526
+ void call.then(forget, forget);
4527
+ return call;
4528
+ }
4529
+ async function drainBoundaryCalls() {
4530
+ while (activeBoundaryCalls.size > 0) {
4531
+ await Promise.allSettled([...activeBoundaryCalls]);
4532
+ }
4533
+ }
4534
+ function cohortParallelStateFor(stateId) {
4535
+ const parallelState = parallelProfile?.parallelStateByWorkingStateId.get(stateId);
4536
+ if (parallelState === undefined)
4537
+ return undefined;
4538
+ if (consumedCohortBoundaries.get(parallelState.stateId) ===
4539
+ publicBoundarySequence) {
4540
+ return undefined;
4541
+ }
4542
+ const pending = pendingCohorts.get(parallelState.stateId);
4543
+ if (pending !== undefined &&
4544
+ pending.boundarySequence === publicBoundarySequence &&
4545
+ !pending.started) {
4546
+ return parallelState;
4547
+ }
4548
+ const active = currentState().activeStateIds;
4549
+ return parallelState.regions.every(({ workingStateId }) => active.includes(workingStateId))
4550
+ ? parallelState
4551
+ : undefined;
4552
+ }
4553
+ function startCohort(parallelState, cohort) {
4554
+ cohort.started = true;
4555
+ consumedCohortBoundaries.set(parallelState.stateId, cohort.boundarySequence);
4556
+ if (pendingCohorts.get(parallelState.stateId) === cohort) {
4557
+ pendingCohorts.delete(parallelState.stateId);
4558
+ }
4559
+ }
4560
+ async function joinCohort(parallelState, member) {
4561
+ let cohort = pendingCohorts.get(parallelState.stateId);
4562
+ if (cohort === undefined ||
4563
+ cohort.boundarySequence !== publicBoundarySequence ||
4564
+ cohort.started) {
4565
+ cohort = {
4566
+ boundarySequence: publicBoundarySequence,
4567
+ members: new Map(),
4568
+ started: false,
4569
+ };
4570
+ pendingCohorts.set(parallelState.stateId, cohort);
4571
+ }
4572
+ const registered = cohort;
4573
+ if (registered.members.has(member.region.key)) {
4574
+ throw new Error(`${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`);
4575
+ }
4576
+ registered.members.set(member.region.key, member);
4577
+ // A member whose invocation stops before every sibling registered —
4578
+ // its region exited, or the boundary aborted — leaves no cohort to
4579
+ // run: each registered member settles with its own cancellation and
4580
+ // the boundary's cohort is spent.
4581
+ const withdraw = () => {
4582
+ if (registered.started)
4583
+ return;
4584
+ startCohort(parallelState, registered);
4585
+ for (const candidate of registered.members.values()) {
4586
+ candidate.settled.reject(candidate.signal.aborted
4587
+ ? candidate.signal.reason
4588
+ : member.signal.reason);
4589
+ }
4590
+ };
4591
+ if (member.signal.aborted) {
4592
+ withdraw();
4593
+ }
4594
+ else {
4595
+ member.signal.addEventListener('abort', withdraw, { once: true });
4596
+ }
4597
+ if (!registered.started &&
4598
+ registered.members.size === parallelState.regions.length) {
4599
+ startCohort(parallelState, registered);
4600
+ void settleCohort(parallelState, registered);
4601
+ }
4602
+ return await member.settled.promise;
4603
+ }
4604
+ async function settleCohort(parallelState, cohort) {
4605
+ const members = parallelState.regions.map(({ key }) => cohort.members.get(key));
4606
+ const cancellation = new AbortController();
4607
+ const completionOrder = [];
4608
+ const decided = new Map();
4609
+ let decisionSequence = 0;
4610
+ // DR-063 §2: each member failure is one decision, ordered as it was
4611
+ // decided, with the cause decided for exactly that value at that
4612
+ // moment — so a value a sibling later rethrows cannot rename it.
4613
+ const decide = (member, error, cause = failureCauses.causeOf(error)) => {
4614
+ if (decided.has(member))
4615
+ return;
4616
+ decided.set(member, { error, cause, sequence: ++decisionSequence });
4617
+ };
4618
+ const isCancellation = (error) => cancellation.signal.aborted &&
4619
+ Object.is(error, cancellation.signal.reason) &&
4620
+ !activeAborts?.isAbortReason(error);
4621
+ const operationFor = (member) => async (context) => {
4622
+ const callSignal = combineAbortSignals(member.signal, cancellation.signal);
4623
+ try {
4624
+ if (activePlayerKeys.has(member.playerKey)) {
4625
+ await member.reportCollision(callSignal);
4626
+ }
4627
+ activePlayerKeys.add(member.playerKey);
4628
+ try {
4629
+ const result = await member.run(callSignal, cancellation.signal, effectAuthorizedPreExistingBlock(member.effectBoundary, context?.baseline));
4630
+ if (result.status !== 'ok' && !cancellation.signal.aborted) {
4631
+ // DR-063 §2: the failure built for a non-`ok` member result
4632
+ // carries the cause decided for exactly that result, and its
4633
+ // siblings are cancelled with it.
4634
+ const cause = resultFailureCauses.get(result) ??
4635
+ runtimeDefectCause(`callPlayer status ${JSON.stringify(result.status)}`);
4636
+ const failure = attachPlaybookFailureCause(markFsmResultFailure(new Error(result.error ??
4637
+ `captainBridge: callPlayer status "${result.status}"`)), cause);
4638
+ failureCauses.retain(failure, cause);
4639
+ decide(member, failure, cause);
4640
+ cancellation.abort(failure);
4641
+ }
4642
+ return result;
4643
+ }
4644
+ finally {
4645
+ activePlayerKeys.delete(member.playerKey);
4646
+ }
4647
+ }
4648
+ catch (error) {
4649
+ if (!isCancellation(error))
4650
+ decide(member, error);
4651
+ if (!cancellation.signal.aborted)
4652
+ cancellation.abort(error);
4653
+ throw error;
4654
+ }
4655
+ finally {
4656
+ completionOrder.push(member);
4657
+ }
4658
+ };
4659
+ const completeEffectBoundary = (completion) => cohortCompletionQueue.add(async () => {
4660
+ const member = members.find(({ roleId }) => roleId === completion.roleId);
4661
+ if (member === undefined ||
4662
+ completion.boundary.boundaryId !== member.effectBoundary.boundaryId) {
4663
+ throw new TypeError(`${label} repository cohort completed an undeclared member`);
4664
+ }
4665
+ const evidence = await completionEvidenceFor(member.input, member.roleId, member.playerId, member.signal, undefined)(completion);
4666
+ const settlement = governedSettlementsByBoundaryId.get(member.effectBoundary.boundaryId);
4667
+ if (settlement?.status === 'unresolved') {
4668
+ decide(member, settlement.error);
4669
+ }
4670
+ return evidence;
4671
+ });
4672
+ const runSignal = activeSignal ?? members[0].signal;
4673
+ const invocationId = randomUUID();
4674
+ let settled;
4675
+ try {
4676
+ for (const member of members)
4677
+ member.signal.throwIfAborted();
4678
+ settled = await repositoryCapability.runCohort({
4679
+ signal: runSignal,
4680
+ invocationId,
4681
+ roleIds: members.map(({ roleId }) => roleId),
4682
+ dispositionsByRole: Object.fromEntries(members.map(({ roleId }) => [roleId, ['unchanged']])),
4683
+ effectBoundaries: Object.fromEntries(members.map(({ roleId, effectBoundary }) => [
4684
+ roleId,
4685
+ effectBoundary,
4686
+ ])),
4687
+ operations: Object.fromEntries(members.map((member) => [member.roleId, operationFor(member)])),
4688
+ completeEffectBoundary,
4689
+ });
4690
+ if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
4691
+ throw new TypeError(`${label} repository cohort changed its invocation identity`);
4692
+ }
4693
+ }
4694
+ catch (error) {
4695
+ for (const member of members) {
4696
+ governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
4697
+ governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
4698
+ refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
4699
+ }
4700
+ if (!isAbortFailure(error, runSignal))
4701
+ controlPlaneError ??= error;
4702
+ for (const member of members)
4703
+ member.settled.reject(error);
4704
+ return;
4705
+ }
4706
+ const releaseOrder = [
4707
+ ...completionOrder,
4708
+ ...members.filter((member) => !completionOrder.includes(member)),
4709
+ ];
4710
+ const acknowledged = new Map();
4711
+ const failures = new Map();
4712
+ for (const member of releaseOrder) {
4713
+ const operation = isPlainObject(settled.operations)
4714
+ ? settled.operations[member.roleId]
4715
+ : undefined;
4716
+ try {
4717
+ const result = acknowledgeGovernedPlayerResult({
4718
+ operation,
4719
+ receipt: isPlainObject(settled.receipts)
4720
+ ? settled.receipts[member.roleId]
4721
+ : undefined,
4722
+ effectLedger: settled.effectLedger,
4723
+ }, member.effectBoundary.boundaryId, 'runCohort');
4724
+ acknowledged.set(member, result);
4725
+ const settlement = governedPlayerSettlements.get(result);
4726
+ if (settlement?.status === 'unresolved') {
4727
+ failures.set(member, settlement.error);
4728
+ }
4729
+ else if (result.status !== 'ok') {
4730
+ failures.set(member, decided.get(member)?.error);
4731
+ }
4732
+ }
4733
+ catch (error) {
4734
+ failures.set(member, error);
4735
+ const operationReason = isPlainObject(operation) &&
4736
+ operation.status === 'rejected' &&
4737
+ Object.is(operation.reason, error);
4738
+ if (!operationReason) {
4739
+ // An acknowledgement the host could not prove is a control-plane
4740
+ // defect, exactly as on the exclusive path.
4741
+ if (!isAbortFailure(error, runSignal))
4742
+ controlPlaneError ??= error;
4743
+ decide(member, error);
4744
+ }
4745
+ }
4746
+ finally {
4747
+ governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
4748
+ governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
4749
+ }
4750
+ }
4751
+ if (failures.size === 0) {
4752
+ for (const member of releaseOrder) {
4753
+ member.settled.resolve(acknowledged.get(member));
4754
+ }
4755
+ return;
4756
+ }
4757
+ for (const result of acknowledged.values()) {
4758
+ governedPlayerSettlements.delete(result);
4759
+ }
4760
+ const first = [...decided.values()].sort((left, right) => left.sequence - right.sequence)[0];
4761
+ const failure = first?.error ??
4762
+ failures.get(releaseOrder.find((member) => failures.has(member))) ??
4763
+ markFsmResultFailure(new Error(`${label} parallel state ${parallelState.stateId} cohort failed`));
4764
+ const cause = first?.cause ?? failureCauses.causeOf(failure);
4765
+ if (cause !== undefined) {
4766
+ failureCauses.retain(failure, cause);
4767
+ attachPlaybookFailureCause(failure, cause);
4768
+ }
4769
+ for (const member of members)
4770
+ member.settled.reject(failure);
4771
+ }
3646
4772
  const boundary = {
3647
4773
  async callPlayer(input, roleId, freshPrompt, signal) {
3648
4774
  // State-entry telemetry/status must precede the call they describe.
@@ -3687,126 +4813,173 @@ export function createXStatePlaybookRuntime(machine, spec) {
3687
4813
  callId,
3688
4814
  };
3689
4815
  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;
4816
+ const runTracedPlayerCall = async (resume = selectedResume,
4817
+ // DR-062 §4: composed from this call's own baseline observation,
4818
+ // so the prompt the trace records is the prompt the player is sent.
4819
+ preExistingBlock,
4820
+ // DR-067 §1: a cohort member's call signal also carries its
4821
+ // cohort's sibling cancellation.
4822
+ callSignal = signal, cohortCancellation) => {
4823
+ const identity = callIdentity(resume);
4824
+ // DR-067 §1: a parallel machine classifies aborts per call, so a
4825
+ // trace sink rejecting with a sibling's cancellation is forgiven
4826
+ // for that call alone.
4827
+ const traceAborts = parallelProfile === undefined
4828
+ ? undefined
4829
+ : abortReasonClassifier(callSignal, activeAborts);
4830
+ const withPreExisting = (text) => preExistingBlock === undefined
4831
+ ? text
4832
+ : `${text}\n\n${preExistingBlock}`;
4833
+ const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
4834
+ const callFreshPrompt = withPreExisting(freshPrompt);
4835
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, callSignal, undefined, traceAborts);
4836
+ let rawResult;
4837
+ try {
4838
+ // An abort may land while the awaited started emission drains
4839
+ // (e.g. fired from the trace sink itself); the host call must
4840
+ // never start after abort, so settle the already-started pair
4841
+ // as `aborted` through the catch below.
4842
+ callSignal.throwIfAborted();
4843
+ rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, callSignal, {
4844
+ resume,
4845
+ ...(callPrompt === callFreshPrompt
4846
+ ? {}
4847
+ : { freshPrompt: callFreshPrompt }),
4848
+ });
4849
+ // A host promise is not required to honor cancellation. Do not
4850
+ // let a late result mutate continuity or publish a successful
4851
+ // finish.
4852
+ callSignal.throwIfAborted();
4853
+ }
4854
+ catch (error) {
4855
+ if (!isAbortFailure(error, callSignal))
4856
+ controlPlaneError ??= error;
3714
4857
  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();
4858
+ await emitTrace('player.call.finished', {
4859
+ ...identity,
4860
+ status: isAbortFailure(error, callSignal)
4861
+ ? 'aborted'
4862
+ : 'error',
4863
+ error: normalizeError(error),
4864
+ }, position, traceAborts);
3730
4865
  }
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);
4866
+ catch {
4867
+ // The original non-abort port rejection remains authoritative.
3747
4868
  }
3748
- let result;
4869
+ // A thrown port call carries no authoritative result, so the
4870
+ // prior token remains available for a later explicit resume.
4871
+ throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
4872
+ }
4873
+ let result;
4874
+ try {
4875
+ result = validatePlayerResult(rawResult);
4876
+ }
4877
+ catch (error) {
4878
+ if (!isAbortFailure(error, callSignal))
4879
+ controlPlaneError ??= error;
3749
4880
  try {
3750
- result = validatePlayerResult(rawResult);
4881
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
3751
4882
  }
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);
4883
+ catch {
4884
+ // The malformed host result remains authoritative.
3762
4885
  }
4886
+ throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
4887
+ }
4888
+ try {
4889
+ updatePlayerResume(roleId, playerId, result);
4890
+ }
4891
+ catch (error) {
4892
+ if (!isAbortFailure(error, callSignal))
4893
+ controlPlaneError ??= error;
3763
4894
  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;
4895
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
3796
4896
  }
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
- });
4897
+ catch {
4898
+ // The continuation-store failure remains authoritative.
3807
4899
  }
3808
- return result;
3809
- };
4900
+ throw error;
4901
+ }
4902
+ await emitTrace('player.call.finished', {
4903
+ ...identity,
4904
+ status: result.status,
4905
+ ...(result.finalText !== undefined
4906
+ ? { finalText: result.finalText }
4907
+ : {}),
4908
+ ...(result.error !== undefined
4909
+ ? { error: normalizeError(result.error) }
4910
+ : {}),
4911
+ ...(result.resumeToken !== undefined
4912
+ ? { resumeToken: result.resumeToken }
4913
+ : {}),
4914
+ }, position, traceAborts);
4915
+ // DR-063 §2: the player bridge builds the failure Error for a
4916
+ // non-`ok` result, so the cause is decided here, where the role,
4917
+ // the player, and the reported error are known, and a successful
4918
+ // result clears it so no later failure inherits it.
4919
+ if (result.status === 'ok') {
4920
+ pendingResultFailureCause = undefined;
4921
+ }
4922
+ else if (result.status === 'aborted') {
4923
+ pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
4924
+ }
4925
+ else {
4926
+ pendingResultFailureCause = playerFailureCause({
4927
+ roleId,
4928
+ ...(playerId === undefined ? {} : { playerId }),
4929
+ error: result.error ??
4930
+ `callPlayer status ${JSON.stringify(result.status)}`,
4931
+ });
4932
+ }
4933
+ if (pendingResultFailureCause !== undefined) {
4934
+ resultFailureCauses.set(result, pendingResultFailureCause);
4935
+ }
4936
+ return result;
4937
+ };
4938
+ const reportPlayerKeyCollision = async (callSignal) => {
4939
+ const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
4940
+ const traceAborts = parallelProfile === undefined
4941
+ ? undefined
4942
+ : abortReasonClassifier(callSignal, activeAborts);
4943
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, callSignal, undefined, traceAborts);
4944
+ await emitTrace('player.call.finished', {
4945
+ ...callIdentity(selectedResume),
4946
+ status: 'error',
4947
+ error: normalizeError(error),
4948
+ }, position, traceAborts);
4949
+ throw error;
4950
+ };
4951
+ // DR-067 §1: the working leaves of one parallel state entered
4952
+ // together run as one all-`unchanged` repository cohort, at most once
4953
+ // per public boundary; a corrective re-ask and a single region
4954
+ // resuming after its Boss reply take the exclusive path below.
4955
+ const cohortState = deferredContinuation === undefined
4956
+ ? cohortParallelStateFor(input.stateId)
4957
+ : undefined;
4958
+ if (cohortState !== undefined) {
4959
+ const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
4960
+ if (effectBoundary === undefined ||
4961
+ repositoryCapability?.runCohort === undefined) {
4962
+ throw new Error(`${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`);
4963
+ }
4964
+ activeGovernedBoundarySeen = true;
4965
+ return await joinCohort(cohortState, {
4966
+ region: cohortState.regions.find(({ workingStateId }) => workingStateId === input.stateId),
4967
+ input,
4968
+ roleId,
4969
+ playerId,
4970
+ playerKey,
4971
+ signal,
4972
+ effectBoundary,
4973
+ run: (callSignal, cohortCancellation, preExistingBlock) => runTracedPlayerCall(selectedResume, preExistingBlock, callSignal, cohortCancellation),
4974
+ reportCollision: reportPlayerKeyCollision,
4975
+ settled: deferredValue(),
4976
+ });
4977
+ }
4978
+ if (activePlayerKeys.has(playerKey)) {
4979
+ await reportPlayerKeyCollision(signal);
4980
+ }
4981
+ activePlayerKeys.add(playerKey);
4982
+ try {
3810
4983
  const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
3811
4984
  if (deferredContinuation !== undefined) {
3812
4985
  if (effectBoundary === undefined ||
@@ -3853,7 +5026,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
3853
5026
  // captured it under this exclusive claim. A host that supplies
3854
5027
  // none states no pre-existing change, exactly as an empty
3855
5028
  // baseline projection does.
3856
- operation: (context) => runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline)),
5029
+ operation: async (context) => {
5030
+ if (requiredRecoveryBaseline !== undefined) {
5031
+ if (!isDeepStrictEqual(snapshotJsonValue(context.baseline), requiredRecoveryBaseline)) {
5032
+ throw new Error(`${label} repository changed after preparation; the stopped step remains paused`);
5033
+ }
5034
+ requiredRecoveryBaseline = undefined;
5035
+ captureRecoveryCheckpoint(input.stateId, freshPrompt, currentEffectLedger().boundaries.findIndex(({ boundaryId }) => boundaryId === effectBoundary.boundaryId));
5036
+ }
5037
+ return runTracedPlayerCall(selectedResume, effectAuthorizedPreExistingBlock(effectBoundary, context?.baseline));
5038
+ },
3857
5039
  completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
3858
5040
  });
3859
5041
  return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
@@ -3875,11 +5057,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
3875
5057
  activePlayerKeys.delete(playerKey);
3876
5058
  }
3877
5059
  },
3878
- markPlayerResultFailure(error) {
3879
- if (pendingResultFailureCause === undefined)
5060
+ markPlayerResultFailure(error, result) {
5061
+ const cause = (result === undefined ? undefined : resultFailureCauses.get(result)) ??
5062
+ pendingResultFailureCause;
5063
+ if (cause === undefined)
3880
5064
  return error;
3881
- failureCauses.retain(error, pendingResultFailureCause);
3882
- return attachPlaybookFailureCause(error, pendingResultFailureCause);
5065
+ failureCauses.retain(error, cause);
5066
+ return attachPlaybookFailureCause(error, cause);
3883
5067
  },
3884
5068
  takeGovernedPlayerOutput(result) {
3885
5069
  const settlement = governedPlayerSettlements.get(result);
@@ -4067,8 +5251,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
4067
5251
  });
4068
5252
  },
4069
5253
  };
5254
+ if (parallelProfile !== undefined) {
5255
+ // DR-067 §1: a parallel machine's boundary drain waits for every player
5256
+ // and judge call it started.
5257
+ const untrackedCallPlayer = boundary.callPlayer;
5258
+ const untrackedCallJudge = boundary.callJudge;
5259
+ boundary.callPlayer = (input, roleId, freshPrompt, signal) => trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
5260
+ boundary.callJudge = (purpose, stateId, prompt, signal) => trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
5261
+ }
4070
5262
  function playerActor(ports) {
4071
5263
  return createPlayerBridge({
5264
+ run: (input, signal, execute) => runJournalledStep('player', input, signal, execute),
4072
5265
  resolveRoleId: requireRoleId,
4073
5266
  validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
4074
5267
  composePlayerPrompt: composeBoundPlayerPrompt,
@@ -4086,7 +5279,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4086
5279
  // adjudication that injects the exact visible finalText as the selected
4087
5280
  // output's question/response.
4088
5281
  function captainActor() {
4089
- return fromPromise(async ({ input, signal }) => {
5282
+ return fromPromise(({ input, signal }) => runJournalledStep('captain', input, signal, async () => {
4090
5283
  const active = combineAbortSignals(signal, activeSignal);
4091
5284
  try {
4092
5285
  await drainEmissions();
@@ -4150,7 +5343,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4150
5343
  }
4151
5344
  throw error;
4152
5345
  }
4153
- });
5346
+ }));
4154
5347
  }
4155
5348
  // Deterministic script actor (slc/link.md §Script execution). Runs
4156
5349
  // `input.command` through `sh -c`, resolves the declared guard
@@ -4158,7 +5351,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4158
5351
  // `playbook.script` telemetry event. No agent call, no adjudication,
4159
5352
  // no `*.call.*` trace.
4160
5353
  function scriptActor() {
4161
- return fromPromise(async ({ input, signal }) => {
5354
+ return fromPromise(({ input, signal }) => runJournalledStep('script', input, signal, async () => {
4162
5355
  await drainEmissions();
4163
5356
  const active = combineAbortSignals(signal, activeSignal);
4164
5357
  const guards = Object.keys(input.result);
@@ -4330,12 +5523,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
4330
5523
  clearTimeout(killTimer);
4331
5524
  }
4332
5525
  }
4333
- });
5526
+ }));
4334
5527
  }
4335
5528
  const nestedBridge = createNestedPlaybookBridge({
4336
5529
  nextCallId: () => `playbook-${++playbookCallSequence}`,
4337
5530
  getBoundarySignal: () => activeSignal,
4338
- callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
5531
+ callPlaybook: (request, signal) => {
5532
+ const call = requireHostPorts().callPlaybook(request, signal);
5533
+ return parallelProfile === undefined
5534
+ ? call
5535
+ : trackBoundaryCall(Promise.resolve(call));
5536
+ },
4339
5537
  emitStarted: async (event, aborts) => {
4340
5538
  playbookCallTurnIds.set(event.callId, activeTurnId);
4341
5539
  if (hasGovernedPlayerStates) {
@@ -4374,7 +5572,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4374
5572
  activeAborts = aborts ?? abortReasonClassifier(signal);
4375
5573
  },
4376
5574
  bindActorSettlement: (aborts) => {
4377
- actorSettlementAborts = aborts;
5575
+ if (parallelProfile === undefined)
5576
+ actorSettlementAborts.length = 0;
5577
+ actorSettlementAborts.push(aborts);
4378
5578
  },
4379
5579
  onControlPlaneError: (error, aborts) => {
4380
5580
  // The shared bridge classifies before reporting against its own
@@ -4412,9 +5612,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
4412
5612
  runtimeDefectCause(normalized.message),
4413
5613
  };
4414
5614
  }
5615
+ // DR-063 §2: the failure that parks the machine is the error its parking
5616
+ // transition carried, but a compiled machine keeps a JSON-safe record of
5617
+ // it as `lastError` (gears2fsm requires JSON-safe context), so neither
5618
+ // that value's identity nor its marker reaches the failed state. The cause
5619
+ // decided for the carried error is retained under the value the machine
5620
+ // kept, so every surface of this turn and of each later one publishes the
5621
+ // runtime's own decision for as long as that failure stands. A kept value
5622
+ // that already answers keeps its cause.
5623
+ function bindParkedFailureCause(context, event) {
5624
+ const kept = context.lastError;
5625
+ if (kept === undefined || kept === null)
5626
+ return;
5627
+ if (failureCauses.causeOf(kept) !== undefined)
5628
+ return;
5629
+ const decided = failureCauses.causeOf(actorErrorOf(event));
5630
+ if (decided !== undefined)
5631
+ failureCauses.retain(kept, decided);
5632
+ }
4415
5633
  // 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) {
5634
+ // a `player-failed` failure named by role; an abort is `aborted`. A
5635
+ // cancellation the runtime itself issued to a cohort member because its
5636
+ // sibling failed is not this player's failure and decides nothing, so the
5637
+ // originator's cause stands (DR-067 §1, playbook-55).
5638
+ function decidePlayerCallFailure(error, roleId, playerId, signal, cohortCancellation) {
5639
+ if (cohortCancellation?.aborted === true &&
5640
+ Object.is(error, cohortCancellation.reason) &&
5641
+ !activeAborts?.isAbortReason(error)) {
5642
+ return error;
5643
+ }
4418
5644
  const cause = isAbortFailure(error, signal)
4419
5645
  ? ABORTED_FAILURE_CAUSE
4420
5646
  : playerFailureCause({
@@ -4459,10 +5685,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
4459
5685
  previousState: previousState ?? null,
4460
5686
  state,
4461
5687
  };
4462
- const pendingBossQuestion = pendingBossQuestionForState(state, context);
4463
- if (pendingBossQuestion !== undefined &&
4464
- !hasUnresolvedReconciliation()) {
4465
- payload.pendingBossQuestion = pendingBossQuestion;
5688
+ if (parallelProfile === undefined) {
5689
+ const pendingBossQuestion = pendingBossQuestionForState(state, context);
5690
+ if (pendingBossQuestion !== undefined &&
5691
+ !hasUnresolvedReconciliation()) {
5692
+ payload.pendingBossQuestion = pendingBossQuestion;
5693
+ }
5694
+ }
5695
+ else if (!hasUnresolvedReconciliation()) {
5696
+ // DR-067 §1: a parallel machine's telemetry carries every question
5697
+ // pending in an active wait, in the plural.
5698
+ const pendingBossQuestions = pendingBossQuestionsFor(state, context);
5699
+ if (pendingBossQuestions.length > 0) {
5700
+ payload.pendingBossQuestions = pendingBossQuestions;
5701
+ }
4466
5702
  }
4467
5703
  if (state.stateId === 'failed') {
4468
5704
  const lastError = failedStateError(context);
@@ -4482,7 +5718,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4482
5718
  message,
4483
5719
  ...(data === undefined ? {} : { data }),
4484
5720
  state,
4485
- ...stateIdentity(state.stateId),
5721
+ ...stateIdentity(statusTraceStateId(state)),
4486
5722
  }, position),
4487
5723
  }));
4488
5724
  void enqueueEmission(async () => {
@@ -4523,8 +5759,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4523
5759
  controlPlaneError ??= error;
4524
5760
  }
4525
5761
  function consumeActorSettlementAborts(forSnapshot = false) {
4526
- const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
4527
- actorSettlementAborts = undefined;
5762
+ const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
4528
5763
  actorSettlementErrorAborts = undefined;
4529
5764
  if (forSnapshot && aborts !== undefined) {
4530
5765
  // XState can report an errored root through both its inspection
@@ -4599,7 +5834,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
4599
5834
  try {
4600
5835
  const snap = inspectionEvent.snapshot;
4601
5836
  const state = normalizePlaybookSnapshot(snap);
4602
- if (state.stateId === undefined) {
5837
+ if (state.stateId === undefined &&
5838
+ !(parallelProfile?.states.some(({ stateId }) => state.activeStateIds.includes(stateId)) ?? false)) {
4603
5839
  throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
4604
5840
  }
4605
5841
  const previousState = priorState;
@@ -4612,6 +5848,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
4612
5848
  latchRuntimeError(error);
4613
5849
  }
4614
5850
  if (state.stateId === 'failed') {
5851
+ if (previousState?.stateId !== 'failed' &&
5852
+ previousState?.stateId !== recoveryCheckpoint?.stateId) {
5853
+ recoveryCheckpoint = undefined;
5854
+ }
4615
5855
  if (previousState?.stateId !== 'failed' ||
4616
5856
  activeGovernedBoundarySeen) {
4617
5857
  latchFailedGovernedAttempt();
@@ -4624,6 +5864,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4624
5864
  }
4625
5865
  const context = (snap.context ??
4626
5866
  {});
5867
+ if (state.stateId === 'failed') {
5868
+ bindParkedFailureCause(context, inspectionEvent.event);
5869
+ }
4627
5870
  if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
4628
5871
  deferredReconciliationOperationId !== undefined) {
4629
5872
  priorState = state;
@@ -4635,7 +5878,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
4635
5878
  validateBoundQuestionProjection();
4636
5879
  }
4637
5880
  const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
4638
- const stateStatuses = completeFailedStatuses(statusesForState(state, context, inspectionEvent.event), state, context);
5881
+ const stateStatuses = completeFailedStatuses(bossRelevantStateIds !== undefined && usesDefaultStatuses
5882
+ ? parallelStatusesFor(previousState, state, context)
5883
+ : statusesForState(state, context, inspectionEvent.event), state, context);
4639
5884
  const outcomeStatuses = usesDefaultStatuses
4640
5885
  ? acceptedOutcomes.map(({ acceptedOutcome }) => ({
4641
5886
  message: `→ ${acceptedOutcome}`,
@@ -4816,13 +6061,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
4816
6061
  emissionQueue.clear();
4817
6062
  judgeQueue.clear();
4818
6063
  appliedReceipts.clear();
6064
+ pendingCohorts.clear();
6065
+ consumedCohortBoundaries.clear();
6066
+ activeBoundaryCalls.clear();
6067
+ cohortCompletionQueue.clear();
4819
6068
  actor = undefined;
4820
6069
  session = undefined;
4821
6070
  savedPorts = undefined;
4822
6071
  runtimePorts = undefined;
4823
6072
  activeSignal = undefined;
4824
6073
  activeAborts = undefined;
4825
- actorSettlementAborts = undefined;
6074
+ actorSettlementAborts.length = 0;
4826
6075
  actorSettlementErrorAborts = undefined;
4827
6076
  activeAbortEmission = undefined;
4828
6077
  activeTurnId = undefined;
@@ -4850,7 +6099,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
4850
6099
  activeDeferredContinuation = undefined;
4851
6100
  deferInspectionEmissions = false;
4852
6101
  deferredInspectionEmissions = [];
4853
- lastBossEvent = undefined;
6102
+ recoveryCheckpoint = undefined;
4854
6103
  suppressInspectionEmissions = false;
4855
6104
  initialized = false;
4856
6105
  traceSequence = 0;
@@ -4867,78 +6116,26 @@ export function createXStatePlaybookRuntime(machine, spec) {
4867
6116
  can.call(snapshot, event) ===
4868
6117
  true);
4869
6118
  }
4870
- // DR-034: where the artifact names the FSM context member its entry
4871
- // action copies the exact Boss text into, that member of the live
4872
- // snapshot is the retry payload's source. The persisted machine snapshot
4873
- // carries it, so the candidate derives identically in the process that
4874
- // exported the snapshot and in one that restored it, and a failure
4875
- // reached after a Boss reply — whose recorded event the failure state
4876
- // refuses — is recoverable too. Naming the member is the artifact's
4877
- // statement that it holds the entry text: a same-named member is never
4878
- // assumed, since inferring one would turn any matching context member
4879
- // into a replay payload without its author saying so.
4880
- // Declared and absent or empty excludes the candidate rather than
4881
- // falling back to the record, which would make the action depend on the
4882
- // process again — the very thing this source exists to end.
4883
- function retryEventFrom(snapshot) {
4884
- const entryEvent = spec.entryEvent;
4885
- if (entryEvent?.contextField === undefined)
4886
- return lastBossEvent;
4887
- const context = snapshot?.context;
4888
- const text = isPlainObject(context)
4889
- ? context[entryEvent.contextField]
4890
- : undefined;
4891
- if (typeof text !== 'string' || text.trim() === '')
4892
- return undefined;
4893
- return { type: entryEvent.type, [entryEvent.textField]: text };
4894
- }
4895
- // The failure-state retry entry replays the recorded last classified
4896
- // event with its recorded payload, or the entry event the declared
4897
- // context member above sources. A candidate whose event the live
4898
- // snapshot does not accept — or whose payload the runtime can source
4899
- // from neither — is excluded rather than completed with invented text.
4900
- function retryActionFor(snapshot, stateId) {
4901
- if (stateId !== 'failed')
4902
- return undefined;
4903
- if (!failedAttemptAllowsReplay())
4904
- return undefined;
4905
- const retryEvent = retryEventFrom(snapshot);
4906
- if (retryEvent === undefined)
6119
+ function retryActionFor(stateId) {
6120
+ if (stateId !== 'failed' || !recoveryCheckpoint)
4907
6121
  return undefined;
4908
- if (!snapshotCan(snapshot, retryEvent))
4909
- return undefined;
4910
- // A recorded explicit-state-jump event names the exact state its
4911
- // replay re-enters: the root BOSS_INTERRUPT shape is a guarded
4912
- // multi-arm list keyed on `targetId`, so the first configured arm
4913
- // may label a different state than the one the recorded event
4914
- // actually resumes.
4915
- const recordedTargetId = retryEvent.type === JUMP_EVENT_TYPE
4916
- ? retryEvent.targetId
4917
- : undefined;
4918
- const target = typeof recordedTargetId === 'string' &&
4919
- recordedTargetId.trim().length > 0
4920
- ? recordedTargetId
4921
- : firstTransitionTarget(machine, stateId, retryEvent.type);
4922
- // PBRT-52: a label is written from a source state description, never
4923
- // from an identifier. Falling back to the target id — or, with no
4924
- // resolvable target, to the FSM event type — makes the label *be* the
4925
- // internal name, which defeats the substitution the label exists for
4926
- // and puts a machine identifier into Boss-facing text
4927
- // (CAPPLAY-5). A candidate whose label can only be an id is excluded
4928
- // exactly like one whose payload cannot be sourced.
4929
- const description = (target === undefined ? undefined : stateDescriptions.get(target)) ??
4930
- stateDescriptions.get(stateId);
4931
- if (description === undefined)
6122
+ const description = stateDescriptions.get(recoveryCheckpoint.stateId);
6123
+ const savedResult = recoveryCheckpoint.result !== undefined;
6124
+ const safe = savedResult
6125
+ ? !hasUnresolvedReconciliation() && currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
6126
+ .every((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint.stateId)
6127
+ : stepReplayIsSafe();
6128
+ if (description === undefined || !safe)
4932
6129
  return undefined;
6130
+ const entry = spec.entryEvent;
6131
+ const target = entry && firstTransitionTarget(machine, stateId, entry.type);
4933
6132
  return {
4934
6133
  action: {
4935
- id: `retry:${retryEvent.type}`,
4936
- label: `Retry: ${description}`,
4937
- // DR-063 §3: retrying a failed call is `ready` — the runtime cannot
4938
- // see whether the Boss changed the outside world since it failed.
6134
+ id: target === recoveryCheckpoint.stateId ? `retry:${entry.type}` : 'retry:step',
6135
+ label: `${savedResult ? 'Continue from saved result' : 'Retry'}: ${description}`,
4939
6136
  standing: 'ready',
4940
6137
  },
4941
- event: retryEvent,
6138
+ retryStep: true,
4942
6139
  };
4943
6140
  }
4944
6141
  /**
@@ -4984,6 +6181,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
4984
6181
  return [];
4985
6182
  }
4986
6183
  const derived = [];
6184
+ const repairable = state.stateId === 'failed' ? repairableReadOnlyBoundary() : undefined;
6185
+ if (repairable !== undefined)
6186
+ derived.push({
6187
+ action: { id: 'retry:restored-step', label: 'Restore the repository and retry the read-only step', standing: 'ready' },
6188
+ retryStep: true,
6189
+ restoreReadOnly: repairable,
6190
+ });
6191
+ const judgment = state.stateId === 'failed' ? recoverableJudgment() : undefined;
6192
+ if (judgment !== undefined)
6193
+ derived.push({
6194
+ action: { id: 'retry:adjudication', label: 'Retry assessment of the saved result', standing: 'ready' },
6195
+ retryJudgment: judgment,
6196
+ });
6197
+ if (judgment !== undefined && !hasUnresolvedReconciliation())
6198
+ return derived;
4987
6199
  if (hasUnresolvedReconciliation()) {
4988
6200
  const operation = deferredReconciliationOperationId === undefined
4989
6201
  ? undefined
@@ -5012,9 +6224,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
5012
6224
  });
5013
6225
  return derived;
5014
6226
  }
5015
- const retry = retryActionFor(snapshot, state.stateId);
6227
+ const retry = retryActionFor(state.stateId);
5016
6228
  if (retry !== undefined)
5017
6229
  derived.push(retry);
6230
+ // A jump can repeat earlier work, unlike checkpoint retry. Its whole
6231
+ // failed attempt must therefore still pass the replay fence.
6232
+ if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
6233
+ return derived;
6234
+ }
5018
6235
  // Jump entries: resumable targets whose explicit-state-jump event the
5019
6236
  // live snapshot accepts (state guards included), sent with the
5020
6237
  // advertised target id and optional textual fields omitted.
@@ -5077,9 +6294,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
5077
6294
  // state whose source declares no description publishes none: an id is
5078
6295
  // never promoted into a description by default.
5079
6296
  function stateDescriptionFor(state) {
6297
+ // DR-067 §1: inside a parallel state, the meaning published is the
6298
+ // root state's — the parallel parent's own description.
5080
6299
  const keys = [
5081
6300
  ...(state.stateId === undefined ? [] : [state.stateId]),
5082
6301
  ...(typeof state.value === 'string' ? [state.value] : []),
6302
+ ...(isPlainObject(state.value) ? Object.keys(state.value) : []),
5083
6303
  ...state.activeStateIds,
5084
6304
  ];
5085
6305
  for (const key of keys) {
@@ -5123,6 +6343,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5123
6343
  }
5124
6344
  const boundSession = bindSession(nextSession);
5125
6345
  const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
6346
+ validateRecoveryCheckpoint(boundSnapshot.recoveryCheckpoint);
5126
6347
  if (kind === 'adopt' &&
5127
6348
  effectLedgerCapability !== undefined &&
5128
6349
  (boundSnapshot.retainedEffectReconciliation?.checkpoint ??
@@ -5189,6 +6410,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5189
6410
  initInFlight = initialization;
5190
6411
  const initTask = (async () => {
5191
6412
  session = boundSession;
6413
+ recoveryCheckpoint = boundSnapshot.recoveryCheckpoint;
5192
6414
  syncDeferredReconciliationOverlay();
5193
6415
  refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5194
6416
  prepareReconstructedGovernedDelivery(boundSnapshot.state, effectLedgerMirror);
@@ -5363,7 +6585,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5363
6585
  const context = (snapshot
5364
6586
  ?.context ?? {});
5365
6587
  const actual = state?.stateId === BOSS_REPLY_WAIT_STATE_ID
5366
- ? pendingBossQuestionFromContext(context)
6588
+ ? singlePendingBossQuestion(state, context)
5367
6589
  : undefined;
5368
6590
  if (!isDeepStrictEqual(actual, expected)) {
5369
6591
  throw new Error(`${label} deferred FSM question does not equal its durable binding`);
@@ -5383,7 +6605,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5383
6605
  emission();
5384
6606
  }
5385
6607
  }
5386
- async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine) {
6608
+ async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine, onAccepted) {
5387
6609
  if (repositoryCapability === undefined) {
5388
6610
  throw new Error(`${label} deferred continuation requires repository.runDeferred`);
5389
6611
  }
@@ -5429,6 +6651,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5429
6651
  continuation.baseline = context?.baseline;
5430
6652
  continuation.playerContinuation = selectPlayerResume(effectBoundary.roleId, boundPlayerId);
5431
6653
  continuationStarted = true;
6654
+ onAccepted?.();
5432
6655
  actor.send(event);
5433
6656
  // The host has durably started this boundary and the FSM has
5434
6657
  // moved: publish the buffered classification line and the
@@ -5622,7 +6845,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5622
6845
  const state = normalizePlaybookSnapshot(snapshot);
5623
6846
  const context = (snapshot.context ??
5624
6847
  {});
5625
- const pending = pendingBossQuestionForState(state, context);
6848
+ const pending = singlePendingBossQuestion(state, context);
5626
6849
  if (pending === undefined ||
5627
6850
  currentBoundDeferredOperation(pending)?.operationId !== operationId) {
5628
6851
  throw new TypeError(`${label} restored deferred wait does not equal its FSM question`);
@@ -5678,110 +6901,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5678
6901
  // Defined only at a safe capture point — initialized, not disposing
5679
6902
  // or disposed, no active public boundary, and the actor quiescent with
5680
6903
  // status `active`.
5681
- exportSnapshot() {
5682
- if (!actor || !session || disposed || disposalPromise !== undefined) {
5683
- return undefined;
5684
- }
5685
- if (activeSignal !== undefined)
5686
- return undefined;
5687
- if (deferredSettlementClosure !== undefined)
5688
- return undefined;
5689
- const pendingCall = nestedBridge.getPendingCall();
5690
- const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
5691
- if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
5692
- return undefined;
5693
- }
5694
- let suspendedCall;
5695
- if (bridgeSuspendedCall !== undefined) {
5696
- if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
5697
- pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
5698
- pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
5699
- return undefined;
5700
- }
5701
- if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
5702
- return undefined;
5703
- }
5704
- const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
5705
- if (bridgeSuspendedCall.turnId !== undefined &&
5706
- bridgeSuspendedCall.turnId !== turnId) {
5707
- return undefined;
5708
- }
5709
- suspendedCall = {
5710
- ...bridgeSuspendedCall,
5711
- ...(turnId === undefined ? {} : { turnId }),
5712
- ...(hasGovernedPlayerStates
5713
- ? {
5714
- effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
5715
- }
5716
- : {}),
5717
- };
5718
- }
5719
- const state = currentState();
5720
- if (state.status !== 'active' || !state.quiescent)
5721
- return undefined;
5722
- const context = actor.getSnapshot()
5723
- .context;
5724
- // DR-063 §2: the failed state's `lastError` is persisted as every live
5725
- // surface publishes it, completed from the runtime's own cause slot, so
5726
- // a thrown value that refused the cause still explains itself after a
5727
- // restart.
5728
- const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
5729
- ? { lastError: failedStateError(context ?? {}) }
5730
- : {});
5731
- effectLedgerMirror = currentEffectLedger();
5732
- refreshRetainedEffectReconciliation(effectLedgerMirror);
5733
- syncDeferredReconciliationOverlay();
5734
- refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5735
- const pending = !hasUnresolvedReconciliation()
5736
- ? pendingBossQuestionForState(state, context ?? {})
5737
- : undefined;
5738
- const failedEffectAttempt = hasGovernedPlayerStates &&
5739
- state.stateId === 'failed' &&
5740
- failedAttemptMatchesCurrentLedger(effectLedgerMirror)
5741
- ? {
5742
- boundaryPrefix: failedEffectBoundaryPrefix,
5743
- attemptId: failedGovernedAttemptId ?? null,
5744
- }
5745
- : undefined;
5746
- return {
5747
- schemaVersion: 4,
5748
- playbookId: session.playbookId,
5749
- machine: machineSnapshot,
5750
- roleResumeTokens: snapshotRoleResumeTokens(),
5751
- sequences: {
5752
- trace: traceSequence,
5753
- turn: turnSequence,
5754
- judgeCall: judgeCallSequence,
5755
- playerCall: playerCallSequence,
5756
- playbookCall: playbookCallSequence,
5757
- ...(declaredActors.has('captain')
5758
- ? { captainCall: captainCallSequence }
5759
- : {}),
5760
- },
5761
- 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
- ],
5772
- effectLedger: effectLedgerMirror,
5773
- ...(retainedEffectSourceSessionId === undefined
5774
- ? {}
5775
- : { retainedEffectSourceSessionId }),
5776
- ...(retainedEffectReconciliation === undefined
5777
- ? {}
5778
- : { retainedEffectReconciliation }),
5779
- ...(failedEffectAttempt === undefined
5780
- ? {}
5781
- : { failedEffectAttempt }),
5782
- ...(suspendedCall === undefined ? {} : { suspendedCall }),
5783
- };
5784
- },
6904
+ exportSnapshot: (checkpoint) => exportRuntimeSnapshot(checkpoint?.child ? { child: checkpoint.child } : undefined),
5785
6905
  // DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
5786
6906
  // exported snapshot under the same immutable session identity.
5787
6907
  // Emits no `session.started`, transition trace, or human status —
@@ -5791,13 +6911,19 @@ export function createXStatePlaybookRuntime(machine, spec) {
5791
6911
  await rehydrateSnapshot('restore', nextSession, snapshot);
5792
6912
  },
5793
6913
  // 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
6914
+ // engagement identity and counter lineage, exposed separately so a
6915
+ // capability-less runtime can omit it. Runtime-visible preflight
6916
+ // mismatches reject before effects; after preflight the new
5797
6917
  // session.started boundary owns failed-start cleanup just like init.
5798
- async adopt(nextSession, snapshot, context) {
5799
- await rehydrateSnapshot('adopt', nextSession, snapshot, context);
5800
- },
6918
+ // DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
6919
+ // machine that declares a parallel state omits the member.
6920
+ ...(parallelProfile === undefined
6921
+ ? {
6922
+ async adopt(nextSession, snapshot, context) {
6923
+ await rehydrateSnapshot('adopt', nextSession, snapshot, context);
6924
+ },
6925
+ }
6926
+ : {}),
5801
6927
  // DR-029 / PBRT-52: side-effect-free control view over the live
5802
6928
  // snapshot, valid at parked quiescence outside an active boundary.
5803
6929
  // The view is detached and frozen; producing it emits nothing and
@@ -5818,9 +6944,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
5818
6944
  const state = currentState();
5819
6945
  const context = (snapshot.context ??
5820
6946
  {});
5821
- const pending = !hasUnresolvedReconciliation()
5822
- ? pendingBossQuestionForState(state, context)
5823
- : undefined;
6947
+ const pendingQuestions = !hasUnresolvedReconciliation()
6948
+ ? pendingBossQuestionsFor(state, context)
6949
+ : [];
5824
6950
  const lastError = state.stateId === 'failed'
5825
6951
  ? failedStateError(context)
5826
6952
  : normalizeErrorFull(context.lastError);
@@ -5828,24 +6954,45 @@ export function createXStatePlaybookRuntime(machine, spec) {
5828
6954
  const stateDescription = !hasUnresolvedReconciliation()
5829
6955
  ? stateDescriptionFor(state)
5830
6956
  : undefined;
6957
+ const actions = deriveControlActions(snapshot).map(({ action }) => action);
6958
+ const retry = actions.find(({ id, standing }) => id.startsWith('retry:') &&
6959
+ (standing ?? 'ready') === 'ready');
6960
+ const stoppedBoundary = recoveryCheckpoint === undefined ? undefined :
6961
+ currentEffectLedger().boundaries.slice(recoveryCheckpoint.boundaryPrefix)
6962
+ .filter((boundary) => runtimeBoundaryIsOwned(boundary) && boundary.sourceStateId === recoveryCheckpoint.stateId).at(-1);
6963
+ const recovery = recoveryCheckpoint === undefined ||
6964
+ (pendingQuestions.length === 0 && retry === undefined) ? undefined : {
6965
+ prompt: recoveryCheckpoint.prompt,
6966
+ ...(stoppedBoundary === undefined ? {} : { evidence: snapshotJsonValue({
6967
+ declaredResults: stoppedBoundary.sourceOutcomeSchema,
6968
+ ...(stoppedBoundary.finalText === undefined ? {} : { savedPlayerText: stoppedBoundary.finalText }),
6969
+ ...(stoppedBoundary.semanticCandidate === undefined ? {} : { selectedResult: stoppedBoundary.semanticCandidate }),
6970
+ ...(stoppedBoundary.physicalReceipt === undefined ? {} : { repositoryReceipt: stoppedBoundary.physicalReceipt }),
6971
+ }) }),
6972
+ ...(repairableReadOnlyBoundary() === undefined ? {} : {
6973
+ preparation: `This read-only step changed the repository. Restore its exact original HEAD and visible files before retrying; do not accept its saved result. Preserve user work. You may put generated outputs aside and prevent their return using local environment or ignore settings. Original observation: ${JSON.stringify(repairableReadOnlyBoundary().baseline)}. Observed after the failed step: ${JSON.stringify(repairableReadOnlyBoundary().after)}.`,
6974
+ }),
6975
+ ...(stateDescriptions.get(recoveryCheckpoint.stateId) === undefined ? {} :
6976
+ { description: stateDescriptions.get(recoveryCheckpoint.stateId) }),
6977
+ continuation: pendingQuestions.length === 0
6978
+ ? { kind: 'runtime', actionId: retry.id }
6979
+ : { kind: 'reply' },
6980
+ };
5831
6981
  return deepFreeze({
5832
6982
  state,
5833
6983
  ...(stateDescription === undefined ? {} : { stateDescription }),
5834
6984
  ...(projectedContext !== undefined
5835
6985
  ? { context: projectedContext }
5836
6986
  : {}),
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
- ],
6987
+ pendingQuestions: pendingQuestions.map((pending) => ({
6988
+ questionId: pending.questionId,
6989
+ asker: pending.asker,
6990
+ question: pending.question,
6991
+ sourceItem: pending.sourceItem,
6992
+ })),
5847
6993
  ...(lastError !== undefined ? { lastError } : {}),
5848
- actions: deriveControlActions(snapshot).map(({ action }) => action),
6994
+ actions,
6995
+ ...(recovery === undefined ? {} : { recovery }),
5849
6996
  });
5850
6997
  },
5851
6998
  // DR-029 / PBRT-52: revalidate the named action against the live
@@ -5893,6 +7040,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
5893
7040
  const callId = `apply-${++applyCallSequence}`;
5894
7041
  const position = { turnId, callId };
5895
7042
  activeTurnId = turnId;
7043
+ publicBoundarySequence += 1;
5896
7044
  beginAutomaticReplayBoundary();
5897
7045
  activeSignal = signal;
5898
7046
  activeAborts = abortReasonClassifier(signal);
@@ -6006,7 +7154,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
6006
7154
  accepted = true;
6007
7155
  try {
6008
7156
  let run;
6009
- if (candidate.unresolvedEffectAction === 'abandon') {
7157
+ if (candidate.retryJudgment !== undefined) {
7158
+ await recoverJudgment(candidate.retryJudgment, signal);
7159
+ run = runResultFor(settledOutcome(signal));
7160
+ }
7161
+ else if (candidate.unresolvedEffectAction === 'abandon') {
6010
7162
  signal.throwIfAborted();
6011
7163
  run = runResultFor('unresolved-effect');
6012
7164
  }
@@ -6025,6 +7177,21 @@ export function createXStatePlaybookRuntime(machine, spec) {
6025
7177
  ? 'no-action'
6026
7178
  : 'quiescent');
6027
7179
  }
7180
+ else if (candidate.retryStep) {
7181
+ if (candidate.restoreReadOnly !== undefined) {
7182
+ await confirmReadOnlyRestoration(candidate.restoreReadOnly, signal);
7183
+ }
7184
+ const checkpoint = recoveryCheckpoint;
7185
+ requiredRecoveryBaseline = currentEffectLedger().boundaries
7186
+ .slice(checkpoint.boundaryPrefix).find(({ restored }) => restored !== undefined)?.restored;
7187
+ try {
7188
+ await resumeCheckpoint(checkpoint);
7189
+ }
7190
+ finally {
7191
+ requiredRecoveryBaseline = undefined;
7192
+ }
7193
+ run = runResultFor(settledOutcome(signal));
7194
+ }
6028
7195
  else {
6029
7196
  actor.send(candidate.event);
6030
7197
  await waitForPlaybookQuiescence(actor, {
@@ -6037,6 +7204,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6037
7204
  receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
6038
7205
  ? {
6039
7206
  disposition: 'failed',
7207
+ run,
6040
7208
  error: ('error' in run ? run.error : undefined) ??
6041
7209
  normalizeError(new Error(`apply settled with outcome ${run.outcome}`)),
6042
7210
  }
@@ -6064,6 +7232,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6064
7232
  if (receipt !== undefined && receipt.disposition !== 'rejected') {
6065
7233
  appliedReceipts.set(key, receipt);
6066
7234
  }
7235
+ if (parallelProfile !== undefined)
7236
+ await drainBoundaryCalls();
6067
7237
  try {
6068
7238
  await drainEmissions();
6069
7239
  }
@@ -6143,7 +7313,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6143
7313
  return receipt;
6144
7314
  },
6145
7315
  unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
6146
- async handleBossInput({ text, signal, }) {
7316
+ async handleBossInput({ text, signal, onAccepted, }) {
6147
7317
  if (!actor || !savedPorts) {
6148
7318
  throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
6149
7319
  }
@@ -6157,6 +7327,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6157
7327
  refreshRetainedEffectFenceFromHost();
6158
7328
  const turnId = ++turnSequence;
6159
7329
  activeTurnId = turnId;
7330
+ publicBoundarySequence += 1;
6160
7331
  beginAutomaticReplayBoundary();
6161
7332
  activeSignal = signal;
6162
7333
  activeAborts = abortReasonClassifier(signal);
@@ -6196,7 +7367,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6196
7367
  const stateId = normalizePlaybookSnapshot(snapshot).stateId;
6197
7368
  const snapshotContext = (snapshot
6198
7369
  .context ?? {});
6199
- deferredPending = pendingBossQuestionForState(normalizePlaybookSnapshot(snapshot), snapshotContext);
7370
+ deferredPending = singlePendingBossQuestion(normalizePlaybookSnapshot(snapshot), snapshotContext);
6200
7371
  pendingRequiresDeferredBinding =
6201
7372
  deferredPending !== undefined &&
6202
7373
  outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred' &&
@@ -6246,14 +7417,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6246
7417
  deferredOperation !== undefined) {
6247
7418
  if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
6248
7419
  const statusLine = classificationStatus(event);
6249
- const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine);
7420
+ const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine, onAccepted);
6250
7421
  if (continuation === 'continued') {
6251
- try {
6252
- lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
6253
- }
6254
- catch {
6255
- lastBossEvent = undefined;
6256
- }
6257
7422
  if (controlPlaneError !== undefined) {
6258
7423
  throw controlPlaneError;
6259
7424
  }
@@ -6262,7 +7427,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
6262
7427
  : 'no-action');
6263
7428
  }
6264
7429
  else {
6265
- lastBossEvent = undefined;
6266
7430
  result = runResultFor('no-action');
6267
7431
  }
6268
7432
  handledDeferred = true;
@@ -6274,7 +7438,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
6274
7438
  }
6275
7439
  else {
6276
7440
  await parkBoundDeferredOperation(deferredOperation.operationId, signal);
6277
- lastBossEvent = undefined;
6278
7441
  result = runResultFor('no-action');
6279
7442
  handledDeferred = true;
6280
7443
  }
@@ -6305,16 +7468,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6305
7468
  suppressInspectionEmissions = false;
6306
7469
  actor.start();
6307
7470
  }
6308
- // DR-029: keep the classified event with its recorded payload
6309
- // as the retry-replay source. Recording is sanitizing, not
6310
- // load-bearing: an override classifier's non-JSON-safe event is
6311
- // simply not recorded, and the turn proceeds unchanged.
6312
- try {
6313
- lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
6314
- }
6315
- catch {
6316
- lastBossEvent = undefined;
6317
- }
7471
+ onAccepted?.();
6318
7472
  actor.send(event);
6319
7473
  await waitForPlaybookQuiescence(actor, {
6320
7474
  pendingCalls: nestedBridge,
@@ -6327,6 +7481,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6327
7481
  catch (error) {
6328
7482
  operationError = error;
6329
7483
  }
7484
+ if (parallelProfile !== undefined)
7485
+ await drainBoundaryCalls();
6330
7486
  let drainError;
6331
7487
  try {
6332
7488
  await drainEmissions();
@@ -6415,6 +7571,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
6415
7571
  return runResultFor('no-action');
6416
7572
  }
6417
7573
  activeTurnId = playbookCallTurnIds.get(input.callId);
7574
+ publicBoundarySequence += 1;
6418
7575
  bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
6419
7576
  ? playbookCallEffectPrefixes.get(input.callId)
6420
7577
  : hasGovernedPlayerStates
@@ -6457,6 +7614,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
6457
7614
  error: normalizeError(input.signal.reason),
6458
7615
  };
6459
7616
  }
7617
+ if (parallelProfile !== undefined)
7618
+ await drainBoundaryCalls();
6460
7619
  let drainError;
6461
7620
  try {
6462
7621
  await drainEmissions();
@@ -6570,10 +7729,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
6570
7729
  emissionQueue.clear();
6571
7730
  judgeQueue.clear();
6572
7731
  appliedReceipts.clear();
7732
+ pendingCohorts.clear();
7733
+ consumedCohortBoundaries.clear();
7734
+ activeBoundaryCalls.clear();
7735
+ cohortCompletionQueue.clear();
6573
7736
  actor = undefined;
6574
7737
  activeSignal = undefined;
6575
7738
  activeAborts = undefined;
6576
- actorSettlementAborts = undefined;
7739
+ actorSettlementAborts.length = 0;
6577
7740
  actorSettlementErrorAborts = undefined;
6578
7741
  activeAbortEmission = undefined;
6579
7742
  activeTurnId = undefined;
@@ -6585,7 +7748,6 @@ export function createXStatePlaybookRuntime(machine, spec) {
6585
7748
  failedGovernedAttemptId = undefined;
6586
7749
  controlPlaneError = undefined;
6587
7750
  emissionFailure = undefined;
6588
- lastBossEvent = undefined;
6589
7751
  retainedEffectSourceSessionId = undefined;
6590
7752
  retainedEffectReconciliation = undefined;
6591
7753
  retainedEffectReconciliationRequired = false;