@sublang/playbook 8.0.0 → 10.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 (66) hide show
  1. package/README.md +3 -3
  2. package/docs/cli.md +66 -23
  3. package/docs/configuration.md +13 -8
  4. package/docs/embedding.md +45 -14
  5. package/package.json +7 -3
  6. package/reference/sdlc/captain.md +14 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  10. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
  14. package/reference/sdlc/code.md +0 -1
  15. package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
  16. package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
  17. package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
  18. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  19. package/reference/sdlc/code.playbook/bin/run.js +365 -63
  20. package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
  21. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
  22. package/reference/sdlc/code.playbook/code.fsm.js +85 -29
  23. package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
  24. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  25. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  26. package/reference/sdlc/code.playbook/code.playbook.js +67 -4
  27. package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
  28. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  29. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  30. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  31. package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
  32. package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
  33. package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
  34. package/reference/sdlc/decide.md +0 -1
  35. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
  36. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  37. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  38. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  39. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +15 -5
  40. package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
  41. package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
  42. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  43. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  44. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  45. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  46. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  47. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  48. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  49. package/reference/sdlc/review.playbook/review.playbook.js +78 -4
  50. package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
  51. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  52. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  53. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  54. package/slc/gears2fsm.md +25 -7
  55. package/slc/link.md +727 -82
  56. package/src/accepted-outcome.d.ts +18 -0
  57. package/src/accepted-outcome.js +94 -0
  58. package/src/accepted-outcome.ts +140 -0
  59. package/src/runtime.d.ts +165 -3
  60. package/src/runtime.ts +214 -2
  61. package/src/xstate-playbook-runtime.d.ts +162 -13
  62. package/src/xstate-playbook-runtime.js +3344 -564
  63. package/src/xstate-playbook-runtime.ts +4873 -637
  64. package/src/xstate-runtime.d.ts +76 -8
  65. package/src/xstate-runtime.js +1001 -64
  66. package/src/xstate-runtime.ts +1640 -91
@@ -5,7 +5,8 @@
5
5
  // machinery that slc/link.md previously regenerated inside every linked
6
6
  // `<name>.playbook.ts` artifact — actor wiring, boundary tracing, judge
7
7
  // classification/adjudication, script execution, nested-playbook bridging,
8
- // Boss-reply suspension, snapshot/restore, and disposal — lives here once.
8
+ // Boss-reply suspension, snapshot restore/adoption, and disposal — lives here
9
+ // once.
9
10
  // A linked artifact supplies only its per-workflow `spec` (options
10
11
  // validation and any strategy overrides) and its own FSM; the factory
11
12
  // interprets the FSM data the artifact already carries.
@@ -15,9 +16,12 @@
15
16
  // its behavior tests are the equivalence proof. Do not change observable
16
17
  // behavior here without consulting those suites.
17
18
  import { spawn } from 'node:child_process';
19
+ import { randomUUID } from 'node:crypto';
20
+ import { isDeepStrictEqual } from 'node:util';
18
21
  import PQueue from 'p-queue';
19
22
  import { createActor, fromPromise } from 'xstate';
20
- import { assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
23
+ import { createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
24
+ import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
21
25
  export const BOSS_REPLY_ERRORS = {
22
26
  missingQuestion: "needsBossReply outcome missing 'question' field",
23
27
  unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
@@ -56,6 +60,95 @@ function isEmptyFinalText(finalText) {
56
60
  return finalText === undefined || finalText.trim().length === 0;
57
61
  }
58
62
  const emptyOkRetryFailures = new WeakSet();
63
+ const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
64
+ const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
65
+ const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
66
+ function deferredValue() {
67
+ let resolve;
68
+ let reject;
69
+ const promise = new Promise((resolvePromise, rejectPromise) => {
70
+ resolve = resolvePromise;
71
+ reject = rejectPromise;
72
+ });
73
+ return { promise, resolve, reject };
74
+ }
75
+ function assertNoConfiguredHostCapabilities(value, label) {
76
+ if (value !== null &&
77
+ typeof value === 'object' &&
78
+ Object.prototype.hasOwnProperty.call(value, HOST_CAPABILITIES_OPTION_KEY)) {
79
+ throw new TypeError(`${label} configured options must not contain hostCapabilities`);
80
+ }
81
+ }
82
+ function configuredOptionsFromFactoryInput(value, label) {
83
+ if (value === null ||
84
+ typeof value !== 'object' ||
85
+ Array.isArray(value) ||
86
+ (Object.getPrototypeOf(value) !== Object.prototype &&
87
+ Object.getPrototypeOf(value) !== null)) {
88
+ throw new TypeError(`${label} schema-3 factory input must be a plain object`);
89
+ }
90
+ const descriptors = Object.getOwnPropertyDescriptors(value);
91
+ const keys = Reflect.ownKeys(value);
92
+ if (keys.length !== 2 ||
93
+ !keys.includes('configuredOptions') ||
94
+ !keys.includes(HOST_CAPABILITIES_OPTION_KEY) ||
95
+ keys.some((key) => {
96
+ const descriptor = descriptors[key];
97
+ return (descriptor?.get !== undefined ||
98
+ descriptor?.set !== undefined ||
99
+ descriptor?.enumerable !== true ||
100
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value'));
101
+ })) {
102
+ throw new TypeError(`${label} schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties`);
103
+ }
104
+ const hostCapabilities = descriptors.hostCapabilities.value;
105
+ if (hostCapabilities === null ||
106
+ typeof hostCapabilities !== 'object' ||
107
+ Array.isArray(hostCapabilities)) {
108
+ throw new TypeError(`${label} schema-3 factory input hostCapabilities must be a live object`);
109
+ }
110
+ const configuredOptions = descriptors.configuredOptions.value;
111
+ assertNoConfiguredHostCapabilities(configuredOptions, label);
112
+ const ledgerDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'effectLedger');
113
+ if (ledgerDescriptor === undefined ||
114
+ !Object.prototype.hasOwnProperty.call(ledgerDescriptor, 'value') ||
115
+ ledgerDescriptor.get !== undefined ||
116
+ ledgerDescriptor.set !== undefined) {
117
+ throw new TypeError(`${label} schema-3 factory input hostCapabilities.effectLedger must be an own data property`);
118
+ }
119
+ const effectLedger = ledgerDescriptor.value;
120
+ if (effectLedger === null ||
121
+ typeof effectLedger !== 'object' ||
122
+ Array.isArray(effectLedger) ||
123
+ typeof effectLedger.snapshot !== 'function' ||
124
+ typeof effectLedger.writeAhead !== 'function') {
125
+ throw new TypeError(`${label} schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions`);
126
+ }
127
+ return {
128
+ configuredOptions,
129
+ hostCapabilities,
130
+ effectLedger: effectLedger,
131
+ };
132
+ }
133
+ function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
134
+ const descriptor = hostCapabilities === undefined
135
+ ? undefined
136
+ : Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
137
+ const repository = descriptor?.value;
138
+ if (descriptor === undefined ||
139
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value') ||
140
+ descriptor.get !== undefined ||
141
+ descriptor.set !== undefined ||
142
+ repository === null ||
143
+ typeof repository !== 'object' ||
144
+ Array.isArray(repository) ||
145
+ typeof repository.runExclusive !==
146
+ 'function' ||
147
+ typeof repository.runDeferred !== 'function') {
148
+ throw new TypeError(`${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`);
149
+ }
150
+ return repository;
151
+ }
59
152
  function markEmptyOkRetryFailure(error) {
60
153
  emptyOkRetryFailures.add(error);
61
154
  return error;
@@ -78,7 +171,7 @@ function isEmptyOkRetryFailure(error) {
78
171
  export const RUNTIME_ABI = 1;
79
172
  /** The linked-artifact schema versions this engine accepts (DR-022). */
80
173
  export const SUPPORTED_ARTIFACT_SCHEMAS = Object.freeze([
81
- 2,
174
+ 3,
82
175
  ]);
83
176
  // PBRT-50: validate a declaration against the loaded engine, schema first,
84
177
  // so one clear diagnostic covers a fully skewed artifact. Declaration-free
@@ -106,6 +199,7 @@ function assertRuntimeCompat(compat, label) {
106
199
  throw new TypeError(`${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
107
200
  `@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`);
108
201
  }
202
+ return artifactSchema;
109
203
  }
110
204
  // ---------------------------------------------------------------------------
111
205
  // Tolerant judge-JSON recovery (slc/link.md §Boss-event mapping).
@@ -233,9 +327,37 @@ export function normalizeErrorFull(err) {
233
327
  return undefined;
234
328
  return normalizeError(err);
235
329
  }
330
+ // slc/link.md §Abort: cancellation is causal identity with the applicable
331
+ // signal's reason — never an `AbortError` name, never bare signal state. A
332
+ // distinct failure observed while the signal is aborted stays a non-abort
333
+ // control error and takes precedence (mirrors DECIDE's bespoke reference).
236
334
  function isAbortFailure(error, signal) {
237
- return (signal.aborted &&
238
- (error === signal.reason || normalizeError(error).name === 'AbortError'));
335
+ return signal.aborted && Object.is(error, signal.reason);
336
+ }
337
+ function abortReasonClassifier(...sources) {
338
+ const captured = sources.filter((source) => source !== undefined);
339
+ return Object.freeze({
340
+ isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
341
+ ? isAbortFailure(error, source)
342
+ : source.isAbortReason(error)),
343
+ });
344
+ }
345
+ /**
346
+ * gears2fsm's canonical Boss-reply wait state. On the runtime's Boss-facing
347
+ * surfaces — state telemetry, status lines, the exported snapshot, and the
348
+ * control view — a context question counts as *pending* only while the
349
+ * machine sits in this state awaiting the reply. Later states retain the
350
+ * answered question in context (the resumed player prompt is composed from
351
+ * it), so an unconditional projection would resurrect it: a failure the
352
+ * resumed player reached would export a question nobody is waiting on,
353
+ * disagreeing with the gated telemetry a mirroring host's ledger follows
354
+ * and failing the shell's snapshot-equality settlement check.
355
+ */
356
+ const BOSS_REPLY_WAIT_STATE_ID = 'awaitBossReply';
357
+ function pendingBossQuestionForState(state, context) {
358
+ if (state.stateId !== BOSS_REPLY_WAIT_STATE_ID)
359
+ return undefined;
360
+ return pendingBossQuestionFromContext(context);
239
361
  }
240
362
  /** Read the FSM context's single pending Boss question, when well-formed. */
241
363
  export function pendingBossQuestionFromContext(context) {
@@ -333,6 +455,97 @@ function sortJson(value) {
333
455
  function stableJson(value, path) {
334
456
  return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
335
457
  }
458
+ // DR-040 task 8: a retained checkpoint authorizes adoption without a replay
459
+ // fence only when the authoritative ledger preserves the checkpoint exactly,
460
+ // has made no deferred-operation progress, and every later physical boundary
461
+ // is complete and proves `unchanged`. This is intentionally the same
462
+ // fail-closed shape as uncertain whole-turn replay.
463
+ function retainedAdoptionCheckpointIsSafe(checkpoint, current) {
464
+ if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
465
+ return false;
466
+ }
467
+ if (!isPlaybookEffectLedgerMonotonicExtension(checkpoint, current)) {
468
+ return false;
469
+ }
470
+ if (!isDeepStrictEqual(current.boundaries.slice(0, checkpoint.boundaries.length), checkpoint.boundaries) ||
471
+ !isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)) {
472
+ return false;
473
+ }
474
+ return current.boundaries
475
+ .slice(checkpoint.boundaries.length)
476
+ .every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged');
477
+ }
478
+ const RETAINED_EFFECT_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
479
+ function requireAdoptionIdentity(value, path) {
480
+ if (typeof value !== 'string' || value.trim().length === 0) {
481
+ throw new TypeError(`${path} must be a non-empty string`);
482
+ }
483
+ return value;
484
+ }
485
+ // DR-038 §5: capture the host-owned source lineage before adoption binds
486
+ // anything. The retained stack already carries both identities: a frame's
487
+ // sessionId names its source runtime, while the common rootSessionId names the
488
+ // retained generation. A suspended parent also needs the host's freshly
489
+ // allocated target child id so its bridge can be re-keyed without leaking a
490
+ // source-session id into the new engagement.
491
+ function snapshotAdoptionContext(value, targetSession, sourceSnapshot) {
492
+ const captured = snapshotJsonValue(value, 'playbook adoption context');
493
+ if (captured === null ||
494
+ Array.isArray(captured) ||
495
+ typeof captured !== 'object') {
496
+ throw new TypeError('playbook adoption context must be an object');
497
+ }
498
+ const fields = captured;
499
+ const hasSuspendedCall = sourceSnapshot.suspendedCall !== undefined;
500
+ const expectedKeys = [
501
+ 'sourceGenerationId',
502
+ 'sourceSessionId',
503
+ ...(hasSuspendedCall ? ['targetChildSessionId'] : []),
504
+ ].sort();
505
+ const actualKeys = Object.keys(fields).sort();
506
+ if (actualKeys.length !== expectedKeys.length ||
507
+ actualKeys.some((key, index) => key !== expectedKeys[index])) {
508
+ throw new TypeError(`playbook adoption context must contain exactly ${expectedKeys.join(', ')}`);
509
+ }
510
+ const sourceSessionId = requireAdoptionIdentity(fields.sourceSessionId, 'playbook adoption context sourceSessionId');
511
+ const sourceGenerationId = requireAdoptionIdentity(fields.sourceGenerationId, 'playbook adoption context sourceGenerationId');
512
+ const sourceIsRoot = sourceSessionId === sourceGenerationId;
513
+ if ((targetSession.depth === 0) !== sourceIsRoot) {
514
+ throw new TypeError('playbook adoption context source identities do not match the target frame depth');
515
+ }
516
+ const targetChildSessionId = hasSuspendedCall
517
+ ? requireAdoptionIdentity(fields.targetChildSessionId, 'playbook adoption context targetChildSessionId')
518
+ : undefined;
519
+ const sourceIds = new Set([
520
+ sourceSessionId,
521
+ sourceGenerationId,
522
+ ...(sourceSnapshot.suspendedCall === undefined
523
+ ? []
524
+ : [sourceSnapshot.suspendedCall.childSessionId]),
525
+ ]);
526
+ const targetIds = [
527
+ targetSession.sessionId,
528
+ targetSession.rootSessionId,
529
+ ...(targetSession.parentSessionId === undefined
530
+ ? []
531
+ : [targetSession.parentSessionId]),
532
+ ...(targetChildSessionId === undefined ? [] : [targetChildSessionId]),
533
+ ];
534
+ if (targetIds.some((identity) => sourceIds.has(identity))) {
535
+ throw new TypeError('playbook adoption target identities must be fresh from the source generation');
536
+ }
537
+ if (targetChildSessionId !== undefined &&
538
+ (targetChildSessionId === targetSession.sessionId ||
539
+ targetChildSessionId === targetSession.rootSessionId ||
540
+ targetChildSessionId === targetSession.parentSessionId)) {
541
+ throw new TypeError('playbook adoption targetChildSessionId must name a fresh child frame');
542
+ }
543
+ return Object.freeze({
544
+ sourceSessionId,
545
+ sourceGenerationId,
546
+ ...(targetChildSessionId === undefined ? {} : { targetChildSessionId }),
547
+ });
548
+ }
336
549
  /**
337
550
  * Default direct-Captain prompt composer (slc/link.md §Captain prompt
338
551
  * composition). Placeholder substitution is presence-based: string fields
@@ -402,6 +615,43 @@ export function defaultBuildJudgePrompt(input, finalText) {
402
615
  }
403
616
  return lines.join('\n');
404
617
  }
618
+ function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
619
+ const lines = [
620
+ 'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
621
+ 'Decide only from the supplied player output and declared outcomes.',
622
+ 'Reply with exactly one JSON object and no prose.',
623
+ '',
624
+ `The ${input.role} role just produced this output:`,
625
+ '',
626
+ '```',
627
+ finalText,
628
+ '```',
629
+ '',
630
+ 'Pick exactly one declared `guard`. Include every semantic-owned field for that guard and no other field.',
631
+ 'Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from their authoritative evidence.',
632
+ '',
633
+ ];
634
+ for (const [guard, description] of Object.entries(input.result)) {
635
+ const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
636
+ .filter(([, authority]) => authority === 'semantic')
637
+ .map(([field]) => field);
638
+ lines.push(`- \`${guard}\` — semantic fields: ${semanticFields.length === 0
639
+ ? '(none)'
640
+ : semanticFields.map((field) => `\`${field}\``).join(', ')}; ${description}`);
641
+ }
642
+ if (correction !== undefined) {
643
+ lines.push('', 'Your first reply was structurally invalid:', '', '```', correction.reply, '```', '', `Validation error: ${correction.error}`, 'Correct only that structure using the same player output and outcome schema.');
644
+ }
645
+ return lines.join('\n');
646
+ }
647
+ function parseGovernedSemanticCandidate(raw) {
648
+ try {
649
+ return parseJudgeJson(raw);
650
+ }
651
+ catch (error) {
652
+ throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
653
+ }
654
+ }
405
655
  const NO_VERBATIM_FIELDS = new Set();
406
656
  /**
407
657
  * LLM-judge adjudicator for delegated players. Coerces the player's
@@ -434,7 +684,12 @@ export async function adjudicatePlayerOutput(spec, input, finalText, ports, sign
434
684
  const verbatim = finalText.trim();
435
685
  for (const field of extractFields(input.result[guard])) {
436
686
  if (verbatimFields.has(field)) {
437
- obj[field] = verbatim;
687
+ Object.defineProperty(obj, field, {
688
+ value: verbatim,
689
+ enumerable: true,
690
+ configurable: true,
691
+ writable: true,
692
+ });
438
693
  continue;
439
694
  }
440
695
  if (typeof obj[field] !== 'string') {
@@ -462,18 +717,23 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
462
717
  let roleId;
463
718
  let prompt;
464
719
  try {
720
+ spec.validateInput?.(input);
465
721
  roleId = spec.resolveRoleId(input);
466
722
  prompt = spec.composePlayerPrompt(input);
467
723
  }
468
724
  catch (error) {
469
- onControlPlaneError?.(error);
725
+ if (!isAbortFailure(error, activeSignal)) {
726
+ onControlPlaneError?.(error);
727
+ }
470
728
  throw error;
471
729
  }
472
730
  const callPlayer = (resume) => boundary
473
731
  ? boundary.callPlayer(input, roleId, prompt, activeSignal)
474
732
  : ports.callPlayer(roleId, prompt, activeSignal, { resume });
475
733
  let result = await callPlayer(false);
476
- if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
734
+ if (result.status === 'ok' &&
735
+ isEmptyFinalText(result.finalText) &&
736
+ (spec.allowsCorrectiveReplay?.(result) ?? true)) {
477
737
  // An abort that lands between the empty first result and the
478
738
  // corrective call ends the turn as ordinary abort settlement with
479
739
  // no second host call — aborts are never retried (DR-028 via
@@ -501,12 +761,22 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
501
761
  throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
502
762
  }
503
763
  try {
504
- const output = await adjudicatePlayerOutput(spec.adjudication, input, finalText, ports, activeSignal, boundary);
764
+ const governed = boundary?.takeGovernedPlayerOutput?.(result);
765
+ if (governed?.status === 'unresolved') {
766
+ throw governed.error;
767
+ }
768
+ const output = governed?.status === 'resolved'
769
+ ? governed.output
770
+ : await adjudicatePlayerOutput(spec.adjudication, input, finalText, ports, activeSignal, boundary);
771
+ boundary?.recordGovernedPlayerOutput?.(result, output);
505
772
  validateBossReplyOutput(input, output, spec.resumableStateIds);
506
773
  return output;
507
774
  }
508
775
  catch (error) {
509
- onControlPlaneError?.(error);
776
+ if (!isAbortFailure(error, activeSignal) &&
777
+ !isFsmResultFailure(error)) {
778
+ onControlPlaneError?.(error);
779
+ }
510
780
  throw error;
511
781
  }
512
782
  });
@@ -670,7 +940,7 @@ export function resumableStateIdsFromMachine(machine) {
670
940
  if (!isPlainObject(config) || !isPlainObject(config.states)) {
671
941
  return new Set();
672
942
  }
673
- const awaitState = config.states.awaitBossReply;
943
+ const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
674
944
  if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
675
945
  return new Set();
676
946
  }
@@ -772,6 +1042,28 @@ function deepFreeze(value) {
772
1042
  // Default transition/status derivation.
773
1043
  // ---------------------------------------------------------------------------
774
1044
  const SUPPRESSED_ENTRY_STATES = new Set(['ready', 'done']);
1045
+ // Bounded escalation for aborted script process groups: SIGTERM first, then
1046
+ // SIGKILL after this grace, so settlement (gated on the shell's own exit)
1047
+ // stays bounded even for TERM-immune commands.
1048
+ const SCRIPT_ABORT_KILL_GRACE_MS = 2000;
1049
+ class ScriptProcessGroupTeardownError extends Error {
1050
+ constructor(pid, message, cause) {
1051
+ super(`script process group ${pid} teardown could not be confirmed: ${message}`, cause === undefined ? undefined : { cause });
1052
+ this.name = 'ScriptProcessGroupTeardownError';
1053
+ }
1054
+ }
1055
+ function isNoSuchProcess(error) {
1056
+ return (typeof error === 'object' &&
1057
+ error !== null &&
1058
+ 'code' in error &&
1059
+ error.code === 'ESRCH');
1060
+ }
1061
+ function isProcessPermissionDenied(error) {
1062
+ return (typeof error === 'object' &&
1063
+ error !== null &&
1064
+ 'code' in error &&
1065
+ error.code === 'EPERM');
1066
+ }
775
1067
  function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
776
1068
  return (event) => {
777
1069
  if (event === null || typeof event !== 'object') {
@@ -796,7 +1088,7 @@ function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
796
1088
  }
797
1089
  function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
798
1090
  if (value === undefined) {
799
- throw new TypeError(`${label} roleStates must be supplied for schema 2`);
1091
+ throw new TypeError(`${label} roleStates must be supplied for schema 3`);
800
1092
  }
801
1093
  const captured = snapshotJsonValue(value, `${label} roleStates`);
802
1094
  if (!isPlainObject(captured)) {
@@ -840,28 +1132,194 @@ function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
840
1132
  }
841
1133
  return statuses;
842
1134
  }
843
- function settlingGuard(event) {
844
- if (!isPlainObject(event) || !isPlainObject(event.output))
845
- return undefined;
846
- const guard = event.output.guard;
847
- return typeof guard === 'string' && guard.trim().length > 0
848
- ? guard
849
- : undefined;
1135
+ const OUTCOME_FIELD_AUTHORITIES = new Set([
1136
+ 'presentation',
1137
+ 'semantic',
1138
+ 'effect',
1139
+ 'runtime',
1140
+ ]);
1141
+ const REPOSITORY_DISPOSITIONS = new Set([
1142
+ 'unchanged',
1143
+ 'one-descendant-commit',
1144
+ 'deferred',
1145
+ ]);
1146
+ const OUTCOME_FIELD_KEY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1147
+ const SEMANTIC_PAYLOAD_FIELDS = new Set([
1148
+ 'irNumber',
1149
+ 'irTask',
1150
+ ]);
1151
+ function requireExactObjectKeys(value, expected, path) {
1152
+ const actual = Object.keys(value);
1153
+ const missing = expected.filter((key) => !actual.includes(key));
1154
+ const extra = actual.filter((key) => !expected.includes(key));
1155
+ if (missing.length === 0 && extra.length === 0)
1156
+ return;
1157
+ throw new TypeError(`${path} must contain exactly ${expected.join(', ')}` +
1158
+ (missing.length === 0 ? '' : `; missing ${missing.join(', ')}`) +
1159
+ (extra.length === 0 ? '' : `; unknown ${extra.join(', ')}`));
1160
+ }
1161
+ function requireAuthorityIdentifier(value, path) {
1162
+ if (!OUTCOME_FIELD_KEY_PATTERN.test(value)) {
1163
+ throw new TypeError(`${path} must be an identifier`);
1164
+ }
1165
+ }
1166
+ function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPayloadFields) {
1167
+ const path = `${label} outcomeAuthority`;
1168
+ if (descriptor === undefined ||
1169
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value') ||
1170
+ descriptor.enumerable !== true) {
1171
+ throw new TypeError(`${path} must be an own enumerable data property for schema 3`);
1172
+ }
1173
+ const captured = snapshotJsonValue(descriptor.value, path);
1174
+ if (!isPlainObject(captured)) {
1175
+ throw new TypeError(`${path} must be an object`);
1176
+ }
1177
+ requireExactObjectKeys(captured, ['governedPlayerStates'], path);
1178
+ const governed = captured.governedPlayerStates;
1179
+ if (!isPlainObject(governed)) {
1180
+ throw new TypeError(`${path}.governedPlayerStates must be an object`);
1181
+ }
1182
+ for (const stateId of playerStates.keys()) {
1183
+ if (!Object.prototype.hasOwnProperty.call(governed, stateId)) {
1184
+ throw new TypeError(`${path}.governedPlayerStates must declare player state ${stateId}`);
1185
+ }
1186
+ }
1187
+ for (const stateId of Object.keys(governed)) {
1188
+ if (!playerStates.has(stateId)) {
1189
+ throw new TypeError(`${path}.governedPlayerStates.${stateId} does not name a player state`);
1190
+ }
1191
+ }
1192
+ const usedVerbatimFields = new Set();
1193
+ const normalizedStates = Object.create(null);
1194
+ for (const [stateId, rawOutcomes] of Object.entries(governed)) {
1195
+ const statePath = `${path}.governedPlayerStates.${stateId}`;
1196
+ if (!isPlainObject(rawOutcomes) || Object.keys(rawOutcomes).length === 0) {
1197
+ throw new TypeError(`${statePath} must declare at least one outcome`);
1198
+ }
1199
+ const outcomes = Object.create(null);
1200
+ for (const [outcome, rawSpec] of Object.entries(rawOutcomes)) {
1201
+ requireAuthorityIdentifier(outcome, `${statePath} outcome key`);
1202
+ const outcomePath = `${statePath}.${outcome}`;
1203
+ if (!isPlainObject(rawSpec)) {
1204
+ throw new TypeError(`${outcomePath} must be an object`);
1205
+ }
1206
+ requireExactObjectKeys(rawSpec, ['fields', 'repositoryDisposition'], outcomePath);
1207
+ if (!isPlainObject(rawSpec.fields)) {
1208
+ throw new TypeError(`${outcomePath}.fields must be an object`);
1209
+ }
1210
+ const fields = Object.create(null);
1211
+ for (const [field, authority] of Object.entries(rawSpec.fields)) {
1212
+ requireAuthorityIdentifier(field, `${outcomePath}.fields key`);
1213
+ if (field === 'guard') {
1214
+ throw new TypeError(`${outcomePath}.fields.guard is not allowed; the outcome key owns the semantic discriminator`);
1215
+ }
1216
+ if (typeof authority !== 'string' ||
1217
+ !OUTCOME_FIELD_AUTHORITIES.has(authority)) {
1218
+ throw new TypeError(`${outcomePath}.fields.${field} must name presentation, semantic, effect, or runtime authority`);
1219
+ }
1220
+ const requiredAuthorities = new Set();
1221
+ if (field === 'latestCommit')
1222
+ requiredAuthorities.add('effect');
1223
+ if (SEMANTIC_PAYLOAD_FIELDS.has(field)) {
1224
+ requiredAuthorities.add('semantic');
1225
+ }
1226
+ if (field === 'question' || verbatimPayloadFields.has(field)) {
1227
+ requiredAuthorities.add('presentation');
1228
+ }
1229
+ if (requiredAuthorities.size > 1) {
1230
+ throw new TypeError(`${outcomePath}.fields.${field} has conflicting linker authority requirements`);
1231
+ }
1232
+ const requiredAuthority = [...requiredAuthorities][0];
1233
+ if (requiredAuthority !== undefined && authority !== requiredAuthority) {
1234
+ throw new TypeError(`${outcomePath}.fields.${field} must use ${requiredAuthority} authority`);
1235
+ }
1236
+ if (verbatimPayloadFields.has(field))
1237
+ usedVerbatimFields.add(field);
1238
+ fields[field] = authority;
1239
+ }
1240
+ const disposition = rawSpec.repositoryDisposition;
1241
+ if (typeof disposition !== 'string' ||
1242
+ !REPOSITORY_DISPOSITIONS.has(disposition)) {
1243
+ throw new TypeError(`${outcomePath}.repositoryDisposition must be unchanged, one-descendant-commit, or deferred`);
1244
+ }
1245
+ if (disposition !== 'one-descendant-commit' &&
1246
+ Object.values(fields).includes('effect')) {
1247
+ throw new TypeError(`${outcomePath} may declare effect-owned fields only for one-descendant-commit`);
1248
+ }
1249
+ outcomes[outcome] = Object.freeze({
1250
+ fields: Object.freeze(fields),
1251
+ repositoryDisposition: disposition,
1252
+ });
1253
+ }
1254
+ for (const [outcome, outcomeSpec] of Object.entries(outcomes)) {
1255
+ if (outcomeSpec.repositoryDisposition !== 'deferred')
1256
+ continue;
1257
+ if (outcome !== 'needsBossReply') {
1258
+ throw new TypeError(`${statePath}.${outcome} may use deferred only for needsBossReply`);
1259
+ }
1260
+ if (outcomeSpec.fields.question !== 'presentation') {
1261
+ throw new TypeError(`${statePath}.needsBossReply deferred outcome must declare presentation-owned question`);
1262
+ }
1263
+ if (!Object.entries(outcomes).some(([other, candidate]) => other !== outcome &&
1264
+ candidate.repositoryDisposition === 'one-descendant-commit')) {
1265
+ throw new TypeError(`${statePath}.needsBossReply deferred outcome requires another one-descendant-commit outcome`);
1266
+ }
1267
+ }
1268
+ normalizedStates[stateId] = Object.freeze(outcomes);
1269
+ }
1270
+ for (const field of verbatimPayloadFields) {
1271
+ requireAuthorityIdentifier(field, `${path} verbatimPayloadFields entry`);
1272
+ if (!usedVerbatimFields.has(field)) {
1273
+ throw new TypeError(`${path} verbatimPayloadFields entry ${field} is absent from governed payload fields`);
1274
+ }
1275
+ }
1276
+ return Object.freeze({
1277
+ governedPlayerStates: Object.freeze(normalizedStates),
1278
+ });
1279
+ }
1280
+ function sameStringSet(left, right) {
1281
+ if (left.length !== right.length)
1282
+ return false;
1283
+ const expected = new Set(right);
1284
+ return left.every((value) => expected.has(value));
1285
+ }
1286
+ function assertGovernedPlayerInput(authority, input, extractFields, label) {
1287
+ if (authority === undefined)
1288
+ return;
1289
+ const state = authority.governedPlayerStates[input.stateId];
1290
+ if (state === undefined) {
1291
+ throw new TypeError(`${label} outcomeAuthority has no governed player state ${input.stateId}`);
1292
+ }
1293
+ const actualOutcomes = Object.keys(input.result);
1294
+ const governedOutcomes = Object.keys(state);
1295
+ if (!sameStringSet(actualOutcomes, governedOutcomes)) {
1296
+ throw new TypeError(`${label} outcomeAuthority for ${input.stateId} must exactly match outcomes ` +
1297
+ governedOutcomes.join(', '));
1298
+ }
1299
+ for (const outcome of governedOutcomes) {
1300
+ const description = input.result[outcome];
1301
+ if (typeof description !== 'string') {
1302
+ throw new TypeError(`${label} player outcome ${input.stateId}.${outcome} must have a string description`);
1303
+ }
1304
+ const describedFields = [...new Set(extractFields(description))];
1305
+ const candidateFields = Object.keys(state[outcome].fields);
1306
+ if (!sameStringSet(describedFields, candidateFields)) {
1307
+ throw new TypeError(`${label} outcomeAuthority fields for ${input.stateId}.${outcome} ` +
1308
+ 'must exactly match its described output fields');
1309
+ }
1310
+ }
850
1311
  }
851
1312
  function askerLabel(asker) {
852
1313
  return asker.kind === 'captain' ? 'Captain' : asker.roleId;
853
1314
  }
854
1315
  function makeDefaultStatusesForState(roleStates) {
855
- return (state, context, event) => {
1316
+ return (state, context) => {
856
1317
  const statuses = [];
857
- const guard = settlingGuard(event);
858
- if (guard !== undefined)
859
- statuses.push({ message: `→ ${guard}` });
860
1318
  const stateId = state.stateId;
861
1319
  if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) {
862
1320
  return statuses;
863
1321
  }
864
- if (stateId === 'awaitBossReply') {
1322
+ if (stateId === BOSS_REPLY_WAIT_STATE_ID) {
865
1323
  const pending = pendingBossQuestionFromContext(context);
866
1324
  if (pending === undefined) {
867
1325
  return [...statuses, { message: 'Awaiting Boss reply.' }];
@@ -1063,7 +1521,14 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
1063
1521
  const state = classifierState(snapshotOrState);
1064
1522
  const stateId = typeof state.value === 'string' ? state.value : undefined;
1065
1523
  const currentState = stateId ?? JSON.stringify(state.value ?? null);
1066
- const pending = pendingBossQuestionFromContext(state.context);
1524
+ // The classifier shares the reply-wait pendingness of every other
1525
+ // surface: outside the wait, a context question a later state retains
1526
+ // is answered history, so the prompt must not present it as pending —
1527
+ // a judge told a question awaits at the failure state is steered toward
1528
+ // a reply it cannot select or toward no action at all.
1529
+ const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
1530
+ ? pendingBossQuestionFromContext(state.context)
1531
+ : undefined;
1067
1532
  const configuredTypes = configuredEventTypesForState(machine, stateId);
1068
1533
  const applicable = [...contracts.values()].filter((contract) => configuredTypes.has(contract.type) &&
1069
1534
  (contract.type !== 'BOSS_REPLY' || pending !== undefined));
@@ -1179,32 +1644,114 @@ function machineDeclaresParallelState(machine) {
1179
1644
  };
1180
1645
  return visit(machine.config);
1181
1646
  }
1182
- /**
1183
- * Build a `PlaybookRuntimeFactory` that interprets the given FSM artifact
1184
- * under the slc/link.md contract. The factory provides every actor kind the
1185
- * machine declares `player`, `script`, `captain`, and nested `playbook`
1186
- * (literal and dynamic) and implements the full runtime lifecycle including
1187
- * the optional parked-session snapshot capability (DR-014).
1188
- *
1189
- * Scope: machines that declare no parallel state (each snapshot exposes
1190
- * exactly one playbook state id). Parallel-region FSMs keep their own linked
1191
- * runtimes.
1192
- */
1647
+ // PBRT-52: the factory's domain is FLAT single-region machines — every
1648
+ // state a direct child of the root, so each snapshot exposes exactly one
1649
+ // playbook state id and every state-keyed lookup (deterministic entries,
1650
+ // retry, reply-wait pendingness, configured events, descriptions) indexes
1651
+ // one unambiguous identity. A compound child would be accepted and then
1652
+ // silently misbehave on all of those gates, so it is rejected up front
1653
+ // exactly like a parallel region.
1654
+ function machineDeclaresNestedState(machine) {
1655
+ const config = machine.config;
1656
+ if (!isPlainObject(config) || !isPlainObject(config.states))
1657
+ return false;
1658
+ return Object.values(config.states).some((stateDef) => isPlainObject(stateDef) &&
1659
+ isPlainObject(stateDef.states) &&
1660
+ Object.keys(stateDef.states).length > 0);
1661
+ }
1662
+ // PBRT-52: the factory's lookups index states by their root key, and the
1663
+ // published playbook identity is `meta.playbook.stateId` — the two must
1664
+ // coincide or a machine can advertise a pending question or retry under an
1665
+ // identity no lookup resolves. A state with no string stateId is just as
1666
+ // dead: every snapshot identity derives from that member, so the first
1667
+ // entry would fail the exactly-one-state-id inspection at runtime.
1668
+ // gears2fsm keeps identity and key equal by construction; a hand-authored
1669
+ // artifact that splits or omits them fails here instead of at a silently
1670
+ // dead gate.
1671
+ function assertFlatStateIdentity(machine, label) {
1672
+ const config = machine.config;
1673
+ const states = isPlainObject(config) && isPlainObject(config.states)
1674
+ ? config.states
1675
+ : undefined;
1676
+ // A machine with no root states has no playbook identity to expose; its
1677
+ // first snapshot would fail the exactly-one-state-id inspection, so it
1678
+ // fails construction with the defect named instead.
1679
+ if (states === undefined || Object.keys(states).length === 0) {
1680
+ throw new Error(`${label} declares no root states; the shared runtime requires at ` +
1681
+ 'least one flat playbook state');
1682
+ }
1683
+ for (const [key, stateDef] of Object.entries(states)) {
1684
+ if (!isPlainObject(stateDef))
1685
+ continue;
1686
+ const meta = isPlainObject(stateDef.meta) ? stateDef.meta : undefined;
1687
+ const playbook = meta !== undefined && isPlainObject(meta.playbook)
1688
+ ? meta.playbook
1689
+ : undefined;
1690
+ const stateId = playbook?.stateId;
1691
+ if (typeof stateId !== 'string') {
1692
+ throw new Error(`${label} state ${key} declares no string meta.playbook.stateId; ` +
1693
+ 'the shared runtime derives every playbook state identity from it');
1694
+ }
1695
+ if (stateId !== key) {
1696
+ throw new Error(`${label} state ${key} declares meta.playbook.stateId ${stateId}; ` +
1697
+ 'the shared runtime requires the playbook state id to equal the state key');
1698
+ }
1699
+ }
1700
+ }
1701
+ function rootFinalStateIdsFromMachine(machine) {
1702
+ const config = machine.config;
1703
+ if (!isPlainObject(config) || !isPlainObject(config.states)) {
1704
+ return new Set();
1705
+ }
1706
+ const stateIds = new Set();
1707
+ for (const [stateId, stateDef] of Object.entries(config.states)) {
1708
+ if (isPlainObject(stateDef) && stateDef.type === 'final') {
1709
+ stateIds.add(stateId);
1710
+ }
1711
+ }
1712
+ return stateIds;
1713
+ }
1714
+ // PBRT-52: whether a final outcome leaves the procedure unfinished remains
1715
+ // authored link metadata. The machine can still prove the mechanical half:
1716
+ // every declared stable id must resolve to one of its root final states.
1717
+ function assertUnfinishedFinalStateIds(value, machine, label) {
1718
+ if (value === undefined)
1719
+ return;
1720
+ const rootFinalStateIds = rootFinalStateIdsFromMachine(machine);
1721
+ for (const stateId of value) {
1722
+ if (typeof stateId !== 'string' || !rootFinalStateIds.has(stateId)) {
1723
+ throw new TypeError(`${label} unfinishedFinalStateIds entry ${JSON.stringify(stateId)} ` +
1724
+ 'does not name a root final state');
1725
+ }
1726
+ }
1727
+ }
1193
1728
  export function createXStatePlaybookRuntime(machine, spec) {
1194
1729
  const label = spec.label ?? 'playbook';
1195
1730
  // DR-022 / PBRT-50: reject an incompatible artifact declaration before any
1196
1731
  // machine interpretation, against this loaded engine's own self-report.
1197
- assertRuntimeCompat(spec.compat, label);
1732
+ const artifactSchema = assertRuntimeCompat(spec.compat, label);
1198
1733
  const specDescriptors = Object.getOwnPropertyDescriptors(spec);
1199
1734
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
1200
- throw new TypeError(`${label} schema-2 artifacts must supply roleStates, not playerStates`);
1735
+ throw new TypeError(`${label} artifacts must supply roleStates, not playerStates`);
1201
1736
  }
1202
1737
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
1203
- throw new TypeError(`${label} schema-2 artifacts must not derive concrete player bindings`);
1738
+ throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
1204
1739
  }
1205
1740
  if (machineDeclaresParallelState(machine)) {
1206
1741
  throw new Error(`${label} uses a parallel state; the shared runtime supports only single-region FSMs`);
1207
1742
  }
1743
+ if (machineDeclaresNestedState(machine)) {
1744
+ throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
1745
+ }
1746
+ assertFlatStateIdentity(machine, label);
1747
+ assertUnfinishedFinalStateIds(spec.unfinishedFinalStateIds, machine, label);
1748
+ const retainedGenerationMetadata = spec.unfinishedFinalStateIds === undefined
1749
+ ? undefined
1750
+ : Object.freeze({
1751
+ unfinishedFinalStateIds: Object.freeze([
1752
+ ...spec.unfinishedFinalStateIds,
1753
+ ]),
1754
+ });
1208
1755
  const declaredActors = collectInvokeSources(machine);
1209
1756
  const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
1210
1757
  // DR-029: source state descriptions label the control actions the
@@ -1237,18 +1784,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
1237
1784
  ((input) => defaultComposePlayerPrompt(input, spec.placeholderFields));
1238
1785
  const composeCaptainPrompt = spec.composeCaptainPrompt ??
1239
1786
  ((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
1787
+ const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
1788
+ const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
1240
1789
  const adjudication = {
1241
1790
  ...(spec.buildJudgePrompt !== undefined
1242
1791
  ? { buildJudgePrompt: spec.buildJudgePrompt }
1243
1792
  : {}),
1244
- ...(spec.extractRequiredFields !== undefined
1245
- ? { extractRequiredFields: spec.extractRequiredFields }
1246
- : {}),
1247
- ...(spec.verbatimPayloadFields !== undefined
1248
- ? { verbatimPayloadFields: spec.verbatimPayloadFields }
1249
- : {}),
1793
+ extractRequiredFields: extractFields,
1794
+ verbatimPayloadFields,
1250
1795
  };
1251
- const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
1796
+ const outcomeAuthority = snapshotOutcomeAuthority(specDescriptors.outcomeAuthority, label, roleStates, verbatimPayloadFields);
1797
+ for (const [stateId, outcomes] of Object.entries(outcomeAuthority.governedPlayerStates)) {
1798
+ if (Object.values(outcomes).some(({ repositoryDisposition }) => repositoryDisposition === 'deferred') &&
1799
+ !resumableStateIds.has(stateId)) {
1800
+ throw new TypeError(`${label} outcomeAuthority deferred state ${stateId} must be registered in resumableStateIds`);
1801
+ }
1802
+ }
1252
1803
  // Build the derived classifier unconditionally: it is the sole validator of
1253
1804
  // supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
1254
1805
  // fail factory construction whether or not this spec overrides the
@@ -1259,6 +1810,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1259
1810
  makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
1260
1811
  const statusesForState = spec.statusesForState ??
1261
1812
  makeDefaultStatusesForState(roleStates);
1813
+ const usesDefaultStatuses = spec.statusesForState === undefined;
1262
1814
  const classificationStatus = spec.classificationStatus ??
1263
1815
  ((event) => event.type);
1264
1816
  const machineInput = spec.machineInput ?? ((options) => options);
@@ -1267,8 +1819,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
1267
1819
  const cwd = options?.cwd;
1268
1820
  return typeof cwd === 'string' ? cwd : undefined;
1269
1821
  });
1270
- return function createPlaybookRuntime(options) {
1271
- const boundOptions = spec.snapshotOptions(options);
1822
+ const createPlaybookRuntime = function createPlaybookRuntime(factoryOptions) {
1823
+ const construction = configuredOptionsFromFactoryInput(factoryOptions, label);
1824
+ const configuredOptions = construction.configuredOptions;
1825
+ const effectLedgerCapability = construction.effectLedger;
1826
+ const hasGovernedPlayerStates = Object.keys(outcomeAuthority.governedPlayerStates).length > 0;
1827
+ const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer((source, acceptedOutcome) => {
1828
+ const governedPlayerStates = outcomeAuthority.governedPlayerStates;
1829
+ if (!Object.prototype.hasOwnProperty.call(governedPlayerStates, source)) {
1830
+ return false;
1831
+ }
1832
+ const declarations = governedPlayerStates[source];
1833
+ return (declarations !== undefined &&
1834
+ Object.prototype.hasOwnProperty.call(declarations, acceptedOutcome));
1835
+ });
1836
+ const repositoryCapability = hasGovernedPlayerStates
1837
+ ? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label)
1838
+ : undefined;
1839
+ const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
1840
+ let effectLedgerMirror = currentEffectLedger();
1841
+ let retainedEffectSourceSessionId;
1842
+ let retainedEffectReconciliation;
1843
+ let retainedEffectReconciliationRequired = false;
1844
+ const playerBoundaryReceipts = new WeakMap();
1845
+ const governedPlayerSettlements = new WeakMap();
1846
+ const governedSettlementsByBoundaryId = new Map();
1847
+ const governedCompletionEvidenceByBoundaryId = new Map();
1848
+ const unresolvedSemanticBoundaryIds = new Set();
1849
+ let reconstructedGovernedDelivery;
1850
+ let reconstructedGovernedPrefixSequence;
1851
+ const reconstructedGovernedResults = new WeakMap();
1852
+ let reconstructedAcceptancePending;
1853
+ const boundOptions = spec.snapshotOptions(configuredOptions);
1854
+ assertNoConfiguredHostCapabilities(boundOptions, label);
1272
1855
  const boundScriptCwd = scriptCwd(boundOptions);
1273
1856
  let actor;
1274
1857
  let session;
@@ -1282,7 +1865,37 @@ export function createXStatePlaybookRuntime(machine, spec) {
1282
1865
  // ports.callPlayer / callCaptain / callJudge see the right cancellation
1283
1866
  // source. undefined between turns; set by the public boundaries.
1284
1867
  let activeSignal;
1868
+ // Immutable cancellation provenance for the active public boundary. A
1869
+ // nested resume widens it to include both invocation and resume signals;
1870
+ // mutable `activeSignal` alone cannot classify a late invocation reason.
1871
+ let activeAborts;
1872
+ // The bridge binds the provenance of a child result immediately before
1873
+ // its promise actor settles. The next root snapshot/error consumes this
1874
+ // one-shot so background settlement emissions retain their owner.
1875
+ let actorSettlementAborts;
1876
+ let actorSettlementErrorAborts;
1877
+ // Exact cancellation observed by an emission owned by the active
1878
+ // boundary. Ordinary runs settle from their signal/state; apply also
1879
+ // needs this phase-local evidence to fold a pre-publication failure into
1880
+ // its accepted receipt.
1881
+ let activeAbortEmission;
1285
1882
  let activeTurnId;
1883
+ // The durable host attempt observed by governed calls in the active
1884
+ // public boundary. The failed-state latch survives later no-action turns;
1885
+ // clearing it at every boundary start must not make unsafe replay appear
1886
+ // newly eligible.
1887
+ let activeGovernedBoundarySeen = false;
1888
+ let activeGovernedAttemptId;
1889
+ let activeEffectLedgerPrefixSequence;
1890
+ let failedGovernedAttemptUnknown = false;
1891
+ let failedEffectBoundaryPrefix;
1892
+ let failedGovernedAttemptId;
1893
+ let deferredReconciliationOperationId;
1894
+ let deferredSettlementClosure;
1895
+ let expectedBoundPendingQuestion;
1896
+ let activeDeferredContinuation;
1897
+ let deferInspectionEmissions = false;
1898
+ let deferredInspectionEmissions = [];
1286
1899
  let controlPlaneError;
1287
1900
  // Previous root-machine state for the inspect-driven telemetry /
1288
1901
  // status emitter. undefined before the first inspect firing.
@@ -1310,6 +1923,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1310
1923
  const privateResumeTokens = new Map();
1311
1924
  const activePlayerKeys = new Set();
1312
1925
  const playbookCallTurnIds = new Map();
1926
+ const playbookCallEffectPrefixes = new Map();
1313
1927
  // Captain and judge work share one serialized lane (slc/link.md
1314
1928
  // §Session lifecycle).
1315
1929
  const judgeQueue = new PQueue({ concurrency: 1 });
@@ -1319,99 +1933,520 @@ export function createXStatePlaybookRuntime(machine, spec) {
1319
1933
  // Inspection callbacks enqueue a complete ordered batch synchronously;
1320
1934
  // imperative boundaries await their queued work directly.
1321
1935
  let emissionFailure;
1322
- function bindSession(nextSession) {
1323
- const bound = snapshotPlaybookSession(nextSession);
1324
- if (bound.roleBindings === undefined)
1325
- return bound;
1326
- const actual = Object.keys(bound.roleBindings).sort();
1327
- const expected = [...declaredRoleIds].sort();
1328
- const missing = expected.filter((roleId) => !actual.includes(roleId));
1329
- const extra = actual.filter((roleId) => !expected.includes(roleId));
1330
- if (missing.length > 0 || extra.length > 0) {
1331
- throw new TypeError(`${label} session roleBindings must cover exactly [${expected.join(', ')}]` +
1332
- `${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
1333
- `${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
1936
+ function runtimeLogicalOperations(ledger = effectLedgerMirror) {
1937
+ if (session === undefined)
1938
+ return [];
1939
+ const runtimeSessionIds = new Set([
1940
+ session.sessionId,
1941
+ ...(retainedEffectSourceSessionId === undefined
1942
+ ? []
1943
+ : [retainedEffectSourceSessionId]),
1944
+ ]);
1945
+ return ledger.logicalOperations.filter((operation) => operation.playbookId === session.playbookId &&
1946
+ runtimeSessionIds.has(operation.runtimeSessionId));
1947
+ }
1948
+ function refreshRetainedEffectReconciliation(current = effectLedgerMirror) {
1949
+ const retained = retainedEffectReconciliation;
1950
+ if (retained === undefined) {
1951
+ retainedEffectReconciliationRequired = false;
1952
+ return;
1953
+ }
1954
+ const safe = retainedAdoptionCheckpointIsSafe(retained.checkpoint, current);
1955
+ retainedEffectReconciliationRequired = !safe;
1956
+ if (safe) {
1957
+ retainedEffectReconciliation = undefined;
1958
+ reconstructedGovernedPrefixSequence = undefined;
1334
1959
  }
1335
- return bound;
1336
1960
  }
1337
- function requireRoleId(input) {
1338
- const roleId = input.role;
1339
- if (typeof roleId !== 'string' ||
1340
- roleId.trim().length === 0 ||
1341
- !declaredRoleIds.includes(roleId)) {
1342
- throw new TypeError(`${label} player input role must name a declared local role`);
1961
+ function bindRetainedEffectReconciliation(retained, current) {
1962
+ retainedEffectReconciliation = retained;
1963
+ refreshRetainedEffectReconciliation(current);
1964
+ reconstructedGovernedPrefixSequence =
1965
+ retainedEffectReconciliation === undefined
1966
+ ? undefined
1967
+ : (retainedEffectReconciliation.checkpoint.boundaries.at(-1)
1968
+ ?.sequence ?? 0);
1969
+ }
1970
+ function refreshRetainedEffectFenceFromHost() {
1971
+ if (retainedEffectReconciliation === undefined)
1972
+ return;
1973
+ const retainedBeforeRefresh = retainedEffectReconciliation;
1974
+ try {
1975
+ effectLedgerMirror = currentEffectLedger();
1976
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
1977
+ syncDeferredReconciliationOverlay();
1978
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
1979
+ }
1980
+ catch {
1981
+ // A fence can open only from validated authoritative evidence. If the
1982
+ // live mirror or its source-owned deferred-operation view cannot be
1983
+ // read exactly, keep every ordinary entry point closed.
1984
+ retainedEffectReconciliation ??= retainedBeforeRefresh;
1985
+ deferredReconciliationOperationId = undefined;
1986
+ retainedEffectReconciliationRequired = true;
1343
1987
  }
1344
- return roleId;
1345
1988
  }
1346
- function resolvedPlayerId(roleId) {
1347
- return session?.roleBindings?.[roleId]?.playerId;
1989
+ function syncDeferredReconciliationOverlay() {
1990
+ const unresolved = runtimeLogicalOperations().filter((operation) => operation.logicalReceipt === undefined &&
1991
+ (operation.checkpointRestorationEligible ||
1992
+ operation.pendingQuestion === undefined));
1993
+ if (unresolved.length > 1) {
1994
+ throw new Error(`${label} effect ledger contains multiple unresolved deferred operations`);
1995
+ }
1996
+ deferredReconciliationOperationId = unresolved[0]?.operationId;
1348
1997
  }
1349
- function promptIdentity(roleId) {
1350
- if (!declaredRoleIds.includes(roleId)) {
1351
- throw new TypeError(`${label} prompt identity lookup rejected undeclared role ${roleId}`);
1998
+ function runtimeBoundaryIsOwned(boundary) {
1999
+ if (session === undefined || boundary.playbookId !== session.playbookId) {
2000
+ return false;
1352
2001
  }
1353
- return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
2002
+ return (boundary.runtimeSessionId === session.sessionId ||
2003
+ boundary.runtimeSessionId === retainedEffectSourceSessionId);
1354
2004
  }
1355
- function composeBoundPlayerPrompt(input) {
1356
- let active = true;
1357
- const lookup = (roleId) => {
1358
- if (!active) {
1359
- throw new Error(`${label} prompt identity lookup is no longer active`);
2005
+ function governedOutcomesForBoundary(candidate) {
2006
+ const outcomes = outcomeAuthority?.governedPlayerStates[candidate.sourceStateId];
2007
+ if (outcomes === undefined)
2008
+ return undefined;
2009
+ if (!isPlainObject(candidate.sourceOutcomeSchema) ||
2010
+ !sameStringSet(Object.keys(candidate.sourceOutcomeSchema), Object.keys(outcomes))) {
2011
+ return undefined;
2012
+ }
2013
+ for (const [guard, description] of Object.entries(candidate.sourceOutcomeSchema)) {
2014
+ if (typeof description !== 'string')
2015
+ return undefined;
2016
+ const describedFields = [...new Set(extractFields(description))];
2017
+ if (!sameStringSet(describedFields, Object.keys(outcomes[guard].fields))) {
2018
+ return undefined;
1360
2019
  }
1361
- return promptIdentity(roleId);
1362
- };
1363
- try {
1364
- return composePlayerPrompt(input, lookup);
1365
2020
  }
1366
- finally {
1367
- active = false;
2021
+ const expectedDispositions = [
2022
+ ...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
2023
+ ];
2024
+ if (!sameStringSet(candidate.dispositions, expectedDispositions)) {
2025
+ return undefined;
1368
2026
  }
2027
+ return outcomes;
1369
2028
  }
1370
- function continuationKey(roleId, playerId) {
1371
- return playerId ?? roleId;
1372
- }
1373
- function roleTokensByContinuationKey(tokens) {
1374
- const byKey = new Map();
1375
- for (const [roleId, token] of Object.entries(tokens)) {
1376
- if (!declaredRoleIds.includes(roleId)) {
1377
- throw new TypeError(`runtime role tokens contain unknown role ${roleId}`);
2029
+ function persistedBoundaryReconciliation(candidate, ledger) {
2030
+ const outcomes = governedOutcomesForBoundary(candidate);
2031
+ if (outcomes === undefined || candidate.semanticCandidate === undefined) {
2032
+ return undefined;
2033
+ }
2034
+ let receipt = candidate.physicalReceipt;
2035
+ let historicalDeferred = false;
2036
+ let awaitingLogicalReceipt = false;
2037
+ if (candidate.logicalOperationId !== undefined) {
2038
+ const operation = ledger.logicalOperations.find(({ operationId }) => operationId === candidate.logicalOperationId);
2039
+ if (operation === undefined)
2040
+ return undefined;
2041
+ const latestBoundaryId = operation.boundaryIds.at(-1);
2042
+ if (latestBoundaryId !== candidate.boundaryId) {
2043
+ // Earlier questions remain independently validated historical
2044
+ // evidence. Their physical same-HEAD receipt, candidate, and
2045
+ // reciprocal operation link must still prove a deferred arm.
2046
+ historicalDeferred = true;
1378
2047
  }
1379
- const key = continuationKey(roleId, resolvedPlayerId(roleId));
1380
- const existing = byKey.get(key);
1381
- if (existing !== undefined && existing !== token) {
1382
- throw new TypeError(`runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
2048
+ else if (operation.logicalReceipt !== undefined) {
2049
+ receipt = operation.logicalReceipt;
2050
+ }
2051
+ else if (operation.pendingQuestion === undefined ||
2052
+ operation.checkpoint === undefined ||
2053
+ !Object.prototype.hasOwnProperty.call(operation, 'playerContinuation')) {
2054
+ return undefined;
2055
+ }
2056
+ else {
2057
+ awaitingLogicalReceipt = true;
1383
2058
  }
1384
- byKey.set(key, token);
1385
- }
1386
- const rolesByKey = new Map();
1387
- for (const roleId of declaredRoleIds) {
1388
- const key = continuationKey(roleId, resolvedPlayerId(roleId));
1389
- rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
1390
2059
  }
1391
- for (const [key, roles] of rolesByKey) {
1392
- if (roles.length < 2)
1393
- continue;
1394
- const present = roles.filter((roleId) => tokens[roleId] !== undefined);
1395
- if (present.length !== 0 && present.length !== roles.length) {
1396
- throw new TypeError(`runtime role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
2060
+ try {
2061
+ const reconciliation = reconcilePlaybookSemanticEvidence({
2062
+ outcomes,
2063
+ semanticCandidate: candidate.semanticCandidate,
2064
+ finalText: candidate.finalText,
2065
+ receipt,
2066
+ });
2067
+ if (awaitingLogicalReceipt &&
2068
+ reconciliation.status !== 'deferred') {
2069
+ return undefined;
1397
2070
  }
2071
+ return {
2072
+ reconciliation,
2073
+ historicalDeferred,
2074
+ };
2075
+ }
2076
+ catch {
2077
+ return undefined;
1398
2078
  }
1399
- return byKey;
1400
2079
  }
1401
- function selectPlayerResume(roleId, playerId) {
1402
- const key = continuationKey(roleId, playerId);
1403
- const selected = session?.playerSessions
1404
- ? session.playerSessions.select(roleId)
1405
- : privateResumeTokens.get(key) ?? false;
1406
- if (selected !== false &&
1407
- (typeof selected !== 'string' || selected.trim().length === 0)) {
1408
- throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
2080
+ function boundaryNeedsSemanticReconciliation(candidate, ledger) {
2081
+ if (!runtimeBoundaryIsOwned(candidate))
2082
+ return false;
2083
+ if (governedOutcomesForBoundary(candidate) === undefined)
2084
+ return true;
2085
+ const persisted = persistedBoundaryReconciliation(candidate, ledger);
2086
+ if (persisted !== undefined) {
2087
+ if (persisted.reconciliation.status === 'unresolved')
2088
+ return true;
2089
+ if (persisted.historicalDeferred) {
2090
+ return persisted.reconciliation.status !== 'deferred';
2091
+ }
2092
+ if (persisted.reconciliation.status === 'deferred' &&
2093
+ candidate.logicalOperationId === undefined) {
2094
+ return true;
2095
+ }
2096
+ return false;
1409
2097
  }
1410
- return selected;
2098
+ if (candidate.physicalReceipt === undefined) {
2099
+ // An unsafe retained suffix is already owned by the task-8 adoption
2100
+ // fence, which may still expose its exact deferred-restoration
2101
+ // action. A same-generation incomplete boundary has no such fence
2102
+ // and remains semantic/effect unresolved until host reconstruction.
2103
+ return retainedEffectReconciliation === undefined;
2104
+ }
2105
+ if (typeof candidate.finalText === 'string' &&
2106
+ candidate.finalText.trim().length > 0) {
2107
+ return true;
2108
+ }
2109
+ return candidate.physicalReceipt.classification !== 'unchanged';
1411
2110
  }
1412
- function updatePlayerResume(roleId, playerId, result) {
1413
- const resumeToken = result.resumeToken;
1414
- if (resumeToken === undefined && result.status !== 'ok')
2111
+ function refreshUnresolvedSemanticReconciliation(current = effectLedgerMirror) {
2112
+ unresolvedSemanticBoundaryIds.clear();
2113
+ if (outcomeAuthority === undefined || session === undefined)
2114
+ return;
2115
+ for (const candidate of current.boundaries) {
2116
+ if (boundaryNeedsSemanticReconciliation(candidate, current)) {
2117
+ unresolvedSemanticBoundaryIds.add(candidate.boundaryId);
2118
+ }
2119
+ }
2120
+ }
2121
+ function prepareReconstructedGovernedDelivery(state, ledger = effectLedgerMirror) {
2122
+ reconstructedGovernedDelivery = undefined;
2123
+ if (state.stateId === undefined ||
2124
+ state.activeStateIds.length !== 1) {
2125
+ return;
2126
+ }
2127
+ const owned = ledger.boundaries.filter(runtimeBoundaryIsOwned);
2128
+ const candidate = reconstructedGovernedPrefixSequence === undefined
2129
+ ? owned.at(-1)
2130
+ : owned.find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
2131
+ if (candidate === undefined || candidate.sourceStateId !== state.stateId) {
2132
+ return;
2133
+ }
2134
+ const persisted = persistedBoundaryReconciliation(candidate, ledger);
2135
+ if (persisted === undefined ||
2136
+ persisted.historicalDeferred ||
2137
+ persisted.reconciliation.status !== 'resolved' ||
2138
+ typeof candidate.finalText !== 'string') {
2139
+ return;
2140
+ }
2141
+ reconstructedGovernedDelivery = {
2142
+ boundary: candidate,
2143
+ finalText: candidate.finalText,
2144
+ settlement: {
2145
+ status: 'resolved',
2146
+ output: persisted.reconciliation.output,
2147
+ },
2148
+ };
2149
+ }
2150
+ function takeReconstructedGovernedPlayerResult(input, roleId) {
2151
+ const reconstructed = reconstructedGovernedDelivery;
2152
+ if (reconstructed === undefined)
2153
+ return undefined;
2154
+ // A reconstructed envelope is consumable once even when a hostile host
2155
+ // changes its mirror between restore validation and actor startup.
2156
+ reconstructedGovernedDelivery = undefined;
2157
+ const current = currentEffectLedger();
2158
+ effectLedgerMirror = current;
2159
+ syncDeferredReconciliationOverlay();
2160
+ refreshUnresolvedSemanticReconciliation(current);
2161
+ const completed = current.boundaries.find(({ boundaryId }) => boundaryId === reconstructed.boundary.boundaryId);
2162
+ const expected = reconstructedGovernedPrefixSequence === undefined
2163
+ ? current.boundaries.filter(runtimeBoundaryIsOwned).at(-1)
2164
+ : current.boundaries
2165
+ .filter(runtimeBoundaryIsOwned)
2166
+ .find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
2167
+ const persisted = completed === undefined
2168
+ ? undefined
2169
+ : persistedBoundaryReconciliation(completed, current);
2170
+ if (completed === undefined ||
2171
+ expected?.boundaryId !== completed.boundaryId ||
2172
+ !isDeepStrictEqual(completed, reconstructed.boundary) ||
2173
+ completed.sourceStateId !== input.stateId ||
2174
+ completed.roleId !== roleId ||
2175
+ !isDeepStrictEqual(completed.sourceOutcomeSchema, input.result) ||
2176
+ persisted === undefined ||
2177
+ persisted.historicalDeferred ||
2178
+ persisted.reconciliation.status !== 'resolved' ||
2179
+ completed.finalText !== reconstructed.finalText ||
2180
+ !isDeepStrictEqual(persisted.reconciliation.output, reconstructed.settlement.output)) {
2181
+ unresolvedSemanticBoundaryIds.add(reconstructed.boundary.boundaryId);
2182
+ throw markFsmResultFailure(new Error(`${label} retained governed semantic envelope is no longer exact`));
2183
+ }
2184
+ validateBossReplyOutput(input, reconstructed.settlement.output, resumableStateIds);
2185
+ const result = validatePlayerResult({
2186
+ status: 'ok',
2187
+ finalText: reconstructed.finalText,
2188
+ });
2189
+ playerBoundaryReceipts.set(result, {
2190
+ boundaryId: completed.boundaryId,
2191
+ attemptId: completed.attemptId,
2192
+ });
2193
+ governedPlayerSettlements.set(result, reconstructed.settlement);
2194
+ reconstructedGovernedResults.set(result, completed);
2195
+ return result;
2196
+ }
2197
+ function acceptReconstructedGovernedDelivery(state) {
2198
+ const accepted = reconstructedAcceptancePending;
2199
+ if (accepted === undefined ||
2200
+ state.stateId === accepted.sourceStateId) {
2201
+ return;
2202
+ }
2203
+ reconstructedAcceptancePending = undefined;
2204
+ let current;
2205
+ try {
2206
+ current = currentEffectLedger();
2207
+ effectLedgerMirror = current;
2208
+ syncDeferredReconciliationOverlay();
2209
+ refreshUnresolvedSemanticReconciliation(current);
2210
+ }
2211
+ catch {
2212
+ unresolvedSemanticBoundaryIds.add(accepted.boundaryId);
2213
+ return;
2214
+ }
2215
+ const acknowledged = current.boundaries.find(({ boundaryId }) => boundaryId === accepted.boundaryId);
2216
+ if (acknowledged === undefined ||
2217
+ !isDeepStrictEqual(acknowledged, accepted)) {
2218
+ unresolvedSemanticBoundaryIds.add(accepted.boundaryId);
2219
+ return;
2220
+ }
2221
+ if (reconstructedGovernedPrefixSequence !== undefined) {
2222
+ reconstructedGovernedPrefixSequence = accepted.sequence;
2223
+ prepareReconstructedGovernedDelivery(state, current);
2224
+ if (current.boundaries
2225
+ .filter(runtimeBoundaryIsOwned)
2226
+ .some(({ sequence }) => sequence > reconstructedGovernedPrefixSequence)) {
2227
+ return;
2228
+ }
2229
+ }
2230
+ if (unresolvedSemanticBoundaryIds.size > 0 ||
2231
+ deferredReconciliationOperationId !== undefined) {
2232
+ return;
2233
+ }
2234
+ // Task 9 has now projected the retained, host-acknowledged envelope
2235
+ // into the FSM. Only after that acceptance may the task-8 adoption
2236
+ // marker retire; an unresolved sibling boundary leaves it intact.
2237
+ retainedEffectReconciliation = undefined;
2238
+ retainedEffectReconciliationRequired = false;
2239
+ reconstructedGovernedPrefixSequence = undefined;
2240
+ }
2241
+ function hasUnresolvedReconciliation() {
2242
+ return (deferredReconciliationOperationId !== undefined ||
2243
+ retainedEffectReconciliationRequired ||
2244
+ unresolvedSemanticBoundaryIds.size > 0);
2245
+ }
2246
+ function unresolvedEffectEnvelopeIdentities() {
2247
+ if (session === undefined)
2248
+ return [];
2249
+ const current = currentEffectLedger();
2250
+ effectLedgerMirror = current;
2251
+ refreshRetainedEffectReconciliation(current);
2252
+ syncDeferredReconciliationOverlay();
2253
+ refreshUnresolvedSemanticReconciliation(current);
2254
+ if (!hasUnresolvedReconciliation())
2255
+ return [];
2256
+ const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
2257
+ const operationIds = new Set();
2258
+ if (deferredReconciliationOperationId !== undefined) {
2259
+ operationIds.add(deferredReconciliationOperationId);
2260
+ }
2261
+ if (retainedEffectReconciliationRequired) {
2262
+ const checkpointLength = retainedEffectReconciliation?.checkpoint
2263
+ .boundaries.length ?? 0;
2264
+ for (const boundary of current.boundaries.slice(checkpointLength)) {
2265
+ if (boundary.physicalReceipt?.classification === 'unchanged') {
2266
+ continue;
2267
+ }
2268
+ boundaryIds.add(boundary.boundaryId);
2269
+ }
2270
+ }
2271
+ for (const boundaryId of [...boundaryIds]) {
2272
+ const boundary = current.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
2273
+ if (boundary?.logicalOperationId !== undefined &&
2274
+ current.logicalOperations.some(({ operationId }) => operationId === boundary.logicalOperationId)) {
2275
+ operationIds.add(boundary.logicalOperationId);
2276
+ for (const memberId of current.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId).boundaryIds) {
2277
+ boundaryIds.delete(memberId);
2278
+ }
2279
+ }
2280
+ }
2281
+ const ordered = [
2282
+ ...[...boundaryIds].map((boundaryId) => ({
2283
+ order: current.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
2284
+ value: { kind: 'boundary', boundaryId },
2285
+ })),
2286
+ ...[...operationIds].map((operationId) => {
2287
+ const operation = current.logicalOperations.find((candidate) => candidate.operationId === operationId);
2288
+ const firstBoundaryId = operation?.boundaryIds[0];
2289
+ return {
2290
+ order: current.boundaries.find(({ boundaryId }) => boundaryId === firstBoundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
2291
+ value: { kind: 'logical-operation', operationId },
2292
+ };
2293
+ }),
2294
+ ].sort((left, right) => left.order - right.order);
2295
+ return deepFreeze(snapshotJsonValue(ordered.map(({ value }) => value), `${label} unresolved effect envelope identities`));
2296
+ }
2297
+ function closeAfterIndeterminateDeferredSettlement(operationId, cause) {
2298
+ try {
2299
+ effectLedgerMirror = currentEffectLedger();
2300
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
2301
+ syncDeferredReconciliationOverlay();
2302
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
2303
+ }
2304
+ catch {
2305
+ // The current host mirror is itself unavailable. The closure below
2306
+ // keeps every public state surface shut until a fresh host recovers
2307
+ // the write-ahead record and constructs a replacement runtime.
2308
+ }
2309
+ expectedBoundPendingQuestion = undefined;
2310
+ deferredSettlementClosure ??= new Error(`${label} deferred settlement is indeterminate; recover the host effect ledger before continuing`, { cause });
2311
+ if (operationId !== undefined &&
2312
+ deferredReconciliationOperationId === undefined) {
2313
+ deferredReconciliationOperationId = operationId;
2314
+ }
2315
+ }
2316
+ function assertDeferredSettlementOpen(method) {
2317
+ if (deferredSettlementClosure !== undefined) {
2318
+ throw new Error(`createPlaybookRuntime.${method}: deferred settlement recovery is required`, { cause: deferredSettlementClosure });
2319
+ }
2320
+ }
2321
+ function currentBoundDeferredOperation(pending) {
2322
+ const projected = {
2323
+ questionId: pending.questionId,
2324
+ asker: pending.asker,
2325
+ question: pending.question,
2326
+ sourceItem: pending.sourceItem,
2327
+ };
2328
+ const matches = runtimeLogicalOperations().filter((operation) => operation.logicalReceipt === undefined &&
2329
+ operation.checkpoint !== undefined &&
2330
+ operation.pendingQuestion !== undefined &&
2331
+ operation.playerContinuation !== undefined &&
2332
+ !operation.checkpointRestorationEligible &&
2333
+ isDeepStrictEqual(operation.pendingQuestion, projected));
2334
+ if (matches.length > 1) {
2335
+ throw new Error(`${label} effect ledger contains multiple operations for one pending question`);
2336
+ }
2337
+ return matches[0];
2338
+ }
2339
+ function continuationBoundarySeed(operation, turnId) {
2340
+ const latestBoundaryId = operation.boundaryIds.at(-1);
2341
+ const latestBoundary = effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === latestBoundaryId);
2342
+ if (latestBoundary === undefined) {
2343
+ throw new Error(`${label} deferred logical operation has no latest physical boundary`);
2344
+ }
2345
+ return {
2346
+ boundaryId: randomUUID(),
2347
+ runtimeSessionId: latestBoundary.runtimeSessionId,
2348
+ turnId,
2349
+ callId: `player-${++playerCallSequence}`,
2350
+ roleId: latestBoundary.roleId,
2351
+ sourceStateId: latestBoundary.sourceStateId,
2352
+ sourceOutcomeSchema: latestBoundary.sourceOutcomeSchema,
2353
+ dispositions: latestBoundary.dispositions,
2354
+ correctionBudget: { limit: 1, spent: false },
2355
+ };
2356
+ }
2357
+ function bindSession(nextSession) {
2358
+ const bound = snapshotPlaybookSession(nextSession);
2359
+ if (bound.roleBindings === undefined)
2360
+ return bound;
2361
+ const actual = Object.keys(bound.roleBindings).sort();
2362
+ const expected = [...declaredRoleIds].sort();
2363
+ const missing = expected.filter((roleId) => !actual.includes(roleId));
2364
+ const extra = actual.filter((roleId) => !expected.includes(roleId));
2365
+ if (missing.length > 0 || extra.length > 0) {
2366
+ throw new TypeError(`${label} session roleBindings must cover exactly [${expected.join(', ')}]` +
2367
+ `${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
2368
+ `${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
2369
+ }
2370
+ return bound;
2371
+ }
2372
+ function requireRoleId(input) {
2373
+ const roleId = input.role;
2374
+ if (typeof roleId !== 'string' ||
2375
+ roleId.trim().length === 0 ||
2376
+ !declaredRoleIds.includes(roleId)) {
2377
+ throw new TypeError(`${label} player input role must name a declared local role`);
2378
+ }
2379
+ return roleId;
2380
+ }
2381
+ function resolvedPlayerId(roleId) {
2382
+ return session?.roleBindings?.[roleId]?.playerId;
2383
+ }
2384
+ function promptIdentity(roleId) {
2385
+ if (!declaredRoleIds.includes(roleId)) {
2386
+ throw new TypeError(`${label} prompt identity lookup rejected undeclared role ${roleId}`);
2387
+ }
2388
+ return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
2389
+ }
2390
+ function composeBoundPlayerPrompt(input) {
2391
+ let active = true;
2392
+ const lookup = (roleId) => {
2393
+ if (!active) {
2394
+ throw new Error(`${label} prompt identity lookup is no longer active`);
2395
+ }
2396
+ return promptIdentity(roleId);
2397
+ };
2398
+ try {
2399
+ return composePlayerPrompt(input, lookup);
2400
+ }
2401
+ finally {
2402
+ active = false;
2403
+ }
2404
+ }
2405
+ function continuationKey(roleId, playerId) {
2406
+ return playerId ?? roleId;
2407
+ }
2408
+ function roleTokensByContinuationKey(tokens) {
2409
+ const byKey = new Map();
2410
+ for (const [roleId, token] of Object.entries(tokens)) {
2411
+ if (!declaredRoleIds.includes(roleId)) {
2412
+ throw new TypeError(`runtime role tokens contain unknown role ${roleId}`);
2413
+ }
2414
+ const key = continuationKey(roleId, resolvedPlayerId(roleId));
2415
+ const existing = byKey.get(key);
2416
+ if (existing !== undefined && existing !== token) {
2417
+ throw new TypeError(`runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
2418
+ }
2419
+ byKey.set(key, token);
2420
+ }
2421
+ const rolesByKey = new Map();
2422
+ for (const roleId of declaredRoleIds) {
2423
+ const key = continuationKey(roleId, resolvedPlayerId(roleId));
2424
+ rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
2425
+ }
2426
+ for (const [key, roles] of rolesByKey) {
2427
+ if (roles.length < 2)
2428
+ continue;
2429
+ const present = roles.filter((roleId) => tokens[roleId] !== undefined);
2430
+ if (present.length !== 0 && present.length !== roles.length) {
2431
+ throw new TypeError(`runtime role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
2432
+ }
2433
+ }
2434
+ return byKey;
2435
+ }
2436
+ function selectPlayerResume(roleId, playerId) {
2437
+ const key = continuationKey(roleId, playerId);
2438
+ const selected = session?.playerSessions
2439
+ ? session.playerSessions.select(roleId)
2440
+ : privateResumeTokens.get(key) ?? false;
2441
+ if (selected !== false &&
2442
+ (typeof selected !== 'string' || selected.trim().length === 0)) {
2443
+ throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
2444
+ }
2445
+ return selected;
2446
+ }
2447
+ function updatePlayerResume(roleId, playerId, result) {
2448
+ const resumeToken = result.resumeToken;
2449
+ if (resumeToken === undefined && result.status !== 'ok')
1415
2450
  return;
1416
2451
  const key = continuationKey(roleId, playerId);
1417
2452
  if (session?.playerSessions) {
@@ -1457,16 +2492,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
1457
2492
  for (const [key, token] of byKey)
1458
2493
  privateResumeTokens.set(key, token);
1459
2494
  }
1460
- function enqueueEmission(fn) {
2495
+ function enqueueEmission(fn, aborts = activeAborts) {
2496
+ // The emission belongs to the boundary enqueueing it: a rejection
2497
+ // causally identical to that boundary's abort reason is the
2498
+ // cancellation's own evidence — never latched, so it cannot poison a
2499
+ // later unrelated boundary (DR-036).
2500
+ const enqueueAborts = aborts;
1461
2501
  const queued = emissionQueue.add(fn).then(() => undefined);
1462
2502
  activeEmissionCalls.add(queued);
1463
2503
  void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
1464
2504
  activeEmissionCalls.delete(queued);
1465
- emissionFailure ??= error;
2505
+ if (enqueueAborts?.isAbortReason(error)) {
2506
+ // Record evidence only when it also belongs to the public
2507
+ // boundary that is still active. A background A cancellation
2508
+ // racing an unrelated B boundary is forgiven under A and must
2509
+ // not change B's settlement.
2510
+ if (activeAborts?.isAbortReason(error)) {
2511
+ activeAbortEmission ??= error;
2512
+ }
2513
+ return;
2514
+ }
2515
+ emissionFailure ??= { error };
1466
2516
  });
1467
2517
  return queued;
1468
2518
  }
1469
- async function drainEmissions() {
2519
+ async function drainEmissions(_aborts = activeAborts) {
1470
2520
  while (true) {
1471
2521
  const active = [...activeEmissionCalls];
1472
2522
  if (active.length > 0)
@@ -1479,8 +2529,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
1479
2529
  }
1480
2530
  }
1481
2531
  if (emissionFailure !== undefined) {
1482
- const error = emissionFailure;
2532
+ const { error } = emissionFailure;
1483
2533
  emissionFailure = undefined;
2534
+ // The failure was classified as distinct by its enqueue owner. If a
2535
+ // later public boundary drains it, retain that classification in the
2536
+ // boundary latch before throwing; its signal must not reinterpret
2537
+ // the same object as cancellation (DR-036 decision 2).
2538
+ if (activeSignal !== undefined)
2539
+ controlPlaneError ??= error;
1484
2540
  throw error;
1485
2541
  }
1486
2542
  }
@@ -1500,7 +2556,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1500
2556
  const currentSession = requireSession();
1501
2557
  const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
1502
2558
  return {
1503
- schemaVersion: 3,
2559
+ schemaVersion: 4,
1504
2560
  sessionId: currentSession.sessionId,
1505
2561
  playbookId: currentSession.playbookId,
1506
2562
  rootSessionId: currentSession.rootSessionId,
@@ -1519,13 +2575,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
1519
2575
  payload: safePayload,
1520
2576
  };
1521
2577
  }
1522
- function emitTrace(type, payload, position = {}) {
2578
+ function emitTrace(type, payload, position = {}, aborts) {
1523
2579
  const currentSession = requireSession();
1524
2580
  const event = createTraceEvent(type, payload, position);
1525
2581
  return enqueueEmission(() => currentSession.ports.emitTelemetry({
1526
2582
  topic: 'playbook.trace',
1527
2583
  payload: event,
1528
- }));
2584
+ }), aborts);
1529
2585
  }
1530
2586
  function stateIdentity(stateId) {
1531
2587
  return stateId === undefined ? {} : { stateId };
@@ -1583,6 +2639,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
1583
2639
  };
1584
2640
  }
1585
2641
  async function emitCallStarted(startedType, finishedType, identity, position,
2642
+ // The applicable combined signal: a start-sink rejection causally
2643
+ // identical to its reason is the cancellation itself, not a control
2644
+ // error — the pair finishes `aborted` and nothing latches
2645
+ // (slc/link.md §Abort).
2646
+ signal,
1586
2647
  // Base payload of the best-effort finish emitted when the start sink
1587
2648
  // rejects; it defaults to the payload the start carried, which the
1588
2649
  // player, judge, and captain pairs take as-is. The apply pair cannot:
@@ -1594,11 +2655,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
1594
2655
  await emitTrace(startedType, identity, position);
1595
2656
  }
1596
2657
  catch (error) {
1597
- controlPlaneError ??= error;
2658
+ if (!isAbortFailure(error, signal))
2659
+ controlPlaneError ??= error;
1598
2660
  try {
1599
2661
  await emitTrace(finishedType, {
1600
2662
  ...finishIdentity,
1601
- status: 'error',
2663
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
1602
2664
  error: normalizeError(error),
1603
2665
  }, position);
1604
2666
  }
@@ -1608,31 +2670,605 @@ export function createXStatePlaybookRuntime(machine, spec) {
1608
2670
  throw error;
1609
2671
  }
1610
2672
  }
2673
+ function governedBoundarySeed(input, roleId, callId, turnId) {
2674
+ const governed = outcomeAuthority?.governedPlayerStates[input.stateId];
2675
+ if (governed === undefined)
2676
+ return undefined;
2677
+ if (!Number.isSafeInteger(turnId) || turnId === undefined || turnId <= 0) {
2678
+ throw new Error(`${label} governed player call requires an active positive turn id`);
2679
+ }
2680
+ const dispositions = [
2681
+ ...new Set(Object.values(governed).map(({ repositoryDisposition }) => repositoryDisposition)),
2682
+ ];
2683
+ if (dispositions.length === 0) {
2684
+ throw new Error(`${label} governed player call has no repository disposition`);
2685
+ }
2686
+ return {
2687
+ boundaryId: randomUUID(),
2688
+ // An adopted runtime keeps one durable effect-owner identity across
2689
+ // every later target generation. New boundaries must join that same
2690
+ // lineage; otherwise a boundary started by an intermediate target is
2691
+ // no longer discoverable after the next adoption.
2692
+ runtimeSessionId: retainedEffectSourceSessionId ?? requireSession().sessionId,
2693
+ turnId,
2694
+ callId,
2695
+ roleId,
2696
+ sourceStateId: input.stateId,
2697
+ sourceOutcomeSchema: snapshotJsonValue(input.result, `${label} governed player source outcome schema`),
2698
+ dispositions,
2699
+ correctionBudget: { limit: 1, spent: false },
2700
+ };
2701
+ }
2702
+ function boundPendingQuestion(input, roleId, output) {
2703
+ if (output.guard !== 'needsBossReply' || typeof output.question !== 'string') {
2704
+ throw new TypeError(`${label} deferred outcome must carry one exact Boss question`);
2705
+ }
2706
+ return {
2707
+ questionId: input.stateId,
2708
+ resumeStateId: input.stateId,
2709
+ sourceItem: input.sourceItem,
2710
+ asker: { kind: 'role', roleId },
2711
+ question: output.question,
2712
+ };
2713
+ }
2714
+ function detachedPlayerContinuation(roleId, playerId) {
2715
+ return snapshotJsonValue(selectPlayerResume(roleId, playerId), `${label} deferred player continuation`);
2716
+ }
2717
+ function completionEvidenceFor(input, roleId, playerId, signal, operationId) {
2718
+ return async (completion) => {
2719
+ const { operation } = completion;
2720
+ let evidence;
2721
+ if (operation.status !== 'fulfilled' ||
2722
+ operation.value.status !== 'ok' ||
2723
+ isEmptyFinalText(operation.value.finalText)) {
2724
+ evidence = operation.status === 'fulfilled' &&
2725
+ operation.value.status === 'ok' &&
2726
+ operation.value.finalText !== undefined
2727
+ ? { finalText: operation.value.finalText }
2728
+ : {};
2729
+ }
2730
+ else {
2731
+ const finalText = operation.value.finalText;
2732
+ evidence = await reconcileGovernedCompletion(input, roleId, playerId, finalText, signal, operationId, completion);
2733
+ }
2734
+ rememberGovernedCompletionEvidence(completion.boundary.boundaryId, evidence);
2735
+ return evidence;
2736
+ };
2737
+ }
2738
+ function rememberGovernedCompletionEvidence(boundaryId, evidence) {
2739
+ const previous = governedCompletionEvidenceByBoundaryId.get(boundaryId);
2740
+ governedCompletionEvidenceByBoundaryId.set(boundaryId, {
2741
+ ...previous,
2742
+ boundaryEvidence: {
2743
+ ...(Object.prototype.hasOwnProperty.call(evidence, 'finalText')
2744
+ ? { finalText: evidence.finalText }
2745
+ : {}),
2746
+ ...(Object.prototype.hasOwnProperty.call(evidence, 'semanticCandidate')
2747
+ ? { semanticCandidate: evidence.semanticCandidate }
2748
+ : {}),
2749
+ },
2750
+ });
2751
+ }
2752
+ function unresolvedGovernedSettlement(reason, error, signal = activeSignal) {
2753
+ if (error !== undefined && signal?.aborted && Object.is(error, signal.reason)) {
2754
+ return { status: 'unresolved', error };
2755
+ }
2756
+ const failure = error instanceof Error
2757
+ ? error
2758
+ : new Error(`${label} governed outcome remains unresolved: ${reason}`);
2759
+ return {
2760
+ status: 'unresolved',
2761
+ error: markFsmResultFailure(failure),
2762
+ };
2763
+ }
2764
+ async function spendSemanticCorrectionBudget(completedBoundary, receipt, finalText, semanticCandidate) {
2765
+ if (effectLedgerCapability === undefined)
2766
+ return undefined;
2767
+ const currentLedger = currentEffectLedger();
2768
+ const current = currentLedger.boundaries.find(({ boundaryId }) => boundaryId === completedBoundary.boundaryId);
2769
+ if (current === undefined ||
2770
+ current.correctionBudget.limit !== 1 ||
2771
+ current.correctionBudget.spent) {
2772
+ return undefined;
2773
+ }
2774
+ if (current.finalText !== undefined &&
2775
+ current.finalText !== finalText) {
2776
+ throw new TypeError(`${label} correction budget boundary conflicts with retained finalText`);
2777
+ }
2778
+ if (current.physicalReceipt !== undefined &&
2779
+ !isDeepStrictEqual(current.physicalReceipt, receipt)) {
2780
+ throw new TypeError(`${label} correction budget boundary conflicts with its repository receipt`);
2781
+ }
2782
+ if (semanticCandidate !== undefined &&
2783
+ current.semanticCandidate !== undefined &&
2784
+ !isDeepStrictEqual(current.semanticCandidate, semanticCandidate)) {
2785
+ throw new TypeError(`${label} correction budget boundary conflicts with its retained semantic candidate`);
2786
+ }
2787
+ const next = {
2788
+ ...current,
2789
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
2790
+ physicalReceipt: receipt,
2791
+ finalText,
2792
+ ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
2793
+ correctionBudget: { limit: 1, spent: true },
2794
+ };
2795
+ const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
2796
+ {
2797
+ kind: 'replace-boundaries',
2798
+ replacements: [{ expected: current, next }],
2799
+ },
2800
+ ]), `${label} semantic correction budget acknowledgement`);
2801
+ effectLedgerMirror = acknowledged;
2802
+ refreshRetainedEffectReconciliation(acknowledged);
2803
+ syncDeferredReconciliationOverlay();
2804
+ refreshUnresolvedSemanticReconciliation(acknowledged);
2805
+ const spent = acknowledged.boundaries.find(({ boundaryId }) => boundaryId === completedBoundary.boundaryId);
2806
+ if (spent === undefined ||
2807
+ !isDeepStrictEqual(spent, next)) {
2808
+ throw new TypeError(`${label} semantic correction budget spend was not acknowledged exactly`);
2809
+ }
2810
+ return spent;
2811
+ }
2812
+ async function reconcileGovernedCompletion(input, roleId, playerId, finalText, signal, operationId, completion) {
2813
+ const outcomes = outcomeAuthority?.governedPlayerStates[input.stateId];
2814
+ if (outcomes === undefined) {
2815
+ throw new TypeError(`${label} governed semantic reconciliation has no authority for ${input.stateId}`);
2816
+ }
2817
+ if (completion.boundary.sourceStateId !== input.stateId ||
2818
+ !isDeepStrictEqual(completion.boundary.sourceOutcomeSchema, input.result)) {
2819
+ throw new TypeError(`${label} governed semantic reconciliation source schema changed`);
2820
+ }
2821
+ let raw;
2822
+ try {
2823
+ raw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes), signal);
2824
+ }
2825
+ catch (error) {
2826
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('judge transport failed', error, signal));
2827
+ return { finalText, unresolved: true };
2828
+ }
2829
+ let candidate;
2830
+ let retainedSemanticCandidate;
2831
+ const retainSemanticCandidate = (value) => {
2832
+ try {
2833
+ retainedSemanticCandidate = snapshotJsonValue(value, `${label} recoverable governed semantic candidate`);
2834
+ }
2835
+ catch {
2836
+ // A malformed or non-detachable reply supplies no durable
2837
+ // candidate; presentation and receipt evidence still survive.
2838
+ }
2839
+ };
2840
+ const unresolvedEvidence = () => ({
2841
+ finalText,
2842
+ ...(retainedSemanticCandidate === undefined
2843
+ ? {}
2844
+ : { semanticCandidate: retainedSemanticCandidate }),
2845
+ unresolved: true,
2846
+ });
2847
+ let reconciliation;
2848
+ let structuralError;
2849
+ try {
2850
+ candidate = parseGovernedSemanticCandidate(raw);
2851
+ retainSemanticCandidate(candidate);
2852
+ reconciliation = reconcilePlaybookSemanticEvidence({
2853
+ outcomes,
2854
+ semanticCandidate: candidate,
2855
+ finalText,
2856
+ receipt: completion.outcomeReceipt,
2857
+ });
2858
+ }
2859
+ catch (error) {
2860
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
2861
+ throw error;
2862
+ }
2863
+ structuralError = error;
2864
+ }
2865
+ if (structuralError !== undefined) {
2866
+ let spent;
2867
+ try {
2868
+ spent = await spendSemanticCorrectionBudget(completion.boundary, completion.receipt, finalText, retainedSemanticCandidate);
2869
+ }
2870
+ catch (error) {
2871
+ // A failed or indeterminate spend cannot authorize another judge.
2872
+ // Let the repository coordinator quarantine its still-owned claim;
2873
+ // an acknowledged write remains durable and one-way on recovery.
2874
+ throw error;
2875
+ }
2876
+ if (spent === undefined) {
2877
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('semantic correction budget is unavailable'));
2878
+ return unresolvedEvidence();
2879
+ }
2880
+ if (signal.aborted) {
2881
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('semantic correction was aborted before its judge call', signal.reason, signal));
2882
+ return unresolvedEvidence();
2883
+ }
2884
+ let correctiveRaw;
2885
+ try {
2886
+ correctiveRaw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes, {
2887
+ reply: raw,
2888
+ error: structuralError.message,
2889
+ }), signal);
2890
+ }
2891
+ catch (error) {
2892
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('corrective judge failed', error, signal));
2893
+ return unresolvedEvidence();
2894
+ }
2895
+ try {
2896
+ candidate = parseGovernedSemanticCandidate(correctiveRaw);
2897
+ retainSemanticCandidate(candidate);
2898
+ reconciliation = reconcilePlaybookSemanticEvidence({
2899
+ outcomes,
2900
+ semanticCandidate: candidate,
2901
+ finalText,
2902
+ receipt: completion.outcomeReceipt,
2903
+ });
2904
+ }
2905
+ catch (error) {
2906
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
2907
+ throw error;
2908
+ }
2909
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('corrective semantic candidate is invalid'));
2910
+ return unresolvedEvidence();
2911
+ }
2912
+ }
2913
+ if (reconciliation === undefined) {
2914
+ throw new Error(`${label} semantic reconciliation produced no decision`);
2915
+ }
2916
+ const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, `${label} governed semantic candidate`);
2917
+ if (reconciliation.status === 'unresolved') {
2918
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason));
2919
+ return { finalText, semanticCandidate, unresolved: true };
2920
+ }
2921
+ const output = reconciliation.output;
2922
+ validateBossReplyOutput(input, output, resumableStateIds);
2923
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, {
2924
+ status: 'resolved',
2925
+ output,
2926
+ });
2927
+ governedCompletionEvidenceByBoundaryId.set(completion.boundary.boundaryId, {
2928
+ boundaryEvidence: {},
2929
+ reconciliationStatus: reconciliation.status,
2930
+ output,
2931
+ });
2932
+ if (reconciliation.status !== 'deferred') {
2933
+ return { finalText, semanticCandidate };
2934
+ }
2935
+ const pending = boundPendingQuestion(input, roleId, output);
2936
+ const bindingId = operationId ?? randomUUID();
2937
+ expectedBoundPendingQuestion = pending;
2938
+ return {
2939
+ finalText,
2940
+ semanticCandidate,
2941
+ deferred: {
2942
+ operationId: bindingId,
2943
+ pendingQuestion: {
2944
+ questionId: pending.questionId,
2945
+ asker: pending.asker,
2946
+ question: pending.question,
2947
+ sourceItem: pending.sourceItem,
2948
+ },
2949
+ playerContinuation: detachedPlayerContinuation(roleId, playerId),
2950
+ },
2951
+ };
2952
+ }
2953
+ async function deferredContinuationCompletionEvidence(completion) {
2954
+ const remember = (evidence) => {
2955
+ rememberGovernedCompletionEvidence(completion.boundary.boundaryId, evidence);
2956
+ return evidence;
2957
+ };
2958
+ const continuation = activeDeferredContinuation;
2959
+ if (continuation === undefined) {
2960
+ throw new Error(`${label} deferred continuation completed without active runtime context`);
2961
+ }
2962
+ const result = continuation.result;
2963
+ if (completion.operation.status !== 'fulfilled' ||
2964
+ completion.operation.value !== null ||
2965
+ result === undefined ||
2966
+ result.status !== 'ok' ||
2967
+ isEmptyFinalText(result.finalText)) {
2968
+ if (completion.outcomeReceipt.classification === 'unchanged' &&
2969
+ (continuation.callError !== undefined ||
2970
+ (result !== undefined && result.status !== 'ok'))) {
2971
+ return remember({});
2972
+ }
2973
+ governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('deferred player result has no semantic evidence', continuation.callError));
2974
+ return remember({
2975
+ ...(result?.status !== 'ok' || result.finalText === undefined
2976
+ ? {}
2977
+ : { finalText: result.finalText }),
2978
+ unresolved: true,
2979
+ });
2980
+ }
2981
+ const input = continuation.input;
2982
+ const roleId = continuation.roleId;
2983
+ const signal = continuation.signal;
2984
+ if (input === undefined || roleId === undefined || signal === undefined) {
2985
+ throw new Error(`${label} deferred continuation lost its bound player identity or signal`);
2986
+ }
2987
+ return remember(await reconcileGovernedCompletion(input, roleId, continuation.playerId, result.finalText, signal, continuation.operationId, completion));
2988
+ }
2989
+ function assertAcknowledgedGovernedEvidence(completed, settlement, ledger) {
2990
+ const expected = governedCompletionEvidenceByBoundaryId.get(completed.boundaryId);
2991
+ if (expected === undefined ||
2992
+ (Object.prototype.hasOwnProperty.call(expected.boundaryEvidence, 'finalText')
2993
+ ? completed.finalText !== expected.boundaryEvidence.finalText
2994
+ : completed.finalText !== undefined) ||
2995
+ (Object.prototype.hasOwnProperty.call(expected.boundaryEvidence, 'semanticCandidate')
2996
+ ? !isDeepStrictEqual(completed.semanticCandidate, expected.boundaryEvidence.semanticCandidate)
2997
+ : completed.semanticCandidate !== undefined)) {
2998
+ throw new TypeError(`${label} repository did not acknowledge the exact governed semantic evidence`);
2999
+ }
3000
+ if (settlement?.status !== 'resolved')
3001
+ return;
3002
+ const persisted = persistedBoundaryReconciliation(completed, ledger);
3003
+ if (persisted === undefined ||
3004
+ persisted.historicalDeferred ||
3005
+ persisted.reconciliation.status !== expected.reconciliationStatus ||
3006
+ !isDeepStrictEqual(persisted.reconciliation.output, expected.output) ||
3007
+ !isDeepStrictEqual(expected.output, settlement.output)) {
3008
+ throw new TypeError(`${label} repository did not acknowledge the exact governed semantic evidence`);
3009
+ }
3010
+ }
3011
+ function recordActiveGovernedAttempt(boundary) {
3012
+ if (activeGovernedAttemptId !== undefined &&
3013
+ activeGovernedAttemptId !== boundary.attemptId) {
3014
+ throw new Error(`${label} governed calls in one runtime boundary used different host attempt ids`);
3015
+ }
3016
+ activeGovernedAttemptId = boundary.attemptId;
3017
+ }
3018
+ function refreshGovernedBoundaryStart(boundaryId) {
3019
+ try {
3020
+ const current = currentEffectLedger();
3021
+ const boundary = current.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
3022
+ if (boundary === undefined)
3023
+ return;
3024
+ effectLedgerMirror = current;
3025
+ refreshRetainedEffectReconciliation(current);
3026
+ recordActiveGovernedAttempt(boundary);
3027
+ }
3028
+ catch {
3029
+ // Preserve the repository failure. A mirror that cannot be read or
3030
+ // validated supplies no evidence authorizing replay.
3031
+ }
3032
+ }
3033
+ function acknowledgeGovernedPlayerResult(value, boundaryId, source = 'runExclusive') {
3034
+ if (!isPlainObject(value) || !isPlainObject(value.operation)) {
3035
+ throw new TypeError(`${label} repository ${source} returned an invalid settlement`);
3036
+ }
3037
+ const ledger = assertPlaybookEffectLedger(value.effectLedger, `${label} repository ${source} effect ledger`);
3038
+ const completed = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
3039
+ if (completed === undefined ||
3040
+ completed.physicalReceipt === undefined ||
3041
+ !isDeepStrictEqual(completed.physicalReceipt, value.receipt)) {
3042
+ throw new TypeError(`${label} repository ${source} did not acknowledge its completed boundary`);
3043
+ }
3044
+ effectLedgerMirror = ledger;
3045
+ refreshRetainedEffectReconciliation(ledger);
3046
+ syncDeferredReconciliationOverlay();
3047
+ refreshUnresolvedSemanticReconciliation(ledger);
3048
+ recordActiveGovernedAttempt(completed);
3049
+ if (value.operation.status === 'rejected') {
3050
+ if (!Object.prototype.hasOwnProperty.call(value.operation, 'reason')) {
3051
+ throw new TypeError(`${label} repository ${source} rejection omitted its reason`);
3052
+ }
3053
+ throw value.operation.reason;
3054
+ }
3055
+ if (value.operation.status !== 'fulfilled' ||
3056
+ !Object.prototype.hasOwnProperty.call(value.operation, 'value')) {
3057
+ throw new TypeError(`${label} repository ${source} returned an invalid operation settlement`);
3058
+ }
3059
+ const result = validatePlayerResult(value.operation.value);
3060
+ playerBoundaryReceipts.set(result, {
3061
+ boundaryId: completed.boundaryId,
3062
+ attemptId: completed.attemptId,
3063
+ });
3064
+ let governedSettlement = governedSettlementsByBoundaryId.get(boundaryId);
3065
+ if (governedSettlement === undefined &&
3066
+ result.status === 'ok' &&
3067
+ !isEmptyFinalText(result.finalText)) {
3068
+ governedSettlement = unresolvedGovernedSettlement('host omitted governed semantic settlement');
3069
+ }
3070
+ assertAcknowledgedGovernedEvidence(completed, governedSettlement, ledger);
3071
+ governedCompletionEvidenceByBoundaryId.delete(boundaryId);
3072
+ let governedOutput = governedSettlement?.status === 'resolved'
3073
+ ? governedSettlement.output
3074
+ : undefined;
3075
+ const governedDisposition = governedOutput === undefined
3076
+ ? undefined
3077
+ : governedOutcomesForBoundary(completed)?.[governedOutput.guard]
3078
+ ?.repositoryDisposition;
3079
+ if (source === 'runExclusive' && governedDisposition === 'deferred') {
3080
+ if (value.deferredStatus !== 'bound' &&
3081
+ value.deferredStatus !== 'unresolved') {
3082
+ throw new TypeError(`${label} deferred settlement omitted its durable binding status`);
3083
+ }
3084
+ const operationId = completed.logicalOperationId;
3085
+ if (operationId === undefined) {
3086
+ throw new TypeError(`${label} deferred settlement omitted its logical operation`);
3087
+ }
3088
+ if (value.deferredStatus === 'bound') {
3089
+ if (expectedBoundPendingQuestion === undefined ||
3090
+ currentBoundDeferredOperation(expectedBoundPendingQuestion)
3091
+ ?.operationId !== operationId) {
3092
+ throw new TypeError(`${label} deferred settlement did not acknowledge its exact bound question`);
3093
+ }
3094
+ }
3095
+ else {
3096
+ if (deferredReconciliationOperationId !== operationId) {
3097
+ throw new TypeError(`${label} unresolved deferred settlement is not structurally unresolved`);
3098
+ }
3099
+ expectedBoundPendingQuestion = undefined;
3100
+ governedSettlement = unresolvedGovernedSettlement('deferred question did not receive an eligible durable binding');
3101
+ governedOutput = undefined;
3102
+ }
3103
+ }
3104
+ else if (source === 'runExclusive' && value.deferredStatus !== undefined) {
3105
+ throw new TypeError(`${label} non-deferred settlement returned a deferred binding status`);
3106
+ }
3107
+ if (governedSettlement !== undefined) {
3108
+ governedSettlementsByBoundaryId.delete(boundaryId);
3109
+ governedPlayerSettlements.set(result, governedSettlement);
3110
+ if (governedSettlement.status === 'unresolved') {
3111
+ unresolvedSemanticBoundaryIds.add(boundaryId);
3112
+ }
3113
+ else {
3114
+ unresolvedSemanticBoundaryIds.delete(boundaryId);
3115
+ }
3116
+ }
3117
+ return result;
3118
+ }
3119
+ function acknowledgedBoundaryIsUnchanged(result) {
3120
+ if (outcomeAuthority === undefined)
3121
+ return true;
3122
+ const identity = playerBoundaryReceipts.get(result);
3123
+ if (identity === undefined)
3124
+ return false;
3125
+ const boundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === identity.boundaryId);
3126
+ return (boundary?.attemptId === identity.attemptId &&
3127
+ boundary.physicalReceipt?.classification === 'unchanged');
3128
+ }
3129
+ function failedAttemptMatchesCurrentLedger(current) {
3130
+ const boundaryPrefix = failedEffectBoundaryPrefix;
3131
+ if (failedGovernedAttemptUnknown ||
3132
+ boundaryPrefix === undefined) {
3133
+ return false;
3134
+ }
3135
+ const causalBoundaries = current.boundaries.filter(({ sequence }) => sequence > boundaryPrefix);
3136
+ const matches = failedGovernedAttemptId === undefined
3137
+ ? causalBoundaries.length === 0
3138
+ : causalBoundaries.length > 0 &&
3139
+ causalBoundaries.every(({ attemptId }) => attemptId === failedGovernedAttemptId);
3140
+ if (!matches)
3141
+ failedGovernedAttemptUnknown = true;
3142
+ return matches;
3143
+ }
3144
+ function failedAttemptAllowsReplay() {
3145
+ if (!hasGovernedPlayerStates)
3146
+ return true;
3147
+ if (unresolvedSemanticBoundaryIds.size > 0)
3148
+ return false;
3149
+ let current;
3150
+ try {
3151
+ current = currentEffectLedger();
3152
+ effectLedgerMirror = current;
3153
+ refreshRetainedEffectReconciliation(current);
3154
+ refreshUnresolvedSemanticReconciliation(current);
3155
+ }
3156
+ catch {
3157
+ failedGovernedAttemptUnknown = true;
3158
+ return false;
3159
+ }
3160
+ if (unresolvedSemanticBoundaryIds.size > 0)
3161
+ return false;
3162
+ if (!failedAttemptMatchesCurrentLedger(current))
3163
+ return false;
3164
+ if (failedGovernedAttemptId === undefined)
3165
+ return true;
3166
+ const boundaries = current.boundaries.filter(({ attemptId }) => attemptId === failedGovernedAttemptId);
3167
+ return (boundaries.length > 0 &&
3168
+ boundaries.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged'));
3169
+ }
3170
+ function captureEffectLedgerPrefixSequence() {
3171
+ if (!hasGovernedPlayerStates)
3172
+ return undefined;
3173
+ try {
3174
+ const current = currentEffectLedger();
3175
+ effectLedgerMirror = current;
3176
+ refreshRetainedEffectReconciliation(current);
3177
+ return current.boundaries.at(-1)?.sequence ?? 0;
3178
+ }
3179
+ catch {
3180
+ return undefined;
3181
+ }
3182
+ }
3183
+ function bindAutomaticReplayBoundary(prefixSequence) {
3184
+ activeGovernedBoundarySeen = false;
3185
+ activeGovernedAttemptId = undefined;
3186
+ activeEffectLedgerPrefixSequence = prefixSequence;
3187
+ }
3188
+ function beginAutomaticReplayBoundary() {
3189
+ bindAutomaticReplayBoundary(captureEffectLedgerPrefixSequence());
3190
+ }
3191
+ function latchFailedGovernedAttempt() {
3192
+ if (!hasGovernedPlayerStates) {
3193
+ failedGovernedAttemptUnknown = false;
3194
+ failedEffectBoundaryPrefix = undefined;
3195
+ failedGovernedAttemptId = undefined;
3196
+ return;
3197
+ }
3198
+ if (activeEffectLedgerPrefixSequence === undefined) {
3199
+ failedGovernedAttemptUnknown = true;
3200
+ failedEffectBoundaryPrefix = undefined;
3201
+ failedGovernedAttemptId = undefined;
3202
+ return;
3203
+ }
3204
+ let current;
3205
+ try {
3206
+ current = currentEffectLedger();
3207
+ effectLedgerMirror = current;
3208
+ refreshRetainedEffectReconciliation(current);
3209
+ }
3210
+ catch {
3211
+ failedGovernedAttemptUnknown = true;
3212
+ failedEffectBoundaryPrefix = undefined;
3213
+ failedGovernedAttemptId = undefined;
3214
+ return;
3215
+ }
3216
+ const attemptIds = new Set(current.boundaries
3217
+ .filter(({ sequence }) => sequence > activeEffectLedgerPrefixSequence)
3218
+ .map(({ attemptId }) => attemptId));
3219
+ if (activeGovernedAttemptId !== undefined) {
3220
+ attemptIds.add(activeGovernedAttemptId);
3221
+ }
3222
+ if (attemptIds.size > 1) {
3223
+ failedGovernedAttemptUnknown = true;
3224
+ failedEffectBoundaryPrefix = undefined;
3225
+ failedGovernedAttemptId = undefined;
3226
+ return;
3227
+ }
3228
+ failedGovernedAttemptUnknown =
3229
+ attemptIds.size === 0 && activeGovernedBoundarySeen;
3230
+ failedEffectBoundaryPrefix = failedGovernedAttemptUnknown
3231
+ ? undefined
3232
+ : activeEffectLedgerPrefixSequence;
3233
+ failedGovernedAttemptId = attemptIds.values().next().value;
3234
+ }
1611
3235
  const boundary = {
1612
3236
  async callPlayer(input, roleId, prompt, signal) {
1613
3237
  // State-entry telemetry/status must precede the call they describe.
1614
3238
  await drainEmissions();
3239
+ signal.throwIfAborted();
3240
+ const deferredContinuation = activeDeferredContinuation;
3241
+ const reconstructed = takeReconstructedGovernedPlayerResult(input, roleId);
3242
+ if (reconstructed !== undefined)
3243
+ return reconstructed;
3244
+ if (deferredContinuation === undefined &&
3245
+ hasUnresolvedReconciliation()) {
3246
+ throw markFsmResultFailure(new Error(`${label} governed semantic reconciliation remains unresolved`));
3247
+ }
1615
3248
  const turnId = activeTurnId;
1616
3249
  const stateId = input.stateId;
1617
3250
  const playerId = resolvedPlayerId(roleId);
1618
- let resume;
3251
+ let selectedResume;
1619
3252
  try {
1620
3253
  signal.throwIfAborted();
1621
- resume = selectPlayerResume(roleId, playerId);
3254
+ selectedResume =
3255
+ deferredContinuation?.playerContinuation ??
3256
+ selectPlayerResume(roleId, playerId);
1622
3257
  }
1623
3258
  catch (error) {
1624
- if (!signal.aborted)
3259
+ if (!isAbortFailure(error, signal))
1625
3260
  controlPlaneError ??= error;
1626
3261
  throw error;
1627
3262
  }
1628
- const callId = `player-${++playerCallSequence}`;
1629
- const identity = {
3263
+ const callId = deferredContinuation?.effectBoundary.callId ??
3264
+ `player-${++playerCallSequence}`;
3265
+ const callIdentity = (resume) => ({
1630
3266
  ...stateIdentity(stateId),
1631
3267
  sourceItem: input.sourceItem,
1632
3268
  roleId,
1633
3269
  ...(playerId === undefined ? {} : { playerId }),
1634
3270
  resume,
1635
- };
3271
+ });
1636
3272
  const position = {
1637
3273
  ...(turnId !== undefined ? { turnId } : {}),
1638
3274
  callId,
@@ -1640,92 +3276,182 @@ export function createXStatePlaybookRuntime(machine, spec) {
1640
3276
  const playerKey = continuationKey(roleId, playerId);
1641
3277
  if (activePlayerKeys.has(playerKey)) {
1642
3278
  const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
1643
- await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position);
1644
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3279
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, signal);
3280
+ await emitTrace('player.call.finished', {
3281
+ ...callIdentity(selectedResume),
3282
+ status: 'error',
3283
+ error: normalizeError(error),
3284
+ }, position);
1645
3285
  throw error;
1646
3286
  }
1647
3287
  activePlayerKeys.add(playerKey);
1648
3288
  try {
1649
- await emitTrace('player.call.started', { ...identity, prompt }, position);
1650
- let rawResult;
1651
- try {
1652
- // An abort may land while the awaited started emission drains
1653
- // (e.g. fired from the trace sink itself); the host call must
1654
- // never start after abort, so settle the already-started pair
1655
- // as `aborted` through the catch below.
1656
- signal.throwIfAborted();
1657
- rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
1658
- // A host promise is not required to honor cancellation. Do not let
1659
- // a late result mutate continuity or publish a successful finish.
1660
- signal.throwIfAborted();
1661
- }
1662
- catch (error) {
1663
- if (!signal.aborted)
1664
- controlPlaneError ??= error;
3289
+ const runTracedPlayerCall = async (resume = selectedResume) => {
3290
+ const identity = callIdentity(resume);
3291
+ await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position, signal);
3292
+ let rawResult;
1665
3293
  try {
1666
- await emitTrace('player.call.finished', {
1667
- ...identity,
1668
- status: signal.aborted ? 'aborted' : 'error',
1669
- error: normalizeError(error),
1670
- }, position);
3294
+ // An abort may land while the awaited started emission drains
3295
+ // (e.g. fired from the trace sink itself); the host call must
3296
+ // never start after abort, so settle the already-started pair
3297
+ // as `aborted` through the catch below.
3298
+ signal.throwIfAborted();
3299
+ rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
3300
+ // A host promise is not required to honor cancellation. Do not
3301
+ // let a late result mutate continuity or publish a successful
3302
+ // finish.
3303
+ signal.throwIfAborted();
1671
3304
  }
1672
- catch {
1673
- // The original non-abort port rejection remains authoritative.
3305
+ catch (error) {
3306
+ if (!isAbortFailure(error, signal))
3307
+ controlPlaneError ??= error;
3308
+ try {
3309
+ await emitTrace('player.call.finished', {
3310
+ ...identity,
3311
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
3312
+ error: normalizeError(error),
3313
+ }, position);
3314
+ }
3315
+ catch {
3316
+ // The original non-abort port rejection remains authoritative.
3317
+ }
3318
+ // A thrown port call carries no authoritative result, so the
3319
+ // prior token remains available for a later explicit resume.
3320
+ throw error;
1674
3321
  }
1675
- // A thrown port call carries no authoritative result, so the
1676
- // prior token remains available for a later explicit resume.
1677
- throw error;
1678
- }
1679
- let result;
1680
- try {
1681
- result = validatePlayerResult(rawResult);
1682
- }
1683
- catch (error) {
1684
- if (!signal.aborted)
1685
- controlPlaneError ??= error;
3322
+ let result;
1686
3323
  try {
1687
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3324
+ result = validatePlayerResult(rawResult);
1688
3325
  }
1689
- catch {
1690
- // The malformed host result remains authoritative.
3326
+ catch (error) {
3327
+ if (!isAbortFailure(error, signal))
3328
+ controlPlaneError ??= error;
3329
+ try {
3330
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3331
+ }
3332
+ catch {
3333
+ // The malformed host result remains authoritative.
3334
+ }
3335
+ throw error;
1691
3336
  }
1692
- throw error;
1693
- }
1694
- try {
1695
- updatePlayerResume(roleId, playerId, result);
1696
- }
1697
- catch (error) {
1698
- if (!signal.aborted)
1699
- controlPlaneError ??= error;
1700
3337
  try {
1701
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3338
+ updatePlayerResume(roleId, playerId, result);
1702
3339
  }
1703
- catch {
1704
- // The continuation-store failure remains authoritative.
3340
+ catch (error) {
3341
+ if (!isAbortFailure(error, signal))
3342
+ controlPlaneError ??= error;
3343
+ try {
3344
+ await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3345
+ }
3346
+ catch {
3347
+ // The continuation-store failure remains authoritative.
3348
+ }
3349
+ throw error;
3350
+ }
3351
+ await emitTrace('player.call.finished', {
3352
+ ...identity,
3353
+ status: result.status,
3354
+ ...(result.finalText !== undefined
3355
+ ? { finalText: result.finalText }
3356
+ : {}),
3357
+ ...(result.error !== undefined
3358
+ ? { error: normalizeError(result.error) }
3359
+ : {}),
3360
+ ...(result.resumeToken !== undefined
3361
+ ? { resumeToken: result.resumeToken }
3362
+ : {}),
3363
+ }, position);
3364
+ return result;
3365
+ };
3366
+ const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
3367
+ if (deferredContinuation !== undefined) {
3368
+ if (effectBoundary === undefined ||
3369
+ effectBoundary.runtimeSessionId !==
3370
+ deferredContinuation.effectBoundary.runtimeSessionId ||
3371
+ effectBoundary.turnId !== deferredContinuation.effectBoundary.turnId ||
3372
+ effectBoundary.callId !== deferredContinuation.effectBoundary.callId ||
3373
+ effectBoundary.roleId !== deferredContinuation.effectBoundary.roleId ||
3374
+ effectBoundary.sourceStateId !==
3375
+ deferredContinuation.effectBoundary.sourceStateId ||
3376
+ !isDeepStrictEqual(effectBoundary.sourceOutcomeSchema, deferredContinuation.effectBoundary.sourceOutcomeSchema) ||
3377
+ !isDeepStrictEqual(effectBoundary.dispositions, deferredContinuation.effectBoundary.dispositions)) {
3378
+ throw new TypeError(`${label} deferred continuation did not invoke its bound player boundary`);
3379
+ }
3380
+ activeGovernedBoundarySeen = true;
3381
+ deferredContinuation.input = input;
3382
+ deferredContinuation.roleId = roleId;
3383
+ deferredContinuation.playerId = playerId;
3384
+ deferredContinuation.signal = signal;
3385
+ try {
3386
+ deferredContinuation.result = await runTracedPlayerCall(selectedResume);
3387
+ }
3388
+ catch (error) {
3389
+ deferredContinuation.callError = error;
3390
+ }
3391
+ finally {
3392
+ deferredContinuation.rawPlayerSettled.resolve();
3393
+ }
3394
+ return await deferredContinuation.delivery.promise;
3395
+ }
3396
+ // Await inside this try so its finally retains the player-key
3397
+ // exclusion until the host operation actually settles.
3398
+ if (effectBoundary === undefined)
3399
+ return await runTracedPlayerCall();
3400
+ if (repositoryCapability === undefined) {
3401
+ throw new Error(`${label} governed player call requires repository.runExclusive`);
3402
+ }
3403
+ activeGovernedBoundarySeen = true;
3404
+ try {
3405
+ const exclusive = await repositoryCapability.runExclusive({
3406
+ signal,
3407
+ effectBoundary,
3408
+ operation: () => runTracedPlayerCall(),
3409
+ completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
3410
+ });
3411
+ return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
3412
+ }
3413
+ catch (error) {
3414
+ if (expectedBoundPendingQuestion !== undefined) {
3415
+ closeAfterIndeterminateDeferredSettlement(undefined, error);
1705
3416
  }
3417
+ expectedBoundPendingQuestion = undefined;
3418
+ governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
3419
+ governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
3420
+ refreshGovernedBoundaryStart(effectBoundary.boundaryId);
3421
+ if (!isAbortFailure(error, signal))
3422
+ controlPlaneError ??= error;
1706
3423
  throw error;
1707
3424
  }
1708
- await emitTrace('player.call.finished', {
1709
- ...identity,
1710
- status: result.status,
1711
- ...(result.finalText !== undefined
1712
- ? { finalText: result.finalText }
1713
- : {}),
1714
- ...(result.error !== undefined
1715
- ? { error: normalizeError(result.error) }
1716
- : {}),
1717
- ...(result.resumeToken !== undefined
1718
- ? { resumeToken: result.resumeToken }
1719
- : {}),
1720
- }, position);
1721
- return result;
1722
3425
  }
1723
3426
  finally {
1724
3427
  activePlayerKeys.delete(playerKey);
1725
3428
  }
1726
3429
  },
3430
+ takeGovernedPlayerOutput(result) {
3431
+ const settlement = governedPlayerSettlements.get(result);
3432
+ if (settlement !== undefined)
3433
+ governedPlayerSettlements.delete(result);
3434
+ return settlement;
3435
+ },
3436
+ recordGovernedPlayerOutput(result, output) {
3437
+ const reconstructed = reconstructedGovernedResults.get(result);
3438
+ if (reconstructed === undefined)
3439
+ return;
3440
+ reconstructedGovernedResults.delete(result);
3441
+ const persisted = persistedBoundaryReconciliation(reconstructed, effectLedgerMirror);
3442
+ if (persisted === undefined ||
3443
+ persisted.reconciliation.status !== 'resolved' ||
3444
+ !isDeepStrictEqual(persisted.reconciliation.output, output)) {
3445
+ unresolvedSemanticBoundaryIds.add(reconstructed.boundaryId);
3446
+ throw markFsmResultFailure(new Error(`${label} reconstructed governed output changed before FSM acceptance`));
3447
+ }
3448
+ reconstructedAcceptancePending = reconstructed;
3449
+ },
1727
3450
  async callJudge(purpose, stateId, prompt, signal) {
1728
3451
  return judgeQueue.add(async () => {
3452
+ const governedSemanticJudge = purpose === 'player-output-adjudication' &&
3453
+ stateId !== undefined &&
3454
+ outcomeAuthority.governedPlayerStates[stateId] !== undefined;
1729
3455
  signal.throwIfAborted();
1730
3456
  // A transition/status queued synchronously by XState must reach
1731
3457
  // the host before the judge call that follows it.
@@ -1738,7 +3464,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1738
3464
  ...(turnId !== undefined ? { turnId } : {}),
1739
3465
  callId,
1740
3466
  };
1741
- await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, position);
3467
+ await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, position, signal);
1742
3468
  let reply;
1743
3469
  try {
1744
3470
  // An abort may land while the awaited started emission drains
@@ -1750,19 +3476,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
1750
3476
  signal.throwIfAborted();
1751
3477
  }
1752
3478
  catch (error) {
1753
- if (!isAbortFailure(error, signal)) {
3479
+ if (!isAbortFailure(error, signal) && !governedSemanticJudge) {
1754
3480
  controlPlaneError ??= error;
1755
3481
  }
1756
3482
  await emitTrace('judge.call.finished', {
1757
3483
  ...identity,
1758
- status: signal.aborted ? 'aborted' : 'error',
3484
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
1759
3485
  error: normalizeError(error),
1760
3486
  }, position);
1761
3487
  throw error;
1762
3488
  }
1763
3489
  if (typeof reply !== 'string') {
1764
3490
  const error = new TypeError('judge reply must be a string');
1765
- controlPlaneError ??= error;
3491
+ if (!governedSemanticJudge)
3492
+ controlPlaneError ??= error;
1766
3493
  await emitTrace('judge.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
1767
3494
  throw error;
1768
3495
  }
@@ -1802,7 +3529,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1802
3529
  ...(turnId !== undefined ? { turnId } : {}),
1803
3530
  callId,
1804
3531
  };
1805
- await emitCallStarted('captain.call.started', 'captain.call.finished', { ...identity, prompt }, position);
3532
+ await emitCallStarted('captain.call.started', 'captain.call.finished', { ...identity, prompt }, position, signal);
1806
3533
  let rawResult;
1807
3534
  try {
1808
3535
  // An abort may land while the awaited started emission drains
@@ -1824,7 +3551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1824
3551
  controlPlaneError ??= error;
1825
3552
  await emitTrace('captain.call.finished', {
1826
3553
  ...identity,
1827
- status: signal.aborted ? 'aborted' : 'error',
3554
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
1828
3555
  error: normalizeError(error),
1829
3556
  }, position);
1830
3557
  throw error;
@@ -1889,12 +3616,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
1889
3616
  function playerActor(ports) {
1890
3617
  return createPlayerBridge({
1891
3618
  resolveRoleId: requireRoleId,
3619
+ validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
1892
3620
  composePlayerPrompt: composeBoundPlayerPrompt,
1893
3621
  adjudication,
1894
3622
  resumableStateIds,
3623
+ allowsCorrectiveReplay: acknowledgedBoundaryIsUnchanged,
1895
3624
  }, ports, () => activeSignal, boundary, (error) => {
1896
- if (!activeSignal?.aborted)
3625
+ if (activeSignal === undefined || !isAbortFailure(error, activeSignal)) {
1897
3626
  controlPlaneError ??= error;
3627
+ }
1898
3628
  });
1899
3629
  }
1900
3630
  // Direct-Captain actor (slc/link.md §Captain prompt composition,
@@ -1961,7 +3691,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1961
3691
  // failure state (PBRT-47); everything else here — a drained
1962
3692
  // emission failure, prompt composition, the port itself,
1963
3693
  // adjudication — is control plane.
1964
- if (!active.aborted && !isFsmResultFailure(error)) {
3694
+ if (!isAbortFailure(error, active) && !isFsmResultFailure(error)) {
1965
3695
  controlPlaneError ??= error;
1966
3696
  }
1967
3697
  throw error;
@@ -1982,57 +3712,181 @@ export function createXStatePlaybookRuntime(machine, spec) {
1982
3712
  const failedGuard = guards[1] ?? guards[0];
1983
3713
  const cwd = boundScriptCwd ?? process.cwd();
1984
3714
  const ports = runtimePorts ?? requireHostPorts();
1985
- const exitStatus = await new Promise((resolve, reject) => {
1986
- let child;
1987
- try {
1988
- child = spawn('sh', ['-c', input.command], {
1989
- cwd,
1990
- stdio: 'ignore',
1991
- });
3715
+ // slc/link.md §Script execution: an already-aborted turn spawns
3716
+ // nothing, and the thrown signal reason keeps the rejection
3717
+ // causally classified as the abort it is.
3718
+ active.throwIfAborted();
3719
+ // Abort ownership — the listener that terminates the group and
3720
+ // the escalation timer — spans the whole invocation body, not
3721
+ // just the spawn-to-close window: an abort landing during the
3722
+ // post-exit emission tail must still kill surviving group
3723
+ // members before the actor settles (slc/link.md §Script
3724
+ // execution). One finally releases both.
3725
+ let child;
3726
+ let killTimer;
3727
+ const signalGroup = (sig) => {
3728
+ if (child?.pid !== undefined) {
3729
+ try {
3730
+ process.kill(-child.pid, sig);
3731
+ }
3732
+ catch {
3733
+ // Confirmation belongs to the bounded liveness probe below:
3734
+ // a failed signal can mean ESRCH, EPERM, or another fault.
3735
+ }
1992
3736
  }
1993
- catch (error) {
1994
- reject(error);
3737
+ };
3738
+ // After a SIGKILL is posted, settlement waits for the group to
3739
+ // stop being signalable — bounded by the same grace so an
3740
+ // unreapable member outside the runtime's control cannot stall
3741
+ // the turn forever. Observed teardown is milliseconds.
3742
+ let groupGonePromise;
3743
+ const awaitGroupGone = () => {
3744
+ const pid = child?.pid;
3745
+ if (pid === undefined)
3746
+ return Promise.resolve();
3747
+ groupGonePromise ??= (async () => {
3748
+ const teardownFailure = (message, cause) => {
3749
+ const failure = new ScriptProcessGroupTeardownError(pid, message, cause);
3750
+ // A teardown failure is not an authored script result.
3751
+ // Surface it at the active public boundary even though
3752
+ // XState also routes the rejected actor through onError.
3753
+ controlPlaneError ??= failure;
3754
+ return failure;
3755
+ };
3756
+ const deadline = Date.now() + SCRIPT_ABORT_KILL_GRACE_MS;
3757
+ let lastProbeError;
3758
+ for (;;) {
3759
+ try {
3760
+ process.kill(-pid, 0);
3761
+ }
3762
+ catch (error) {
3763
+ if (isNoSuchProcess(error))
3764
+ return;
3765
+ // EPERM confirms that at least one process in the group
3766
+ // still exists but is not signalable by this process. Keep
3767
+ // waiting for ESRCH within the bound; every other probe
3768
+ // error makes confirmation itself unreliable immediately.
3769
+ if (!isProcessPermissionDenied(error)) {
3770
+ throw teardownFailure('the liveness probe failed', error);
3771
+ }
3772
+ lastProbeError = error;
3773
+ }
3774
+ if (Date.now() >= deadline) {
3775
+ throw teardownFailure(`the group remained signalable after ${SCRIPT_ABORT_KILL_GRACE_MS}ms`, lastProbeError);
3776
+ }
3777
+ await new Promise((tick) => setTimeout(tick, 5));
3778
+ }
3779
+ })();
3780
+ return groupGonePromise;
3781
+ };
3782
+ const onAbort = () => {
3783
+ signalGroup('SIGTERM');
3784
+ killTimer = setTimeout(() => signalGroup('SIGKILL'), SCRIPT_ABORT_KILL_GRACE_MS);
3785
+ };
3786
+ // An abort observed once the shell has already exited rejects
3787
+ // with the signal's reason before guard resolution and before
3788
+ // starting any further script emission — after killing whatever
3789
+ // group members outlived the shell. The shell's own exit ended
3790
+ // the TERM grace's purpose, so escalation is immediate here.
3791
+ const settleIfAborted = async () => {
3792
+ if (!active.aborted)
1995
3793
  return;
3794
+ signalGroup('SIGKILL');
3795
+ await awaitGroupGone();
3796
+ active.throwIfAborted();
3797
+ };
3798
+ let invocationFailed = false;
3799
+ try {
3800
+ const exitStatus = await new Promise((resolve, reject) => {
3801
+ try {
3802
+ // detached: the shell leads its own POSIX process group,
3803
+ // so an abort can terminate the command's whole group — a
3804
+ // lone SIGTERM to the wrapper never reaches backgrounded
3805
+ // members.
3806
+ child = spawn('sh', ['-c', input.command], {
3807
+ cwd,
3808
+ stdio: 'ignore',
3809
+ detached: true,
3810
+ });
3811
+ }
3812
+ catch (error) {
3813
+ reject(error);
3814
+ return;
3815
+ }
3816
+ // On abort, terminate the group and escalate — but settle
3817
+ // only from 'close', after the shell itself has exited, so
3818
+ // the turn never reports quiescence while the script still
3819
+ // runs (slc/link.md §Abort). SIGKILL is untrappable, so
3820
+ // 'close' is bounded by the grace.
3821
+ active.addEventListener('abort', onAbort, { once: true });
3822
+ child.on('error', (error) => {
3823
+ reject(error);
3824
+ });
3825
+ child.on('close', (code) => {
3826
+ if (active.aborted) {
3827
+ // The shell may exit cooperatively on the group SIGTERM
3828
+ // while a TERM-immune same-group descendant survives;
3829
+ // the group stays addressable while any member lives,
3830
+ // so kill it and await its disappearance before
3831
+ // settling (slc/link.md §Script execution).
3832
+ signalGroup('SIGKILL');
3833
+ void awaitGroupGone().then(() => reject(active.reason), reject);
3834
+ return;
3835
+ }
3836
+ resolve(typeof code === 'number' ? code : 1);
3837
+ });
3838
+ });
3839
+ await settleIfAborted();
3840
+ await ports.emitStatus(`Executed script for ${input.stateId} (exit ${exitStatus}).`);
3841
+ await settleIfAborted();
3842
+ await ports.emitTelemetry({
3843
+ topic: 'playbook.script',
3844
+ payload: {
3845
+ stateId: input.stateId,
3846
+ sourceItem: input.sourceItem,
3847
+ exitStatus,
3848
+ },
3849
+ });
3850
+ await settleIfAborted();
3851
+ if (exitStatus === 0) {
3852
+ return { guard: okGuard, exitStatus: 0 };
1996
3853
  }
1997
- const onAbort = () => {
1998
- child.kill('SIGTERM');
1999
- reject(active.reason ?? new Error('script aborted'));
2000
- };
2001
- if (active.aborted) {
2002
- onAbort();
2003
- return;
3854
+ return { guard: failedGuard, exitStatus };
3855
+ }
3856
+ catch (error) {
3857
+ // Preserve the invocation's authoritative exact cancellation or
3858
+ // distinct sink failure after teardown succeeds. The finally
3859
+ // block may replace it only with a distinct teardown failure
3860
+ // when the process group cannot be confirmed gone.
3861
+ invocationFailed = true;
3862
+ throw error;
3863
+ }
3864
+ finally {
3865
+ try {
3866
+ if (active.aborted) {
3867
+ signalGroup('SIGKILL');
3868
+ await awaitGroupGone();
3869
+ if (!invocationFailed)
3870
+ active.throwIfAborted();
3871
+ }
2004
3872
  }
2005
- active.addEventListener('abort', onAbort, { once: true });
2006
- child.on('error', (error) => {
2007
- active.removeEventListener('abort', onAbort);
2008
- reject(error);
2009
- });
2010
- child.on('close', (code) => {
3873
+ finally {
2011
3874
  active.removeEventListener('abort', onAbort);
2012
- resolve(typeof code === 'number' ? code : 1);
2013
- });
2014
- });
2015
- await ports.emitStatus(`Executed script for ${input.stateId} (exit ${exitStatus}).`);
2016
- await ports.emitTelemetry({
2017
- topic: 'playbook.script',
2018
- payload: {
2019
- stateId: input.stateId,
2020
- sourceItem: input.sourceItem,
2021
- exitStatus,
2022
- },
2023
- });
2024
- if (exitStatus === 0) {
2025
- return { guard: okGuard, exitStatus: 0 };
3875
+ if (killTimer !== undefined)
3876
+ clearTimeout(killTimer);
3877
+ }
2026
3878
  }
2027
- return { guard: failedGuard, exitStatus };
2028
3879
  });
2029
3880
  }
2030
3881
  const nestedBridge = createNestedPlaybookBridge({
2031
3882
  nextCallId: () => `playbook-${++playbookCallSequence}`,
2032
3883
  getBoundarySignal: () => activeSignal,
2033
3884
  callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
2034
- emitStarted: async (event) => {
3885
+ emitStarted: async (event, aborts) => {
2035
3886
  playbookCallTurnIds.set(event.callId, activeTurnId);
3887
+ if (hasGovernedPlayerStates) {
3888
+ playbookCallEffectPrefixes.set(event.callId, activeEffectLedgerPrefixSequence);
3889
+ }
2036
3890
  await emitTrace('playbook.call.started', {
2037
3891
  stateId: event.stateId,
2038
3892
  playbookId: event.playbookId,
@@ -2040,9 +3894,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2040
3894
  }, {
2041
3895
  ...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
2042
3896
  callId: event.callId,
2043
- });
3897
+ }, aborts);
2044
3898
  },
2045
- emitFinished: async (event) => {
3899
+ emitFinished: async (event, aborts) => {
2046
3900
  const turnId = playbookCallTurnIds.get(event.callId);
2047
3901
  try {
2048
3902
  await emitTrace('playbook.call.finished', {
@@ -2053,22 +3907,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
2053
3907
  }, {
2054
3908
  ...(turnId !== undefined ? { turnId } : {}),
2055
3909
  callId: event.callId,
2056
- });
3910
+ }, aborts);
2057
3911
  }
2058
3912
  finally {
2059
3913
  playbookCallTurnIds.delete(event.callId);
3914
+ playbookCallEffectPrefixes.delete(event.callId);
2060
3915
  }
2061
3916
  },
2062
3917
  drain: drainEmissions,
2063
- bindResumeSignal: (signal) => {
3918
+ bindResumeSignal: (signal, aborts) => {
2064
3919
  activeSignal = signal;
3920
+ activeAborts = aborts ?? abortReasonClassifier(signal);
3921
+ },
3922
+ bindActorSettlement: (aborts) => {
3923
+ actorSettlementAborts = aborts;
2065
3924
  },
2066
- onControlPlaneError: (error) => {
2067
- if (!activeSignal?.aborted)
3925
+ onControlPlaneError: (error, aborts) => {
3926
+ // The shared bridge classifies before reporting against its own
3927
+ // invocation-and-resume signals; classify once more here against
3928
+ // the boundary signal so a report that is the active boundary's
3929
+ // exact abort reason can never masquerade as a control error
3930
+ // (slc/link.md §Abort).
3931
+ if (!aborts?.isAbortReason(error) &&
3932
+ !activeAborts?.isAbortReason(error)) {
2068
3933
  controlPlaneError ??= error;
3934
+ }
2069
3935
  },
2070
- onBackgroundError: (error) => {
2071
- emissionFailure ??= error;
3936
+ onBackgroundError: (error, aborts) => {
3937
+ if (!aborts?.isAbortReason(error))
3938
+ emissionFailure ??= { error };
2072
3939
  },
2073
3940
  });
2074
3941
  function tracePositionForActiveTurn() {
@@ -2082,11 +3949,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
2082
3949
  previousState: previousState ?? null,
2083
3950
  state,
2084
3951
  };
2085
- if (state.stateId === 'awaitBossReply') {
2086
- const pendingBossQuestion = pendingBossQuestionFromContext(context);
2087
- if (pendingBossQuestion !== undefined) {
2088
- payload.pendingBossQuestion = pendingBossQuestion;
2089
- }
3952
+ const pendingBossQuestion = pendingBossQuestionForState(state, context);
3953
+ if (pendingBossQuestion !== undefined &&
3954
+ !hasUnresolvedReconciliation()) {
3955
+ payload.pendingBossQuestion = pendingBossQuestion;
2090
3956
  }
2091
3957
  if (state.stateId === 'failed') {
2092
3958
  const lastError = normalizeErrorFull(context.lastError);
@@ -2095,9 +3961,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
2095
3961
  }
2096
3962
  return snapshotJsonValue(payload, 'FSM telemetry payload');
2097
3963
  }
2098
- function enqueueTransitionEmission(payload, state, statuses, position) {
3964
+ function enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, position, aborts) {
2099
3965
  const currentSession = requireSession();
2100
3966
  const transitionTrace = createTraceEvent('fsm.transition', payload, position);
3967
+ const acceptedOutcomeTraces = acceptedOutcomes.map((acceptedOutcome) => createTraceEvent('outcome.accepted', acceptedOutcome, position));
2101
3968
  const statusEmissions = statuses.map(({ message, data }) => ({
2102
3969
  message,
2103
3970
  data,
@@ -2117,6 +3984,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2117
3984
  topic: 'playbook.fsm.state',
2118
3985
  payload,
2119
3986
  });
3987
+ for (const acceptedOutcome of acceptedOutcomeTraces) {
3988
+ await currentSession.ports.emitTelemetry({
3989
+ topic: 'playbook.trace',
3990
+ payload: acceptedOutcome,
3991
+ });
3992
+ }
2120
3993
  for (const status of statusEmissions) {
2121
3994
  await currentSession.ports.emitTelemetry({
2122
3995
  topic: 'playbook.trace',
@@ -2124,13 +3997,38 @@ export function createXStatePlaybookRuntime(machine, spec) {
2124
3997
  });
2125
3998
  await currentSession.ports.emitStatus(status.message, status.data);
2126
3999
  }
2127
- }).catch(() => undefined);
4000
+ }, aborts).catch(() => undefined);
2128
4001
  }
2129
- function latchInspectionError(error) {
2130
- if (activeSignal !== undefined)
2131
- controlPlaneError ??= error;
4002
+ // One classifying latch for every runtime-observed error — inspection
4003
+ // failures and root-actor errors alike. Outside a boundary the error
4004
+ // rides the emission channel, which the next boundary's (or init's)
4005
+ // drain throws; inside a boundary it is a control-plane error unless it
4006
+ // is the boundary signal's own abort reason (slc/link.md §Abort).
4007
+ function latchRuntimeError(error, aborts = activeAborts) {
4008
+ if (aborts?.isAbortReason(error))
4009
+ return;
4010
+ if (activeSignal === undefined)
4011
+ emissionFailure ??= { error };
2132
4012
  else
2133
- emissionFailure ??= error;
4013
+ controlPlaneError ??= error;
4014
+ }
4015
+ function consumeActorSettlementAborts(forSnapshot = false) {
4016
+ const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
4017
+ actorSettlementAborts = undefined;
4018
+ actorSettlementErrorAborts = undefined;
4019
+ if (forSnapshot && aborts !== undefined) {
4020
+ // XState can report an errored root through both its inspection
4021
+ // snapshot and subscriber. Keep the same provenance through that
4022
+ // synchronous notification only; an ordinary transition must not
4023
+ // lend it to a later unrelated actor error.
4024
+ actorSettlementErrorAborts = aborts;
4025
+ queueMicrotask(() => {
4026
+ if (actorSettlementErrorAborts === aborts) {
4027
+ actorSettlementErrorAborts = undefined;
4028
+ }
4029
+ });
4030
+ }
4031
+ return aborts;
2134
4032
  }
2135
4033
  // PBRT-6: the single seam that stops this runtime's actor. Stopping a
2136
4034
  // still-running actor fires one more `@xstate.snapshot` for the
@@ -2144,10 +4042,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2144
4042
  if (!actor)
2145
4043
  return;
2146
4044
  suppressInspectionEmissions = true;
4045
+ acceptedOutcomeConsumer.reset();
2147
4046
  actor.stop();
2148
4047
  }
2149
4048
  function buildActor(ports, machineSnapshot) {
2150
4049
  priorState = undefined;
4050
+ acceptedOutcomeConsumer.reset();
2151
4051
  const actors = {};
2152
4052
  if (declaredActors.has('player'))
2153
4053
  actors.player = playerActor(ports);
@@ -2170,12 +4070,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
2170
4070
  ? {}
2171
4071
  : { snapshot: machineSnapshot }),
2172
4072
  inspect: (inspectionEvent) => {
2173
- if (inspectionEvent.type !== '@xstate.snapshot')
2174
- return;
2175
4073
  if (inspectionEvent.actorRef !== builtActor)
2176
4074
  return;
2177
4075
  if (suppressInspectionEmissions)
2178
4076
  return;
4077
+ if (inspectionEvent.type === '@xstate.action') {
4078
+ try {
4079
+ acceptedOutcomeConsumer.capture(inspectionEvent.action);
4080
+ }
4081
+ catch (error) {
4082
+ latchRuntimeError(error);
4083
+ }
4084
+ return;
4085
+ }
4086
+ if (inspectionEvent.type !== '@xstate.snapshot')
4087
+ return;
4088
+ const settlementAborts = consumeActorSettlementAborts(true);
2179
4089
  try {
2180
4090
  const snap = inspectionEvent.snapshot;
2181
4091
  const state = normalizePlaybookSnapshot(snap);
@@ -2183,18 +4093,70 @@ export function createXStatePlaybookRuntime(machine, spec) {
2183
4093
  throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
2184
4094
  }
2185
4095
  const previousState = priorState;
4096
+ acceptReconstructedGovernedDelivery(state);
4097
+ let acceptedOutcomes = [];
4098
+ try {
4099
+ acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
4100
+ }
4101
+ catch (error) {
4102
+ latchRuntimeError(error);
4103
+ }
4104
+ if (state.stateId === 'failed') {
4105
+ if (previousState?.stateId !== 'failed' ||
4106
+ activeGovernedBoundarySeen) {
4107
+ latchFailedGovernedAttempt();
4108
+ }
4109
+ }
4110
+ else if (previousState?.stateId === 'failed') {
4111
+ failedEffectBoundaryPrefix = undefined;
4112
+ failedGovernedAttemptId = undefined;
4113
+ failedGovernedAttemptUnknown = false;
4114
+ }
2186
4115
  const context = (snap.context ??
2187
4116
  {});
4117
+ if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
4118
+ deferredReconciliationOperationId !== undefined) {
4119
+ priorState = state;
4120
+ return;
4121
+ }
4122
+ if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
4123
+ expectedBoundPendingQuestion !== undefined &&
4124
+ !deferInspectionEmissions) {
4125
+ validateBoundQuestionProjection();
4126
+ }
2188
4127
  const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
2189
- const statuses = statusesForState(state, context, inspectionEvent.event);
2190
- enqueueTransitionEmission(payload, state, statuses, tracePositionForActiveTurn());
4128
+ const stateStatuses = statusesForState(state, context, inspectionEvent.event);
4129
+ const outcomeStatuses = usesDefaultStatuses
4130
+ ? acceptedOutcomes.map(({ acceptedOutcome }) => ({
4131
+ message: `→ ${acceptedOutcome}`,
4132
+ }))
4133
+ : [];
4134
+ const statuses = [...outcomeStatuses, ...stateStatuses];
4135
+ const publish = () => enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, tracePositionForActiveTurn(), settlementAborts);
4136
+ if (deferInspectionEmissions) {
4137
+ deferredInspectionEmissions.push(publish);
4138
+ }
4139
+ else {
4140
+ publish();
4141
+ }
2191
4142
  priorState = state;
2192
4143
  }
2193
4144
  catch (error) {
2194
- latchInspectionError(error);
4145
+ acceptedOutcomeConsumer.reset();
4146
+ latchRuntimeError(error, settlementAborts);
2195
4147
  }
2196
4148
  },
2197
4149
  });
4150
+ // A synchronously-errored actor is already quiescent, so the turn's
4151
+ // quiescence wait never subscribes and XState would report the error
4152
+ // as unhandled after the boundary returns. Observe it through the
4153
+ // classifying latch: mid-boundary it is the control-plane error
4154
+ // unless it is the abort reason itself; at startup it rides the
4155
+ // emission channel so `init`'s own drain rejects with it and the
4156
+ // failed-start cleanup runs (slc/link.md §Session lifecycle).
4157
+ builtActor.subscribe({
4158
+ error: (error) => latchRuntimeError(error, consumeActorSettlementAborts()),
4159
+ });
2198
4160
  return builtActor;
2199
4161
  }
2200
4162
  function runResultFor(outcome, error) {
@@ -2202,6 +4164,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2202
4164
  if (outcome === 'quiescent' || outcome === 'no-action') {
2203
4165
  return { outcome, state };
2204
4166
  }
4167
+ if (outcome === 'unresolved-effect') {
4168
+ return { outcome, state };
4169
+ }
2205
4170
  if (outcome === 'suspended') {
2206
4171
  const pendingCall = nestedBridge.getPendingCall();
2207
4172
  if (!pendingCall) {
@@ -2211,14 +4176,19 @@ export function createXStatePlaybookRuntime(machine, spec) {
2211
4176
  }
2212
4177
  if (outcome === 'terminal') {
2213
4178
  const output = actor?.getSnapshot()?.output;
2214
- if (output !== undefined) {
2215
- return {
2216
- outcome,
2217
- state,
2218
- output: snapshotJsonValue(output, 'terminal playbook output'),
2219
- };
2220
- }
2221
- return { outcome, state };
4179
+ const stateDescription = !hasUnresolvedReconciliation()
4180
+ ? stateDescriptionFor(state)
4181
+ : undefined;
4182
+ return {
4183
+ outcome,
4184
+ state,
4185
+ ...(stateDescription === undefined ? {} : { stateDescription }),
4186
+ ...(output === undefined
4187
+ ? {}
4188
+ : {
4189
+ output: snapshotJsonValue(output, 'terminal playbook output'),
4190
+ }),
4191
+ };
2222
4192
  }
2223
4193
  const failure = error ??
2224
4194
  (outcome === 'failed'
@@ -2236,15 +4206,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
2236
4206
  function settledOutcome(signal) {
2237
4207
  if (nestedBridge.getPendingCall())
2238
4208
  return 'suspended';
2239
- if (signal.aborted)
2240
- return 'aborted';
2241
4209
  const state = currentState();
2242
4210
  if (state.status === 'error') {
4211
+ // An errored actor outranks a coincident abort unless the actor's
4212
+ // error is the abort reason itself (slc/link.md §Abort).
2243
4213
  const actorError = actor?.getSnapshot()?.error;
4214
+ if (actorError !== undefined && isAbortFailure(actorError, signal)) {
4215
+ return 'aborted';
4216
+ }
2244
4217
  throw actorError ?? new Error(`${label} actor entered error status`);
2245
4218
  }
4219
+ // Terminal completion outranks a coincident abort: the work finished,
4220
+ // and reporting `aborted` would hide a terminal machine behind a
4221
+ // settlement a later turn silently restarts (slc/link.md §Abort).
2246
4222
  if (state.status === 'done')
2247
4223
  return 'terminal';
4224
+ if (signal.aborted)
4225
+ return 'aborted';
2248
4226
  if (state.stateId === 'failed')
2249
4227
  return 'failed';
2250
4228
  return 'quiescent';
@@ -2255,12 +4233,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2255
4233
  ...stateIdentity(result.state.stateId),
2256
4234
  };
2257
4235
  }
2258
- // Shared failed-start cleanup for init and restore: stop the actor,
2259
- // abort/drain nested and host work, optionally emit one best-effort
4236
+ // Shared failed-start cleanup for init, restore, and adoption: stop the
4237
+ // actor, abort/drain nested and host work, optionally emit one best-effort
2260
4238
  // session.disposed boundary, and unbind every closure field so dispose
2261
- // stays callable. The caller rethrows its original failure. A restore
2262
- // failure skips the disposal trace — the parked session was never
2263
- // re-bound in this process, so its persisted snapshot stays
4239
+ // stays callable. The caller rethrows its original failure. A snapshot
4240
+ // start failure skips the disposal trace — the parked generation was
4241
+ // never re-bound in this process, so its persisted snapshot stays
2264
4242
  // authoritative (DR-014 §2).
2265
4243
  async function cleanupFailedStart(cause, options) {
2266
4244
  let finalState;
@@ -2308,6 +4286,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2308
4286
  privateResumeTokens.clear();
2309
4287
  activePlayerKeys.clear();
2310
4288
  playbookCallTurnIds.clear();
4289
+ playbookCallEffectPrefixes.clear();
2311
4290
  activeEmissionCalls.clear();
2312
4291
  emissionQueue.clear();
2313
4292
  judgeQueue.clear();
@@ -2317,10 +4296,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
2317
4296
  savedPorts = undefined;
2318
4297
  runtimePorts = undefined;
2319
4298
  activeSignal = undefined;
4299
+ activeAborts = undefined;
4300
+ actorSettlementAborts = undefined;
4301
+ actorSettlementErrorAborts = undefined;
4302
+ activeAbortEmission = undefined;
2320
4303
  activeTurnId = undefined;
4304
+ activeGovernedBoundarySeen = false;
4305
+ activeGovernedAttemptId = undefined;
4306
+ activeEffectLedgerPrefixSequence = undefined;
4307
+ failedGovernedAttemptUnknown = false;
4308
+ failedEffectBoundaryPrefix = undefined;
4309
+ failedGovernedAttemptId = undefined;
2321
4310
  controlPlaneError = undefined;
2322
4311
  emissionFailure = undefined;
2323
4312
  priorState = undefined;
4313
+ retainedEffectSourceSessionId = undefined;
4314
+ retainedEffectReconciliation = undefined;
4315
+ retainedEffectReconciliationRequired = false;
4316
+ reconstructedGovernedDelivery = undefined;
4317
+ reconstructedGovernedPrefixSequence = undefined;
4318
+ reconstructedAcceptancePending = undefined;
4319
+ governedSettlementsByBoundaryId.clear();
4320
+ governedCompletionEvidenceByBoundaryId.clear();
4321
+ unresolvedSemanticBoundaryIds.clear();
4322
+ deferredReconciliationOperationId = undefined;
4323
+ deferredSettlementClosure = undefined;
4324
+ expectedBoundPendingQuestion = undefined;
4325
+ activeDeferredContinuation = undefined;
4326
+ deferInspectionEmissions = false;
4327
+ deferredInspectionEmissions = [];
2324
4328
  lastBossEvent = undefined;
2325
4329
  suppressInspectionEmissions = false;
2326
4330
  initialized = false;
@@ -2338,28 +4342,58 @@ export function createXStatePlaybookRuntime(machine, spec) {
2338
4342
  can.call(snapshot, event) ===
2339
4343
  true);
2340
4344
  }
4345
+ // DR-034: where the artifact names the FSM context member its entry
4346
+ // action copies the exact Boss text into, that member of the live
4347
+ // snapshot is the retry payload's source. The persisted machine snapshot
4348
+ // carries it, so the candidate derives identically in the process that
4349
+ // exported the snapshot and in one that restored it, and a failure
4350
+ // reached after a Boss reply — whose recorded event the failure state
4351
+ // refuses — is recoverable too. Naming the member is the artifact's
4352
+ // statement that it holds the entry text: a same-named member is never
4353
+ // assumed, since inferring one would turn any matching context member
4354
+ // into a replay payload without its author saying so.
4355
+ // Declared and absent or empty excludes the candidate rather than
4356
+ // falling back to the record, which would make the action depend on the
4357
+ // process again — the very thing this source exists to end.
4358
+ function retryEventFrom(snapshot) {
4359
+ const entryEvent = spec.entryEvent;
4360
+ if (entryEvent?.contextField === undefined)
4361
+ return lastBossEvent;
4362
+ const context = snapshot?.context;
4363
+ const text = isPlainObject(context)
4364
+ ? context[entryEvent.contextField]
4365
+ : undefined;
4366
+ if (typeof text !== 'string' || text.trim() === '')
4367
+ return undefined;
4368
+ return { type: entryEvent.type, [entryEvent.textField]: text };
4369
+ }
2341
4370
  // The failure-state retry entry replays the recorded last classified
2342
- // event with its recorded payload. A candidate whose event the live
2343
- // snapshot does not accept or whose payload the runtime never
2344
- // recordedis excluded rather than completed with invented text.
4371
+ // event with its recorded payload, or the entry event the declared
4372
+ // context member above sources. A candidate whose event the live
4373
+ // snapshot does not accept or whose payload the runtime can source
4374
+ // from neither — is excluded rather than completed with invented text.
2345
4375
  function retryActionFor(snapshot, stateId) {
2346
- if (stateId !== 'failed' || lastBossEvent === undefined) {
4376
+ if (stateId !== 'failed')
2347
4377
  return undefined;
2348
- }
2349
- if (!snapshotCan(snapshot, lastBossEvent))
4378
+ if (!failedAttemptAllowsReplay())
4379
+ return undefined;
4380
+ const retryEvent = retryEventFrom(snapshot);
4381
+ if (retryEvent === undefined)
4382
+ return undefined;
4383
+ if (!snapshotCan(snapshot, retryEvent))
2350
4384
  return undefined;
2351
4385
  // A recorded explicit-state-jump event names the exact state its
2352
4386
  // replay re-enters: the root BOSS_INTERRUPT shape is a guarded
2353
4387
  // multi-arm list keyed on `targetId`, so the first configured arm
2354
4388
  // may label a different state than the one the recorded event
2355
4389
  // actually resumes.
2356
- const recordedTargetId = lastBossEvent.type === JUMP_EVENT_TYPE
2357
- ? lastBossEvent.targetId
4390
+ const recordedTargetId = retryEvent.type === JUMP_EVENT_TYPE
4391
+ ? retryEvent.targetId
2358
4392
  : undefined;
2359
4393
  const target = typeof recordedTargetId === 'string' &&
2360
4394
  recordedTargetId.trim().length > 0
2361
4395
  ? recordedTargetId
2362
- : firstTransitionTarget(machine, stateId, lastBossEvent.type);
4396
+ : firstTransitionTarget(machine, stateId, retryEvent.type);
2363
4397
  // PBRT-52: a label is written from a source state description, never
2364
4398
  // from an identifier. Falling back to the target id — or, with no
2365
4399
  // resolvable target, to the FSM event type — makes the label *be* the
@@ -2373,10 +4407,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
2373
4407
  return undefined;
2374
4408
  return {
2375
4409
  action: {
2376
- id: `retry:${lastBossEvent.type}`,
4410
+ id: `retry:${retryEvent.type}`,
2377
4411
  label: `Retry: ${description}`,
2378
4412
  },
2379
- event: lastBossEvent,
4413
+ event: retryEvent,
2380
4414
  };
2381
4415
  }
2382
4416
  function deriveControlActions(snapshot) {
@@ -2399,6 +4433,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
2399
4433
  return [];
2400
4434
  }
2401
4435
  const derived = [];
4436
+ if (hasUnresolvedReconciliation()) {
4437
+ const operation = deferredReconciliationOperationId === undefined
4438
+ ? undefined
4439
+ : effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === deferredReconciliationOperationId);
4440
+ const deferredRestoreOperationId = operation?.checkpointRestorationEligible === true
4441
+ ? operation.operationId
4442
+ : undefined;
4443
+ derived.push({
4444
+ action: {
4445
+ id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
4446
+ label: 'Retry unresolved effect reconciliation',
4447
+ },
4448
+ unresolvedEffectAction: 'reconcile',
4449
+ ...(deferredRestoreOperationId === undefined
4450
+ ? {}
4451
+ : { deferredRestoreOperationId }),
4452
+ }, {
4453
+ action: {
4454
+ id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
4455
+ label: 'Abandon unresolved workflow attempt',
4456
+ },
4457
+ unresolvedEffectAction: 'abandon',
4458
+ });
4459
+ return derived;
4460
+ }
2402
4461
  const retry = retryActionFor(snapshot, state.stateId);
2403
4462
  if (retry !== undefined)
2404
4463
  derived.push(retry);
@@ -2446,45 +4505,555 @@ export function createXStatePlaybookRuntime(machine, spec) {
2446
4505
  try {
2447
4506
  projected[key] = snapshotJsonValue(value instanceof Error ? normalizeError(value) : value, `control context ${key}`);
2448
4507
  }
2449
- catch {
2450
- // Declared but not JSON-safe — dropped.
4508
+ catch {
4509
+ // Declared but not JSON-safe — dropped.
4510
+ }
4511
+ }
4512
+ return Object.keys(projected).length === 0 ? undefined : projected;
4513
+ }
4514
+ // PBRT-52: the view's Boss-facing state description — the meaning of the
4515
+ // state the runtime is in, written by the artifact's own source, from the
4516
+ // same descriptions its action labels are written from. A control view is
4517
+ // the only grounding a controller host has for a status answer, and an
4518
+ // internal state id is not Boss-appropriate text
4519
+ // (CAPPLAY-5), so the runtime publishes the meaning
4520
+ // rather than leaving the host to substitute the identifier for it. A
4521
+ // state whose source declares no description publishes none: an id is
4522
+ // never promoted into a description by default.
4523
+ function stateDescriptionFor(state) {
4524
+ const keys = [
4525
+ ...(state.stateId === undefined ? [] : [state.stateId]),
4526
+ ...(typeof state.value === 'string' ? [state.value] : []),
4527
+ ...state.activeStateIds,
4528
+ ];
4529
+ for (const key of keys) {
4530
+ const description = stateDescriptions.get(key);
4531
+ if (description !== undefined)
4532
+ return description;
4533
+ }
4534
+ return undefined;
4535
+ }
4536
+ function receiptTracePayload(receipt) {
4537
+ return {
4538
+ disposition: receipt.disposition,
4539
+ ...(receipt.disposition === 'rejected'
4540
+ ? { reason: receipt.reason }
4541
+ : {}),
4542
+ ...(receipt.disposition === 'failed' ? { error: receipt.error } : {}),
4543
+ ...(receipt.disposition === 'executed' ? { run: receipt.run } : {}),
4544
+ };
4545
+ }
4546
+ // DR-014 / DR-038: restore and adoption share one transactional snapshot
4547
+ // start. Adoption deliberately differs at the public boundary so a host
4548
+ // can feature-detect permission to bind a retained generation to a fresh
4549
+ // engagement identity; the runtime-visible schema, playbook, and actor
4550
+ // state remain exact, while adoption deliberately re-keys a suspended
4551
+ // bridge into the fresh target counter and session lineage.
4552
+ async function rehydrateSnapshot(kind, nextSession, snapshot, context) {
4553
+ if (initialized || disposed || disposalPromise !== undefined) {
4554
+ throw new Error(`createPlaybookRuntime.${kind}: already initialized`);
4555
+ }
4556
+ const boundSession = bindSession(nextSession);
4557
+ const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
4558
+ if (kind === 'adopt' &&
4559
+ effectLedgerCapability !== undefined &&
4560
+ (boundSnapshot.retainedEffectReconciliation?.checkpoint ??
4561
+ boundSnapshot.effectLedger).boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
4562
+ throw new TypeError('retained runtime checkpoint contains an incomplete physical boundary');
4563
+ }
4564
+ const hostEffectLedger = currentEffectLedger();
4565
+ if (kind === 'restore' &&
4566
+ !isDeepStrictEqual(boundSnapshot.effectLedger, hostEffectLedger)) {
4567
+ throw new TypeError('runtime snapshot effectLedger does not equal the current host mirror');
4568
+ }
4569
+ if (kind === 'adopt' &&
4570
+ !isPlaybookEffectLedgerMonotonicExtension(boundSnapshot.effectLedger, hostEffectLedger)) {
4571
+ throw new TypeError('retained runtime snapshot effectLedger is not a monotonic prefix of the current host mirror');
4572
+ }
4573
+ effectLedgerMirror = hostEffectLedger;
4574
+ if (declaredActors.has('captain') &&
4575
+ boundSnapshot.sequences.captainCall === undefined) {
4576
+ throw new TypeError('runtime snapshot sequences.captainCall is required for a direct-Captain artifact');
4577
+ }
4578
+ const adoptionContext = kind === 'adopt'
4579
+ ? snapshotAdoptionContext(context, boundSession, boundSnapshot)
4580
+ : undefined;
4581
+ if (adoptionContext !== undefined &&
4582
+ effectLedgerCapability !== undefined &&
4583
+ !RETAINED_EFFECT_SESSION_ID_PATTERN.test(adoptionContext.sourceSessionId)) {
4584
+ throw new TypeError('schema-3 retained adoption sourceSessionId must be a canonical UUID');
4585
+ }
4586
+ const retainedReconciliation = boundSnapshot.retainedEffectReconciliation ??
4587
+ (adoptionContext !== undefined &&
4588
+ effectLedgerCapability !== undefined &&
4589
+ !retainedAdoptionCheckpointIsSafe(boundSnapshot.effectLedger, hostEffectLedger)
4590
+ ? Object.freeze({
4591
+ sourceSessionId: boundSnapshot.retainedEffectSourceSessionId ??
4592
+ adoptionContext.sourceSessionId,
4593
+ checkpoint: boundSnapshot.effectLedger,
4594
+ })
4595
+ : undefined);
4596
+ retainedEffectSourceSessionId =
4597
+ boundSnapshot.retainedEffectSourceSessionId ??
4598
+ boundSnapshot.retainedEffectReconciliation?.sourceSessionId ??
4599
+ (adoptionContext !== undefined && effectLedgerCapability !== undefined
4600
+ ? adoptionContext.sourceSessionId
4601
+ : undefined);
4602
+ bindRetainedEffectReconciliation(retainedReconciliation, hostEffectLedger);
4603
+ const sourceSuspendedCall = boundSnapshot.suspendedCall;
4604
+ const suspendedCall = adoptionContext !== undefined && sourceSuspendedCall !== undefined
4605
+ ? Object.freeze({
4606
+ callId: 'playbook-1',
4607
+ stateId: sourceSuspendedCall.stateId,
4608
+ playbookId: sourceSuspendedCall.playbookId,
4609
+ text: sourceSuspendedCall.text,
4610
+ childSessionId: adoptionContext.targetChildSessionId,
4611
+ })
4612
+ : sourceSuspendedCall;
4613
+ let priorExternalPlayerTokens;
4614
+ let externalStoreRestoreAttempted = false;
4615
+ let adoptionStartAttempted = false;
4616
+ initialized = true;
4617
+ let finishInitialization;
4618
+ const initialization = new Promise((resolve) => {
4619
+ finishInitialization = resolve;
4620
+ });
4621
+ initInFlight = initialization;
4622
+ const initTask = (async () => {
4623
+ session = boundSession;
4624
+ syncDeferredReconciliationOverlay();
4625
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
4626
+ prepareReconstructedGovernedDelivery(boundSnapshot.state, effectLedgerMirror);
4627
+ savedPorts = boundSession.ports;
4628
+ runtimePorts = createRuntimePorts(boundSession.ports);
4629
+ if (adoptionContext === undefined) {
4630
+ traceSequence = boundSnapshot.sequences.trace;
4631
+ turnSequence = boundSnapshot.sequences.turn;
4632
+ judgeCallSequence = boundSnapshot.sequences.judgeCall;
4633
+ playerCallSequence = boundSnapshot.sequences.playerCall;
4634
+ playbookCallSequence = boundSnapshot.sequences.playbookCall;
4635
+ captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
4636
+ // The runtime snapshot carries no apply counter (PBRT-50); every
4637
+ // apply boundary consumed trace numbers, so the persisted trace
4638
+ // counter is a collision-safe id floor here too, keeping
4639
+ // `apply-<n>` call ids unique across a snapshot start.
4640
+ applyCallSequence = boundSnapshot.sequences.trace;
4641
+ }
4642
+ else {
4643
+ // DR-038 §5: a new engagement owns a new counter space. A
4644
+ // rebased live child consumes the first target-local playbook id;
4645
+ // all other counters begin before their first target boundary.
4646
+ traceSequence = 0;
4647
+ turnSequence = 0;
4648
+ judgeCallSequence = 0;
4649
+ playerCallSequence = 0;
4650
+ playbookCallSequence = suspendedCall === undefined ? 0 : 1;
4651
+ captainCallSequence = 0;
4652
+ applyCallSequence = 0;
4653
+ }
4654
+ // Same-engagement restore owns the snapshot's token projection.
4655
+ // Adoption leaves it inert: the fresh engagement's player ledger (or
4656
+ // the absence of one) is authoritative, and its binding rules land
4657
+ // independently under DR-038 §4.
4658
+ if (kind === 'restore') {
4659
+ if (boundSession.playerSessions) {
4660
+ priorExternalPlayerTokens = snapshotRoleResumeTokens();
4661
+ externalStoreRestoreAttempted = true;
4662
+ }
4663
+ restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
4664
+ }
4665
+ nestedBridge.prepareRestore(suspendedCall);
4666
+ if (suspendedCall !== undefined) {
4667
+ playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
4668
+ if (hasGovernedPlayerStates) {
4669
+ const savedPrefix = kind === 'restore'
4670
+ ? suspendedCall.effectBoundaryPrefixSequence
4671
+ : undefined;
4672
+ // Pre-task-5 snapshots and retained-generation adoption have no
4673
+ // target-local causal prefix. Zero is conservative; an explicit
4674
+ // null preserves an observation failure as unknown/fail-closed.
4675
+ playbookCallEffectPrefixes.set(suspendedCall.callId, savedPrefix === null ? undefined : (savedPrefix ?? 0));
4676
+ }
4677
+ }
4678
+ suppressInspectionEmissions = true;
4679
+ actor = buildActor(runtimePorts, boundSnapshot.machine);
4680
+ if (adoptionContext !== undefined) {
4681
+ adoptionStartAttempted = true;
4682
+ await emitTrace('session.started', {
4683
+ state: boundSnapshot.state,
4684
+ ...stateIdentity(boundSnapshot.state.stateId),
4685
+ adoption: {
4686
+ sourceSessionId: adoptionContext.sourceSessionId,
4687
+ sourceGenerationId: adoptionContext.sourceGenerationId,
4688
+ ...(sourceSuspendedCall === undefined ||
4689
+ suspendedCall === undefined
4690
+ ? {}
4691
+ : {
4692
+ sourceCallId: sourceSuspendedCall.callId,
4693
+ sourceChildSessionId: sourceSuspendedCall.childSessionId,
4694
+ targetCallId: suspendedCall.callId,
4695
+ targetChildSessionId: suspendedCall.childSessionId,
4696
+ }),
4697
+ },
4698
+ }, suspendedCall === undefined
4699
+ ? {}
4700
+ : { callId: suspendedCall.callId });
4701
+ }
4702
+ actor.start();
4703
+ // A start-time actor error rides the startup emission channel
4704
+ // (latchRuntimeError); consume both latches here so the original
4705
+ // error outranks the derived status check below.
4706
+ {
4707
+ const startupFailure = emissionFailure;
4708
+ if (controlPlaneError !== undefined ||
4709
+ startupFailure !== undefined) {
4710
+ const startupError = controlPlaneError !== undefined
4711
+ ? controlPlaneError
4712
+ : startupFailure.error;
4713
+ controlPlaneError = undefined;
4714
+ if (emissionFailure === startupFailure) {
4715
+ emissionFailure = undefined;
4716
+ }
4717
+ throw startupError;
4718
+ }
4719
+ }
4720
+ const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
4721
+ ? {}
4722
+ : {
4723
+ pendingCall: {
4724
+ callId: suspendedCall.callId,
4725
+ playbookId: suspendedCall.playbookId,
4726
+ childSessionId: suspendedCall.childSessionId,
4727
+ },
4728
+ });
4729
+ if (restoredState.status !== 'active') {
4730
+ throw new Error(`createPlaybookRuntime.${kind}: restored actor status is ${restoredState.status}, expected active`);
4731
+ }
4732
+ if (stableJson(restoredState, 'restored runtime state') !==
4733
+ stableJson(boundSnapshot.state, 'runtime snapshot state')) {
4734
+ throw new Error(`createPlaybookRuntime.${kind}: restored actor state does not match snapshot state`);
4735
+ }
4736
+ const restoredFailedEffectAttempt = restoredState.stateId === 'failed' && kind === 'restore'
4737
+ ? boundSnapshot.failedEffectAttempt
4738
+ : undefined;
4739
+ failedEffectBoundaryPrefix =
4740
+ restoredFailedEffectAttempt?.boundaryPrefix;
4741
+ failedGovernedAttemptId =
4742
+ typeof restoredFailedEffectAttempt?.attemptId === 'string'
4743
+ ? restoredFailedEffectAttempt.attemptId
4744
+ : undefined;
4745
+ activeGovernedBoundarySeen = false;
4746
+ activeGovernedAttemptId = undefined;
4747
+ activeEffectLedgerPrefixSequence = undefined;
4748
+ failedGovernedAttemptUnknown =
4749
+ restoredState.stateId === 'failed' &&
4750
+ kind === 'restore' &&
4751
+ hasGovernedPlayerStates &&
4752
+ restoredFailedEffectAttempt === undefined;
4753
+ priorState = restoredState;
4754
+ await drainEmissions();
4755
+ suppressInspectionEmissions = false;
4756
+ acceptReconstructedGovernedDelivery(currentState());
4757
+ // Final fallible step: after this publication the authoritative
4758
+ // child has rejoined ordinary resume/abort ownership, so no later
4759
+ // snapshot-start validation may trigger failed-start rollback.
4760
+ nestedBridge.confirmRestore();
4761
+ })();
4762
+ try {
4763
+ await initTask;
4764
+ }
4765
+ catch (error) {
4766
+ let failure = error;
4767
+ if (externalStoreRestoreAttempted &&
4768
+ priorExternalPlayerTokens !== undefined) {
4769
+ try {
4770
+ boundSession.playerSessions.restore(priorExternalPlayerTokens);
4771
+ }
4772
+ catch (rollbackError) {
4773
+ failure = new AggregateError([error, rollbackError], `createPlaybookRuntime.${kind} and player continuation rollback failed`);
4774
+ }
4775
+ }
4776
+ await cleanupFailedStart(failure, {
4777
+ emitDisposal: adoptionStartAttempted,
4778
+ });
4779
+ throw failure;
4780
+ }
4781
+ finally {
4782
+ finishInitialization();
4783
+ if (initInFlight === initialization)
4784
+ initInFlight = undefined;
4785
+ }
4786
+ }
4787
+ function validateBoundQuestionProjection() {
4788
+ const expected = expectedBoundPendingQuestion;
4789
+ if (expected === undefined)
4790
+ return;
4791
+ const snapshot = actor?.getSnapshot();
4792
+ const state = snapshot === undefined
4793
+ ? undefined
4794
+ : normalizePlaybookSnapshot(snapshot);
4795
+ const context = (snapshot
4796
+ ?.context ?? {});
4797
+ const actual = state?.stateId === BOSS_REPLY_WAIT_STATE_ID
4798
+ ? pendingBossQuestionFromContext(context)
4799
+ : undefined;
4800
+ if (!isDeepStrictEqual(actual, expected)) {
4801
+ throw new Error(`${label} deferred FSM question does not equal its durable binding`);
4802
+ }
4803
+ const operation = currentBoundDeferredOperation(expected);
4804
+ if (operation === undefined) {
4805
+ throw new Error(`${label} deferred FSM question has no exact durable operation`);
4806
+ }
4807
+ expectedBoundPendingQuestion = undefined;
4808
+ }
4809
+ function settleDeferredInspectionBuffer(publish) {
4810
+ const buffered = deferredInspectionEmissions;
4811
+ deferredInspectionEmissions = [];
4812
+ deferInspectionEmissions = false;
4813
+ if (publish) {
4814
+ for (const emission of buffered)
4815
+ emission();
4816
+ }
4817
+ }
4818
+ async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine) {
4819
+ if (repositoryCapability === undefined) {
4820
+ throw new Error(`${label} deferred continuation requires repository.runDeferred`);
4821
+ }
4822
+ const effectBoundary = continuationBoundarySeed(operation, turnId);
4823
+ const continuation = {
4824
+ operationId: operation.operationId,
4825
+ effectBoundary,
4826
+ rawPlayerSettled: deferredValue(),
4827
+ delivery: deferredValue(),
4828
+ };
4829
+ activeDeferredContinuation = continuation;
4830
+ deferInspectionEmissions = true;
4831
+ let continuationStarted = false;
4832
+ let deliverySettled = false;
4833
+ deferredInspectionEmissions =
4834
+ classificationLine === undefined
4835
+ ? []
4836
+ : [() => void runtimePorts.emitStatus(classificationLine)];
4837
+ try {
4838
+ const result = await repositoryCapability.runDeferred({
4839
+ mode: 'continue',
4840
+ signal,
4841
+ operationId: operation.operationId,
4842
+ effectBoundary,
4843
+ operation: async ({ playerContinuation }) => {
4844
+ const selectedContinuation = retainedEffectSourceSessionId === undefined
4845
+ ? playerContinuation
4846
+ : selectPlayerResume(effectBoundary.roleId, resolvedPlayerId(effectBoundary.roleId));
4847
+ if (selectedContinuation !== false &&
4848
+ (typeof selectedContinuation !== 'string' ||
4849
+ selectedContinuation.trim().length === 0)) {
4850
+ throw new TypeError(`${label} bound deferred player continuation is invalid`);
4851
+ }
4852
+ // Retained adoption owns a fresh Captain-session player ledger;
4853
+ // no source token becomes target ownership. Same-engagement
4854
+ // continuation still uses the exact durable binding.
4855
+ continuation.playerContinuation = selectedContinuation;
4856
+ continuationStarted = true;
4857
+ actor.send(event);
4858
+ // The invoked player remains gated inside boundary.callPlayer.
4859
+ // Return to the host only after the raw player call settles so it
4860
+ // can capture and persist the receipt before any actor output or
4861
+ // error reaches XState.
4862
+ await continuation.rawPlayerSettled.promise;
4863
+ return null;
4864
+ },
4865
+ completeEffectBoundary: deferredContinuationCompletionEvidence,
4866
+ });
4867
+ effectLedgerMirror = assertPlaybookEffectLedger(result.effectLedger, `${label} deferred continuation effect ledger`);
4868
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
4869
+ syncDeferredReconciliationOverlay();
4870
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
4871
+ if (result.status !== 'continued') {
4872
+ if (continuationStarted) {
4873
+ deliverySettled = true;
4874
+ continuation.delivery.reject(markFsmResultFailure(new Error(`${label} deferred continuation remains unresolved`)));
4875
+ await waitForPlaybookQuiescence(actor, {
4876
+ pendingCalls: nestedBridge,
4877
+ });
4878
+ if (controlPlaneError !== undefined)
4879
+ throw controlPlaneError;
4880
+ }
4881
+ expectedBoundPendingQuestion = undefined;
4882
+ settleDeferredInspectionBuffer(false);
4883
+ return 'unresolved';
4884
+ }
4885
+ const completed = effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === effectBoundary.boundaryId);
4886
+ if (completed?.physicalReceipt === undefined ||
4887
+ !isDeepStrictEqual(completed.physicalReceipt, result.receipt)) {
4888
+ throw new TypeError(`${label} deferred continuation did not acknowledge its physical boundary`);
4889
+ }
4890
+ recordActiveGovernedAttempt(completed);
4891
+ let settlement = governedSettlementsByBoundaryId.get(effectBoundary.boundaryId);
4892
+ if (settlement === undefined &&
4893
+ continuation.result?.status === 'ok' &&
4894
+ !isEmptyFinalText(continuation.result.finalText)) {
4895
+ settlement = unresolvedGovernedSettlement('host omitted governed semantic settlement');
4896
+ }
4897
+ assertAcknowledgedGovernedEvidence(completed, settlement, effectLedgerMirror);
4898
+ governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
4899
+ governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
4900
+ if (settlement?.status === 'resolved' &&
4901
+ settlement.output.guard === 'needsBossReply' &&
4902
+ result.deferredStatus !== 'bound') {
4903
+ settlement = unresolvedGovernedSettlement('deferred question did not receive an eligible durable binding');
4904
+ }
4905
+ if (settlement?.status === 'unresolved') {
4906
+ unresolvedSemanticBoundaryIds.add(effectBoundary.boundaryId);
4907
+ }
4908
+ else if (settlement?.status === 'resolved') {
4909
+ unresolvedSemanticBoundaryIds.delete(effectBoundary.boundaryId);
4910
+ }
4911
+ if (result.logicalReceipt !== undefined) {
4912
+ const completedOperation = effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === operation.operationId);
4913
+ if (completedOperation?.logicalReceipt === undefined ||
4914
+ !isDeepStrictEqual(completedOperation.logicalReceipt, result.logicalReceipt)) {
4915
+ throw new TypeError(`${label} deferred continuation did not acknowledge its cumulative receipt`);
4916
+ }
4917
+ }
4918
+ if (settlement?.status === 'resolved' &&
4919
+ settlement.output.guard === 'needsBossReply') {
4920
+ if (result.deferredStatus !== 'bound' &&
4921
+ result.deferredStatus !== 'unresolved') {
4922
+ throw new TypeError(`${label} repeated deferred settlement omitted its durable binding status`);
4923
+ }
4924
+ }
4925
+ else if (settlement?.status === 'resolved' &&
4926
+ result.logicalReceipt === undefined) {
4927
+ throw new TypeError(`${label} final deferred settlement omitted its cumulative receipt`);
4928
+ }
4929
+ if (continuation.callError !== undefined) {
4930
+ deliverySettled = true;
4931
+ continuation.delivery.reject(continuation.callError);
4932
+ }
4933
+ else if (continuation.result === undefined) {
4934
+ deliverySettled = true;
4935
+ continuation.delivery.reject(markFsmResultFailure(new Error(`${label} deferred player returned no result`)));
4936
+ }
4937
+ else {
4938
+ if (settlement !== undefined) {
4939
+ governedPlayerSettlements.set(continuation.result, settlement);
4940
+ }
4941
+ // The bound answer authorizes exactly this one player call. Clear
4942
+ // its live delivery scope before XState can advance through a
4943
+ // nested call and invoke a later governed player in the same turn.
4944
+ activeDeferredContinuation = undefined;
4945
+ deliverySettled = true;
4946
+ continuation.delivery.resolve(continuation.result);
4947
+ }
4948
+ await waitForPlaybookQuiescence(actor, {
4949
+ pendingCalls: nestedBridge,
4950
+ });
4951
+ if (controlPlaneError !== undefined)
4952
+ throw controlPlaneError;
4953
+ if (!hasUnresolvedReconciliation()) {
4954
+ validateBoundQuestionProjection();
4955
+ settleDeferredInspectionBuffer(true);
4956
+ }
4957
+ else {
4958
+ expectedBoundPendingQuestion = undefined;
4959
+ settleDeferredInspectionBuffer(false);
2451
4960
  }
4961
+ return 'continued';
4962
+ }
4963
+ catch (error) {
4964
+ let failure = error;
4965
+ governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
4966
+ governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
4967
+ if (continuationStarted && !deliverySettled) {
4968
+ deliverySettled = true;
4969
+ continuation.delivery.reject(error);
4970
+ try {
4971
+ await waitForPlaybookQuiescence(actor, {
4972
+ pendingCalls: nestedBridge,
4973
+ });
4974
+ }
4975
+ catch (drainError) {
4976
+ failure = new AggregateError([error, drainError], `${label} deferred continuation rejection and actor drain both failed`);
4977
+ }
4978
+ }
4979
+ if (continuationStarted) {
4980
+ closeAfterIndeterminateDeferredSettlement(operation.operationId, failure);
4981
+ }
4982
+ settleDeferredInspectionBuffer(false);
4983
+ throw failure;
4984
+ }
4985
+ finally {
4986
+ activeDeferredContinuation = undefined;
2452
4987
  }
2453
- return Object.keys(projected).length === 0 ? undefined : projected;
2454
4988
  }
2455
- // PBRT-52: the view's Boss-facing state description — the meaning of the
2456
- // state the runtime is in, written by the artifact's own source, from the
2457
- // same descriptions its action labels are written from. A control view is
2458
- // the only grounding a controller host has for a status answer, and an
2459
- // internal state id is not Boss-appropriate text
2460
- // (CAPPLAY-5), so the runtime publishes the meaning
2461
- // rather than leaving the host to substitute the identifier for it. A
2462
- // state whose source declares no description publishes none: an id is
2463
- // never promoted into a description by default.
2464
- function stateDescriptionFor(state) {
2465
- const keys = [
2466
- ...(state.stateId === undefined ? [] : [state.stateId]),
2467
- ...(typeof state.value === 'string' ? [state.value] : []),
2468
- ...state.activeStateIds,
2469
- ];
2470
- for (const key of keys) {
2471
- const description = stateDescriptions.get(key);
2472
- if (description !== undefined)
2473
- return description;
4989
+ function isExactDeferredBossReply(snapshot, event, pending) {
4990
+ const candidate = event;
4991
+ return (candidate.type === 'BOSS_REPLY' &&
4992
+ (candidate.questionId === undefined ||
4993
+ candidate.questionId === pending.questionId) &&
4994
+ typeof candidate.answer === 'string' &&
4995
+ candidate.answer.trim().length > 0 &&
4996
+ snapshotCan(snapshot, event));
4997
+ }
4998
+ async function parkBoundDeferredOperation(operationId, signal) {
4999
+ if (repositoryCapability === undefined) {
5000
+ throw new Error(`${label} deferred parking requires repository.runDeferred`);
5001
+ }
5002
+ const parked = await repositoryCapability.runDeferred({
5003
+ mode: 'park',
5004
+ signal,
5005
+ operationId,
5006
+ });
5007
+ if (parked.status !== 'parked') {
5008
+ throw new TypeError(`${label} repository refused to park its deferred operation`);
5009
+ }
5010
+ effectLedgerMirror = assertPlaybookEffectLedger(parked.effectLedger, `${label} parked deferred effect ledger`);
5011
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
5012
+ syncDeferredReconciliationOverlay();
5013
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5014
+ if (deferredReconciliationOperationId !== operationId) {
5015
+ throw new TypeError(`${label} parked deferred operation is not structurally unresolved`);
2474
5016
  }
2475
- return undefined;
2476
5017
  }
2477
- function receiptTracePayload(receipt) {
2478
- return {
2479
- disposition: receipt.disposition,
2480
- ...(receipt.disposition === 'rejected'
2481
- ? { reason: receipt.reason }
2482
- : {}),
2483
- ...(receipt.disposition === 'failed' ? { error: receipt.error } : {}),
2484
- ...(receipt.disposition === 'executed' ? { run: receipt.run } : {}),
2485
- };
5018
+ async function restoreBoundDeferredOperation(operationId, signal) {
5019
+ if (repositoryCapability === undefined) {
5020
+ throw new Error(`${label} deferred restoration requires repository.runDeferred`);
5021
+ }
5022
+ const restored = await repositoryCapability.runDeferred({
5023
+ mode: 'restore',
5024
+ signal,
5025
+ operationId,
5026
+ });
5027
+ if (restored.status === 'parked') {
5028
+ throw new TypeError(`${label} repository returned a park result for deferred restoration`);
5029
+ }
5030
+ effectLedgerMirror = assertPlaybookEffectLedger(restored.effectLedger, `${label} restored deferred effect ledger`);
5031
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
5032
+ syncDeferredReconciliationOverlay();
5033
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5034
+ if (restored.status === 'restored') {
5035
+ if (deferredReconciliationOperationId !== undefined) {
5036
+ throw new TypeError(`${label} restored deferred operation remained unresolved`);
5037
+ }
5038
+ const snapshot = actor.getSnapshot();
5039
+ const state = normalizePlaybookSnapshot(snapshot);
5040
+ const context = (snapshot.context ??
5041
+ {});
5042
+ const pending = pendingBossQuestionForState(state, context);
5043
+ if (pending === undefined ||
5044
+ currentBoundDeferredOperation(pending)?.operationId !== operationId) {
5045
+ throw new TypeError(`${label} restored deferred wait does not equal its FSM question`);
5046
+ }
5047
+ }
5048
+ else if (deferredReconciliationOperationId !== operationId) {
5049
+ throw new TypeError(`${label} unresolved deferred restoration lost its operation identity`);
5050
+ }
5051
+ return restored.status;
2486
5052
  }
2487
5053
  const runtime = {
5054
+ ...(retainedGenerationMetadata === undefined
5055
+ ? {}
5056
+ : { retainedGenerationMetadata }),
2488
5057
  async init(nextSession) {
2489
5058
  if (initialized || disposed || disposalPromise !== undefined) {
2490
5059
  throw new Error('createPlaybookRuntime.init: already initialized');
@@ -2498,6 +5067,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2498
5067
  initInFlight = initialization;
2499
5068
  const initTask = (async () => {
2500
5069
  session = boundSession;
5070
+ syncDeferredReconciliationOverlay();
5071
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
2501
5072
  savedPorts = boundSession.ports;
2502
5073
  runtimePorts = createRuntimePorts(boundSession.ports);
2503
5074
  suppressInspectionEmissions = false;
@@ -2530,6 +5101,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2530
5101
  }
2531
5102
  if (activeSignal !== undefined)
2532
5103
  return undefined;
5104
+ if (deferredSettlementClosure !== undefined)
5105
+ return undefined;
2533
5106
  const pendingCall = nestedBridge.getPendingCall();
2534
5107
  const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
2535
5108
  if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
@@ -2553,6 +5126,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2553
5126
  suspendedCall = {
2554
5127
  ...bridgeSuspendedCall,
2555
5128
  ...(turnId === undefined ? {} : { turnId }),
5129
+ ...(hasGovernedPlayerStates
5130
+ ? {
5131
+ effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
5132
+ }
5133
+ : {}),
2556
5134
  };
2557
5135
  }
2558
5136
  const state = currentState();
@@ -2561,9 +5139,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
2561
5139
  const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
2562
5140
  const context = actor.getSnapshot()
2563
5141
  .context;
2564
- const pending = pendingBossQuestionFromContext(context ?? {});
5142
+ effectLedgerMirror = currentEffectLedger();
5143
+ refreshRetainedEffectReconciliation(effectLedgerMirror);
5144
+ syncDeferredReconciliationOverlay();
5145
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
5146
+ const pending = !hasUnresolvedReconciliation()
5147
+ ? pendingBossQuestionForState(state, context ?? {})
5148
+ : undefined;
5149
+ const failedEffectAttempt = hasGovernedPlayerStates &&
5150
+ state.stateId === 'failed' &&
5151
+ failedAttemptMatchesCurrentLedger(effectLedgerMirror)
5152
+ ? {
5153
+ boundaryPrefix: failedEffectBoundaryPrefix,
5154
+ attemptId: failedGovernedAttemptId ?? null,
5155
+ }
5156
+ : undefined;
2565
5157
  return {
2566
- schemaVersion: 3,
5158
+ schemaVersion: 4,
2567
5159
  playbookId: session.playbookId,
2568
5160
  machine: machineSnapshot,
2569
5161
  roleResumeTokens: snapshotRoleResumeTokens(),
@@ -2588,6 +5180,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
2588
5180
  sourceItem: pending.sourceItem,
2589
5181
  },
2590
5182
  ],
5183
+ effectLedger: effectLedgerMirror,
5184
+ ...(retainedEffectSourceSessionId === undefined
5185
+ ? {}
5186
+ : { retainedEffectSourceSessionId }),
5187
+ ...(retainedEffectReconciliation === undefined
5188
+ ? {}
5189
+ : { retainedEffectReconciliation }),
5190
+ ...(failedEffectAttempt === undefined
5191
+ ? {}
5192
+ : { failedEffectAttempt }),
2591
5193
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
2592
5194
  };
2593
5195
  },
@@ -2597,99 +5199,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
2597
5199
  // the session already started; the next public boundary continues
2598
5200
  // the contiguous trace sequence.
2599
5201
  async restore(nextSession, snapshot) {
2600
- if (initialized || disposed || disposalPromise !== undefined) {
2601
- throw new Error('createPlaybookRuntime.restore: already initialized');
2602
- }
2603
- const boundSession = bindSession(nextSession);
2604
- const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
2605
- if (declaredActors.has('captain') &&
2606
- boundSnapshot.sequences.captainCall === undefined) {
2607
- throw new TypeError('runtime snapshot sequences.captainCall is required for a direct-Captain artifact');
2608
- }
2609
- const suspendedCall = boundSnapshot.suspendedCall;
2610
- let priorExternalPlayerTokens;
2611
- let externalStoreRestoreAttempted = false;
2612
- initialized = true;
2613
- let finishInitialization;
2614
- const initialization = new Promise((resolve) => {
2615
- finishInitialization = resolve;
2616
- });
2617
- initInFlight = initialization;
2618
- const initTask = (async () => {
2619
- session = boundSession;
2620
- savedPorts = boundSession.ports;
2621
- runtimePorts = createRuntimePorts(boundSession.ports);
2622
- traceSequence = boundSnapshot.sequences.trace;
2623
- turnSequence = boundSnapshot.sequences.turn;
2624
- judgeCallSequence = boundSnapshot.sequences.judgeCall;
2625
- playerCallSequence = boundSnapshot.sequences.playerCall;
2626
- playbookCallSequence = boundSnapshot.sequences.playbookCall;
2627
- captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
2628
- // The runtime snapshot carries no apply counter (PBRT-50); every
2629
- // apply boundary consumed trace numbers, so the persisted trace
2630
- // counter is a collision-safe id floor here too, keeping
2631
- // `apply-<n>` call ids unique across restore.
2632
- applyCallSequence = boundSnapshot.sequences.trace;
2633
- if (boundSession.playerSessions) {
2634
- priorExternalPlayerTokens = snapshotRoleResumeTokens();
2635
- externalStoreRestoreAttempted = true;
2636
- }
2637
- restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
2638
- nestedBridge.prepareRestore(suspendedCall);
2639
- if (suspendedCall !== undefined) {
2640
- playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
2641
- }
2642
- suppressInspectionEmissions = true;
2643
- actor = buildActor(runtimePorts, boundSnapshot.machine);
2644
- actor.start();
2645
- if (controlPlaneError !== undefined)
2646
- throw controlPlaneError;
2647
- const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
2648
- ? {}
2649
- : {
2650
- pendingCall: {
2651
- callId: suspendedCall.callId,
2652
- playbookId: suspendedCall.playbookId,
2653
- childSessionId: suspendedCall.childSessionId,
2654
- },
2655
- });
2656
- if (restoredState.status !== 'active') {
2657
- throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
2658
- }
2659
- if (stableJson(restoredState, 'restored runtime state') !==
2660
- stableJson(boundSnapshot.state, 'runtime snapshot state')) {
2661
- throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
2662
- }
2663
- priorState = restoredState;
2664
- await drainEmissions();
2665
- suppressInspectionEmissions = false;
2666
- // Final fallible step: after this publication the authoritative
2667
- // child has rejoined ordinary resume/abort ownership, so no later
2668
- // restore validation may trigger failed-start rollback.
2669
- nestedBridge.confirmRestore();
2670
- })();
2671
- try {
2672
- await initTask;
2673
- }
2674
- catch (error) {
2675
- let failure = error;
2676
- if (externalStoreRestoreAttempted &&
2677
- priorExternalPlayerTokens !== undefined) {
2678
- try {
2679
- boundSession.playerSessions.restore(priorExternalPlayerTokens);
2680
- }
2681
- catch (rollbackError) {
2682
- failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
2683
- }
2684
- }
2685
- await cleanupFailedStart(failure, { emitDisposal: false });
2686
- throw failure;
2687
- }
2688
- finally {
2689
- finishInitialization();
2690
- if (initInFlight === initialization)
2691
- initInFlight = undefined;
2692
- }
5202
+ await rehydrateSnapshot('restore', nextSession, snapshot);
5203
+ },
5204
+ // DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
5205
+ // engagement identity and counter lineage, exposed separately so
5206
+ // capability-less bespoke runtimes can omit it. Runtime-visible
5207
+ // preflight mismatches reject before effects; after preflight the new
5208
+ // session.started boundary owns failed-start cleanup just like init.
5209
+ async adopt(nextSession, snapshot, context) {
5210
+ await rehydrateSnapshot('adopt', nextSession, snapshot, context);
2693
5211
  },
2694
5212
  // DR-029 / PBRT-52: side-effect-free control view over the live
2695
5213
  // snapshot, valid at parked quiescence outside an active boundary.
@@ -2705,14 +5223,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
2705
5223
  if (activeSignal !== undefined) {
2706
5224
  throw new Error('createPlaybookRuntime.describe: another runtime turn is active');
2707
5225
  }
5226
+ assertDeferredSettlementOpen('describe');
5227
+ refreshRetainedEffectFenceFromHost();
2708
5228
  const snapshot = actor.getSnapshot();
2709
5229
  const state = currentState();
2710
5230
  const context = (snapshot.context ??
2711
5231
  {});
2712
- const pending = pendingBossQuestionFromContext(context);
5232
+ const pending = !hasUnresolvedReconciliation()
5233
+ ? pendingBossQuestionForState(state, context)
5234
+ : undefined;
2713
5235
  const lastError = normalizeErrorFull(context.lastError);
2714
5236
  const projectedContext = projectControlContext(context);
2715
- const stateDescription = stateDescriptionFor(state);
5237
+ const stateDescription = !hasUnresolvedReconciliation()
5238
+ ? stateDescriptionFor(state)
5239
+ : undefined;
2716
5240
  return deepFreeze({
2717
5241
  state,
2718
5242
  ...(stateDescription === undefined ? {} : { stateDescription }),
@@ -2745,6 +5269,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2745
5269
  if (input === null || typeof input !== 'object') {
2746
5270
  throw new TypeError('createPlaybookRuntime.apply: input must be an object');
2747
5271
  }
5272
+ assertDeferredSettlementOpen('apply');
2748
5273
  const { actionId, key, signal } = input;
2749
5274
  if (typeof actionId !== 'string' || actionId.length === 0) {
2750
5275
  throw new TypeError('createPlaybookRuntime.apply: actionId must be a non-empty string');
@@ -2772,11 +5297,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
2772
5297
  // An abort before acceptance ends the call with no receipt
2773
5298
  // recorded, like every other pre-acceptance failure.
2774
5299
  signal.throwIfAborted();
5300
+ refreshRetainedEffectFenceFromHost();
2775
5301
  const turnId = ++turnSequence;
2776
5302
  const callId = `apply-${++applyCallSequence}`;
2777
5303
  const position = { turnId, callId };
2778
5304
  activeTurnId = turnId;
5305
+ beginAutomaticReplayBoundary();
2779
5306
  activeSignal = signal;
5307
+ activeAborts = abortReasonClassifier(signal);
5308
+ activeAbortEmission = undefined;
2780
5309
  controlPlaneError = undefined;
2781
5310
  // Every receipt variant is normalized and frozen where it is built,
2782
5311
  // inside the guarded region, so the recording step below cannot
@@ -2822,9 +5351,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
2822
5351
  // final for their key. Past publication such a failure is therefore
2823
5352
  // re-latched onto the emission channel, surfacing from the next
2824
5353
  // public boundary's drain, and `apply` still does not throw past
2825
- // acceptance (PBRT-52).
5354
+ // acceptance (PBRT-52). A delivery rejection causally identical to
5355
+ // this call's own abort reason evidences the cancellation and is
5356
+ // dropped — never carried to a later unrelated boundary
5357
+ // (slc/link.md §Abort).
2826
5358
  const latchDeliveryFailure = (error) => {
2827
- emissionFailure ??= error;
5359
+ if (isAbortFailure(error, signal))
5360
+ return;
5361
+ emissionFailure ??= { error };
2828
5362
  };
2829
5363
  try {
2830
5364
  try {
@@ -2844,7 +5378,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2844
5378
  key,
2845
5379
  ...receiptTracePayload({ disposition: 'rejected', reason }),
2846
5380
  });
2847
- await emitCallStarted('apply.started', 'apply.finished', identity, position, preAcceptanceFinish('apply.started trace sink rejected'));
5381
+ await emitCallStarted('apply.started', 'apply.finished', identity, position, signal, preAcceptanceFinish('apply.started trace sink rejected'));
2848
5382
  // An abort may land while the awaited started emission drains
2849
5383
  // (e.g. fired from the trace sink itself); the action must
2850
5384
  // never execute after abort. Settle the already-started pair
@@ -2880,13 +5414,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
2880
5414
  // the key, so the action can never execute twice.
2881
5415
  accepted = true;
2882
5416
  try {
2883
- actor.send(candidate.event);
2884
- await waitForPlaybookQuiescence(actor, {
2885
- pendingCalls: nestedBridge,
2886
- });
5417
+ let run;
5418
+ if (candidate.unresolvedEffectAction === 'abandon') {
5419
+ signal.throwIfAborted();
5420
+ run = runResultFor('unresolved-effect');
5421
+ }
5422
+ else if (candidate.unresolvedEffectAction === 'reconcile') {
5423
+ if (candidate.deferredRestoreOperationId !== undefined) {
5424
+ await restoreBoundDeferredOperation(candidate.deferredRestoreOperationId, signal);
5425
+ }
5426
+ else {
5427
+ // Receipt reconstruction itself belongs to the host. The
5428
+ // runtime may only re-read that authoritative mirror; it
5429
+ // never replays a player to manufacture missing evidence.
5430
+ refreshRetainedEffectFenceFromHost();
5431
+ }
5432
+ signal.throwIfAborted();
5433
+ run = runResultFor(hasUnresolvedReconciliation()
5434
+ ? 'no-action'
5435
+ : 'quiescent');
5436
+ }
5437
+ else {
5438
+ actor.send(candidate.event);
5439
+ await waitForPlaybookQuiescence(actor, {
5440
+ pendingCalls: nestedBridge,
5441
+ });
5442
+ run = runResultFor(settledOutcome(signal));
5443
+ }
2887
5444
  if (controlPlaneError !== undefined)
2888
5445
  throw controlPlaneError;
2889
- const run = runResultFor(settledOutcome(signal));
2890
5446
  receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
2891
5447
  ? {
2892
5448
  disposition: 'failed',
@@ -2923,6 +5479,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
2923
5479
  catch (error) {
2924
5480
  settlementError = error;
2925
5481
  }
5482
+ // Exact cancellation is not a control-plane latch, but after apply
5483
+ // acceptance and before publication it is still settlement evidence
5484
+ // and therefore folds into the owed failed receipt (DR-036 §4).
5485
+ settlementError ??= activeAbortEmission;
2926
5486
  // Fold before the finish emission, the last point at which the
2927
5487
  // traced disposition and the returned one can still be made the
2928
5488
  // same value.
@@ -2964,7 +5524,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2964
5524
  // wedge every later public boundary behind "another runtime turn
2965
5525
  // is active".
2966
5526
  activeSignal = undefined;
5527
+ activeAborts = undefined;
5528
+ activeAbortEmission = undefined;
2967
5529
  activeTurnId = undefined;
5530
+ activeGovernedBoundarySeen = false;
5531
+ activeGovernedAttemptId = undefined;
5532
+ activeEffectLedgerPrefixSequence = undefined;
2968
5533
  controlPlaneError = undefined;
2969
5534
  }
2970
5535
  // Past acceptance every settlement failure has been folded into the
@@ -2986,6 +5551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2986
5551
  }
2987
5552
  return receipt;
2988
5553
  },
5554
+ unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
2989
5555
  async handleBossInput({ text, signal, }) {
2990
5556
  if (!actor || !savedPorts) {
2991
5557
  throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
@@ -2996,122 +5562,247 @@ export function createXStatePlaybookRuntime(machine, spec) {
2996
5562
  if (activeSignal !== undefined) {
2997
5563
  throw new Error('createPlaybookRuntime.handleBossInput: another runtime turn is active');
2998
5564
  }
5565
+ assertDeferredSettlementOpen('handleBossInput');
5566
+ refreshRetainedEffectFenceFromHost();
2999
5567
  const turnId = ++turnSequence;
3000
5568
  activeTurnId = turnId;
5569
+ beginAutomaticReplayBoundary();
3001
5570
  activeSignal = signal;
5571
+ activeAborts = abortReasonClassifier(signal);
5572
+ activeAbortEmission = undefined;
3002
5573
  controlPlaneError = undefined;
3003
5574
  let result;
3004
5575
  let operationError;
5576
+ // The boundary sentinel releases on every exit: a settlement defect
5577
+ // past the drain — a snapshot normalization throw inside
5578
+ // `runResultFor` included — must never wedge every later public
5579
+ // boundary and `dispose` itself behind "another runtime turn is
5580
+ // active". Mirrors the apply boundary's finally.
3005
5581
  try {
3006
- await emitTrace('boss.input.received', { text }, { turnId });
3007
- // 1. Map the Boss text to an FSM event: deterministic exact entry
3008
- // where applicable (slc/link.md §Boss-event mapping), judge
3009
- // classification otherwise.
3010
- let event;
3011
- const trimmed = text.trim();
3012
- if (trimmed !== '') {
3013
- const snapshot = actor.getSnapshot();
3014
- const terminal = snapshot.status === 'done';
3015
- const stateId = normalizePlaybookSnapshot(snapshot).stateId;
3016
- if (spec.entryEvent !== undefined &&
3017
- (stateId === 'ready' || terminal)) {
3018
- event = {
3019
- type: spec.entryEvent.type,
3020
- [spec.entryEvent.textField]: text,
3021
- };
5582
+ try {
5583
+ await emitTrace('boss.input.received', { text }, { turnId });
5584
+ // Record the attempted input, then refuse a boundary that entered
5585
+ // aborted before deterministic mapping or the classifier can
5586
+ // perform host-visible work (DR-036 §5).
5587
+ signal.throwIfAborted();
5588
+ // 1. Map the Boss text to an FSM event: deterministic exact entry
5589
+ // where applicable (slc/link.md §Boss-event mapping), judge
5590
+ // classification otherwise.
5591
+ let event;
5592
+ let classifiedSnapshot;
5593
+ let deferredPending;
5594
+ let deferredOperation;
5595
+ let pendingRequiresDeferredBinding = false;
5596
+ const trimmed = text.trim();
5597
+ if (trimmed !== '') {
5598
+ const snapshot = actor.getSnapshot();
5599
+ classifiedSnapshot = snapshot;
5600
+ const terminal = snapshot.status === 'done';
5601
+ const stateId = normalizePlaybookSnapshot(snapshot).stateId;
5602
+ const snapshotContext = (snapshot
5603
+ .context ?? {});
5604
+ deferredPending = pendingBossQuestionForState(normalizePlaybookSnapshot(snapshot), snapshotContext);
5605
+ pendingRequiresDeferredBinding =
5606
+ deferredPending !== undefined &&
5607
+ outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred';
5608
+ deferredOperation =
5609
+ !pendingRequiresDeferredBinding
5610
+ ? undefined
5611
+ : currentBoundDeferredOperation(deferredPending);
5612
+ // PBRT-1 / slc/link.md §Boss-event mapping: the idle entry, the
5613
+ // recoverable failure state, and the reconstructed terminal all
5614
+ // accept exactly one ordinary textual entry event, so delivered
5615
+ // text enters deterministically — no judge call to spend and no
5616
+ // classifier whim to settle a restart as no action. Every other
5617
+ // parked state — a reply wait or an authored mid-workflow
5618
+ // checkpoint — classifies under its own Boss-event contracts.
5619
+ if (hasUnresolvedReconciliation()) {
5620
+ event = undefined;
5621
+ }
5622
+ else if (stateId === 'failed' &&
5623
+ !failedAttemptAllowsReplay()) {
5624
+ event = undefined;
5625
+ }
5626
+ else if (spec.entryEvent !== undefined &&
5627
+ (stateId === 'ready' || stateId === 'failed' || terminal)) {
5628
+ event = {
5629
+ type: spec.entryEvent.type,
5630
+ [spec.entryEvent.textField]: text,
5631
+ };
5632
+ }
5633
+ else {
5634
+ event = await classifyBossText(text, runtimePorts, signal, snapshot, boundary, boundOptions);
5635
+ }
5636
+ signal.throwIfAborted();
3022
5637
  }
3023
- else {
3024
- event = await classifyBossText(text, runtimePorts, signal, snapshot, boundary, boundOptions);
5638
+ if (event !== undefined &&
5639
+ deferredPending !== undefined &&
5640
+ pendingRequiresDeferredBinding &&
5641
+ deferredOperation === undefined &&
5642
+ hasGovernedPlayerStates &&
5643
+ deferredReconciliationOperationId === undefined) {
5644
+ throw new Error(`${label} pending governed Boss question has no durable logical operation`);
3025
5645
  }
3026
- signal.throwIfAborted();
3027
- }
3028
- // Empty input, no-action classifier output, or invalid classifier
3029
- // output nothing to send.
3030
- if (event === undefined) {
3031
- result = runResultFor('no-action');
3032
- }
3033
- else {
3034
- // 2. Optional Captain-pane classification line: the bare FSM
3035
- // event type, emitted before the FSM advances.
3036
- const statusLine = classificationStatus(event);
3037
- if (statusLine !== undefined) {
3038
- await runtimePorts.emitStatus(statusLine);
5646
+ let handledDeferred = false;
5647
+ if (event !== undefined &&
5648
+ classifiedSnapshot !== undefined &&
5649
+ deferredPending !== undefined &&
5650
+ deferredOperation !== undefined) {
5651
+ if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
5652
+ const statusLine = classificationStatus(event);
5653
+ const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine);
5654
+ if (continuation === 'continued') {
5655
+ try {
5656
+ lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
5657
+ }
5658
+ catch {
5659
+ lastBossEvent = undefined;
5660
+ }
5661
+ if (controlPlaneError !== undefined) {
5662
+ throw controlPlaneError;
5663
+ }
5664
+ result = runResultFor(!hasUnresolvedReconciliation()
5665
+ ? settledOutcome(signal)
5666
+ : 'no-action');
5667
+ }
5668
+ else {
5669
+ lastBossEvent = undefined;
5670
+ result = runResultFor('no-action');
5671
+ }
5672
+ handledDeferred = true;
5673
+ }
5674
+ else if (event.type === 'BOSS_REPLY') {
5675
+ // A malformed, empty, or mismatched answer does not consume
5676
+ // the durable wait and starts no repository or player work.
5677
+ event = undefined;
5678
+ }
5679
+ else {
5680
+ await parkBoundDeferredOperation(deferredOperation.operationId, signal);
5681
+ lastBossEvent = undefined;
5682
+ result = runResultFor('no-action');
5683
+ handledDeferred = true;
5684
+ }
3039
5685
  }
3040
- signal.throwIfAborted();
3041
- // 3. A final actor cannot accept new events; reconstruct only
3042
- // after classification produced a real event.
3043
- if (actor.getSnapshot().status === 'done') {
3044
- stopActor();
3045
- actor = buildActor(runtimePorts);
3046
- // The replacement actor's snapshots are real state entries.
3047
- suppressInspectionEmissions = false;
3048
- actor.start();
5686
+ // Empty input, no-action classifier output, or invalid classifier
5687
+ // output nothing to send.
5688
+ if (handledDeferred) {
5689
+ // The deferred host transaction already decided whether the
5690
+ // authored continuation ran; never send its event a second time.
5691
+ }
5692
+ else if (event === undefined) {
5693
+ result = runResultFor('no-action');
5694
+ }
5695
+ else {
5696
+ // 2. Optional Captain-pane classification line: the bare FSM
5697
+ // event type, emitted before the FSM advances.
5698
+ const statusLine = classificationStatus(event);
5699
+ if (statusLine !== undefined) {
5700
+ await runtimePorts.emitStatus(statusLine);
5701
+ }
5702
+ signal.throwIfAborted();
5703
+ // 3. A final actor cannot accept new events; reconstruct only
5704
+ // after classification produced a real event.
5705
+ if (actor.getSnapshot().status === 'done') {
5706
+ stopActor();
5707
+ actor = buildActor(runtimePorts);
5708
+ // The replacement actor's snapshots are real state entries.
5709
+ suppressInspectionEmissions = false;
5710
+ actor.start();
5711
+ }
5712
+ // DR-029: keep the classified event with its recorded payload
5713
+ // as the retry-replay source. Recording is sanitizing, not
5714
+ // load-bearing: an override classifier's non-JSON-safe event is
5715
+ // simply not recorded, and the turn proceeds unchanged.
5716
+ try {
5717
+ lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
5718
+ }
5719
+ catch {
5720
+ lastBossEvent = undefined;
5721
+ }
5722
+ actor.send(event);
5723
+ await waitForPlaybookQuiescence(actor, {
5724
+ pendingCalls: nestedBridge,
5725
+ });
5726
+ if (controlPlaneError !== undefined)
5727
+ throw controlPlaneError;
5728
+ result = runResultFor(settledOutcome(signal));
3049
5729
  }
3050
- // DR-029: keep the classified event with its recorded payload
3051
- // as the retry-replay source. Recording is sanitizing, not
3052
- // load-bearing: an override classifier's non-JSON-safe event is
3053
- // simply not recorded, and the turn proceeds unchanged.
5730
+ }
5731
+ catch (error) {
5732
+ operationError = error;
5733
+ }
5734
+ let drainError;
5735
+ try {
5736
+ await drainEmissions();
5737
+ }
5738
+ catch (error) {
5739
+ drainError = error;
5740
+ }
5741
+ const latchedControlError = controlPlaneError;
5742
+ // A drain rejection that is the exact abort reason evidences the
5743
+ // cancellation, not a control-plane failure (slc/link.md §Abort).
5744
+ const drainAbort = drainError !== undefined && isAbortFailure(drainError, signal);
5745
+ const effectiveDrainError = drainAbort ? undefined : drainError;
5746
+ const primaryError = latchedControlError ?? effectiveDrainError ?? operationError;
5747
+ const abortError = latchedControlError === undefined &&
5748
+ effectiveDrainError === undefined &&
5749
+ ((operationError !== undefined &&
5750
+ isAbortFailure(operationError, signal)) ||
5751
+ (drainAbort && operationError === undefined));
5752
+ // A deferred continuation whose actor advanced before the host's
5753
+ // completion write became authoritative has no safe public FSM
5754
+ // settlement. The durable uncertain record is the only recovery
5755
+ // source, so do not project the actor's advanced snapshot into a
5756
+ // `boss.input.settled` event.
5757
+ const settlementResult = deferredSettlementClosure !== undefined
5758
+ ? undefined
5759
+ : primaryError === undefined
5760
+ ? (result ?? runResultFor('no-action'))
5761
+ : runResultFor(abortError ? 'aborted' : 'failed', primaryError);
5762
+ let settlementEmissionError;
5763
+ if (settlementResult !== undefined) {
3054
5764
  try {
3055
- lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
5765
+ await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
3056
5766
  }
3057
- catch {
3058
- lastBossEvent = undefined;
5767
+ catch (error) {
5768
+ settlementEmissionError = error;
3059
5769
  }
3060
- actor.send(event);
3061
- await waitForPlaybookQuiescence(actor, {
3062
- pendingCalls: nestedBridge,
3063
- });
3064
- if (controlPlaneError !== undefined)
3065
- throw controlPlaneError;
3066
- result = runResultFor(settledOutcome(signal));
3067
5770
  }
5771
+ try {
5772
+ await drainEmissions();
5773
+ }
5774
+ catch (error) {
5775
+ settlementEmissionError ??= error;
5776
+ }
5777
+ if (settlementEmissionError !== undefined &&
5778
+ isAbortFailure(settlementEmissionError, signal)) {
5779
+ settlementEmissionError = undefined;
5780
+ }
5781
+ const failure = controlPlaneError ??
5782
+ latchedControlError ??
5783
+ effectiveDrainError ??
5784
+ (abortError
5785
+ ? (settlementEmissionError ?? operationError)
5786
+ : (operationError ?? settlementEmissionError));
5787
+ if (failure !== undefined &&
5788
+ !(abortError && settlementEmissionError === undefined)) {
5789
+ throw failure;
5790
+ }
5791
+ if (settlementResult === undefined) {
5792
+ throw deferredSettlementClosure;
5793
+ }
5794
+ return settlementResult;
3068
5795
  }
3069
- catch (error) {
3070
- operationError = error;
3071
- }
3072
- let drainError;
3073
- try {
3074
- await drainEmissions();
3075
- }
3076
- catch (error) {
3077
- drainError = error;
3078
- }
3079
- const latchedControlError = controlPlaneError;
3080
- const primaryError = latchedControlError ?? drainError ?? operationError;
3081
- const abortError = latchedControlError === undefined &&
3082
- drainError === undefined &&
3083
- operationError !== undefined &&
3084
- isAbortFailure(operationError, signal);
3085
- const settlementResult = primaryError === undefined
3086
- ? (result ?? runResultFor('no-action'))
3087
- : runResultFor(abortError ? 'aborted' : 'failed', primaryError);
3088
- let settlementEmissionError;
3089
- try {
3090
- await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
3091
- }
3092
- catch (error) {
3093
- settlementEmissionError = error;
3094
- }
3095
- try {
3096
- await drainEmissions();
3097
- }
3098
- catch (error) {
3099
- settlementEmissionError ??= error;
3100
- }
3101
- const failure = controlPlaneError ??
3102
- latchedControlError ??
3103
- drainError ??
3104
- (abortError
3105
- ? (settlementEmissionError ?? operationError)
3106
- : (operationError ?? settlementEmissionError));
3107
- activeSignal = undefined;
3108
- activeTurnId = undefined;
3109
- controlPlaneError = undefined;
3110
- if (failure !== undefined &&
3111
- !(abortError && settlementEmissionError === undefined)) {
3112
- throw failure;
5796
+ finally {
5797
+ activeSignal = undefined;
5798
+ activeAborts = undefined;
5799
+ activeAbortEmission = undefined;
5800
+ activeTurnId = undefined;
5801
+ activeGovernedBoundarySeen = false;
5802
+ activeGovernedAttemptId = undefined;
5803
+ activeEffectLedgerPrefixSequence = undefined;
5804
+ controlPlaneError = undefined;
3113
5805
  }
3114
- return settlementResult;
3115
5806
  },
3116
5807
  async resumePlaybookCall(input) {
3117
5808
  if (!actor || !savedPorts) {
@@ -3123,43 +5814,99 @@ export function createXStatePlaybookRuntime(machine, spec) {
3123
5814
  if (activeSignal !== undefined) {
3124
5815
  throw new Error('createPlaybookRuntime.resumePlaybookCall: another runtime turn is active');
3125
5816
  }
5817
+ refreshRetainedEffectFenceFromHost();
5818
+ if (hasUnresolvedReconciliation()) {
5819
+ return runResultFor('no-action');
5820
+ }
3126
5821
  activeTurnId = playbookCallTurnIds.get(input.callId);
5822
+ bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
5823
+ ? playbookCallEffectPrefixes.get(input.callId)
5824
+ : hasGovernedPlayerStates
5825
+ ? 0
5826
+ : undefined);
3127
5827
  activeSignal = input.signal;
5828
+ activeAborts = abortReasonClassifier(input.signal);
5829
+ activeAbortEmission = undefined;
3128
5830
  controlPlaneError = undefined;
3129
- let result;
3130
- let operationError;
3131
- try {
3132
- await nestedBridge.resume(input);
3133
- }
3134
- catch (error) {
3135
- operationError = error;
3136
- }
3137
- try {
3138
- await waitForPlaybookQuiescence(actor, {
3139
- pendingCalls: nestedBridge,
3140
- });
3141
- result = runResultFor(settledOutcome(input.signal));
3142
- }
3143
- catch (error) {
3144
- operationError ??= error;
3145
- }
3146
- let drainError;
5831
+ // The boundary sentinel releases on every exit, mirroring
5832
+ // `handleBossInput` and the apply boundary.
3147
5833
  try {
3148
- await drainEmissions();
3149
- }
3150
- catch (error) {
3151
- drainError = error;
5834
+ let result;
5835
+ let operationError;
5836
+ try {
5837
+ await nestedBridge.resume(input);
5838
+ }
5839
+ catch (error) {
5840
+ operationError = error;
5841
+ }
5842
+ try {
5843
+ await waitForPlaybookQuiescence(actor, {
5844
+ pendingCalls: nestedBridge,
5845
+ });
5846
+ result = runResultFor(settledOutcome(input.signal));
5847
+ }
5848
+ catch (error) {
5849
+ operationError ??= error;
5850
+ }
5851
+ // A resume refused because its signal was already aborted
5852
+ // delivers nothing: the pending call survives for a later
5853
+ // resume, and the boundary settles `aborted` rather than
5854
+ // advertising `suspended` (slc/link.md §Nested playbook bridge).
5855
+ if (operationError !== undefined &&
5856
+ isAbortFailure(operationError, input.signal) &&
5857
+ nestedBridge.getPendingCall()?.callId === input.callId) {
5858
+ result = {
5859
+ outcome: 'aborted',
5860
+ state: currentState(),
5861
+ error: normalizeError(input.signal.reason),
5862
+ };
5863
+ }
5864
+ let drainError;
5865
+ try {
5866
+ await drainEmissions();
5867
+ }
5868
+ catch (error) {
5869
+ drainError = error;
5870
+ }
5871
+ const aborts = activeAborts ?? abortReasonClassifier(input.signal);
5872
+ // A control-plane latch has already classified its failure as
5873
+ // distinct under the owning operation. Never reinterpret it
5874
+ // against this later resume signal (DR-036 decision 2).
5875
+ const controlFailure = controlPlaneError;
5876
+ const drainAbort = controlFailure === undefined &&
5877
+ drainError !== undefined &&
5878
+ aborts.isAbortReason(drainError);
5879
+ const operationAbort = controlFailure === undefined &&
5880
+ operationError !== undefined &&
5881
+ aborts.isAbortReason(operationError);
5882
+ const abortEvidence = activeAbortEmission ??
5883
+ (drainAbort ? drainError : undefined) ??
5884
+ (operationAbort ? operationError : undefined);
5885
+ const failure = controlFailure ??
5886
+ (drainAbort ? undefined : drainError) ??
5887
+ (operationAbort ? undefined : operationError);
5888
+ if (failure !== undefined)
5889
+ throw failure;
5890
+ if (abortEvidence !== undefined &&
5891
+ result?.outcome !== 'terminal' &&
5892
+ result?.outcome !== 'suspended') {
5893
+ result = runResultFor('aborted', abortEvidence);
5894
+ }
5895
+ if (result === undefined) {
5896
+ throw new Error('playbook resume produced no runtime result');
5897
+ }
5898
+ return result;
3152
5899
  }
3153
- const failure = controlPlaneError ?? drainError ?? operationError;
3154
- activeSignal = undefined;
3155
- activeTurnId = undefined;
3156
- controlPlaneError = undefined;
3157
- if (failure !== undefined)
3158
- throw failure;
3159
- if (result === undefined) {
3160
- throw new Error('playbook resume produced no runtime result');
5900
+ finally {
5901
+ activeSignal = undefined;
5902
+ activeAborts = undefined;
5903
+ activeAbortEmission = undefined;
5904
+ activeTurnId = undefined;
5905
+ activeGovernedBoundarySeen = false;
5906
+ activeGovernedAttemptId = undefined;
5907
+ activeEffectLedgerPrefixSequence = undefined;
5908
+ controlPlaneError = undefined;
3161
5909
  }
3162
- return result;
3163
5910
  },
3164
5911
  dispose() {
3165
5912
  if (disposalPromise !== undefined)
@@ -3222,16 +5969,42 @@ export function createXStatePlaybookRuntime(machine, spec) {
3222
5969
  }
3223
5970
  activePlayerKeys.clear();
3224
5971
  playbookCallTurnIds.clear();
5972
+ playbookCallEffectPrefixes.clear();
3225
5973
  activeEmissionCalls.clear();
3226
5974
  emissionQueue.clear();
3227
5975
  judgeQueue.clear();
3228
5976
  appliedReceipts.clear();
3229
5977
  actor = undefined;
3230
5978
  activeSignal = undefined;
5979
+ activeAborts = undefined;
5980
+ actorSettlementAborts = undefined;
5981
+ actorSettlementErrorAborts = undefined;
5982
+ activeAbortEmission = undefined;
3231
5983
  activeTurnId = undefined;
5984
+ activeGovernedBoundarySeen = false;
5985
+ activeGovernedAttemptId = undefined;
5986
+ activeEffectLedgerPrefixSequence = undefined;
5987
+ failedGovernedAttemptUnknown = false;
5988
+ failedEffectBoundaryPrefix = undefined;
5989
+ failedGovernedAttemptId = undefined;
3232
5990
  controlPlaneError = undefined;
3233
5991
  emissionFailure = undefined;
3234
5992
  lastBossEvent = undefined;
5993
+ retainedEffectSourceSessionId = undefined;
5994
+ retainedEffectReconciliation = undefined;
5995
+ retainedEffectReconciliationRequired = false;
5996
+ reconstructedGovernedDelivery = undefined;
5997
+ reconstructedGovernedPrefixSequence = undefined;
5998
+ reconstructedAcceptancePending = undefined;
5999
+ governedSettlementsByBoundaryId.clear();
6000
+ governedCompletionEvidenceByBoundaryId.clear();
6001
+ unresolvedSemanticBoundaryIds.clear();
6002
+ deferredReconciliationOperationId = undefined;
6003
+ deferredSettlementClosure = undefined;
6004
+ expectedBoundPendingQuestion = undefined;
6005
+ activeDeferredContinuation = undefined;
6006
+ deferInspectionEmissions = false;
6007
+ deferredInspectionEmissions = [];
3235
6008
  savedPorts = undefined;
3236
6009
  runtimePorts = undefined;
3237
6010
  session = undefined;
@@ -3261,4 +6034,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
3261
6034
  };
3262
6035
  return runtime;
3263
6036
  };
6037
+ Object.defineProperty(createPlaybookRuntime, 'compat', {
6038
+ value: Object.freeze({ artifactSchema, runtimeAbi: RUNTIME_ABI }),
6039
+ enumerable: true,
6040
+ writable: false,
6041
+ configurable: false,
6042
+ });
6043
+ return createPlaybookRuntime;
3264
6044
  }