@sublang/playbook 9.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 (64) hide show
  1. package/docs/cli.md +51 -8
  2. package/docs/embedding.md +38 -12
  3. package/package.json +7 -3
  4. package/reference/sdlc/captain.md +14 -10
  5. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  6. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  7. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  8. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  9. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  10. package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
  11. package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
  12. package/reference/sdlc/code.md +0 -1
  13. package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
  14. package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
  15. package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
  16. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  17. package/reference/sdlc/code.playbook/bin/run.js +365 -63
  18. package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
  19. package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
  20. package/reference/sdlc/code.playbook/code.fsm.js +74 -25
  21. package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
  22. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  23. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  24. package/reference/sdlc/code.playbook/code.playbook.js +54 -2
  25. package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
  26. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  27. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  28. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  29. package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
  30. package/reference/sdlc/code.playbook/playbook-captain.js +1850 -72
  31. package/reference/sdlc/code.playbook/playbook-captain.ts +2759 -96
  32. package/reference/sdlc/decide.md +0 -1
  33. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
  34. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  35. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  36. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  37. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
  38. package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
  39. package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
  40. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  41. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  42. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  43. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  44. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  45. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  46. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  47. package/reference/sdlc/review.playbook/review.playbook.js +65 -2
  48. package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
  49. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  50. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  51. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  52. package/slc/gears2fsm.md +6 -5
  53. package/slc/link.md +544 -41
  54. package/src/accepted-outcome.d.ts +18 -0
  55. package/src/accepted-outcome.js +94 -0
  56. package/src/accepted-outcome.ts +140 -0
  57. package/src/runtime.d.ts +164 -3
  58. package/src/runtime.ts +213 -2
  59. package/src/xstate-playbook-runtime.d.ts +149 -10
  60. package/src/xstate-playbook-runtime.js +2569 -270
  61. package/src/xstate-playbook-runtime.ts +4133 -490
  62. package/src/xstate-runtime.d.ts +59 -1
  63. package/src/xstate-runtime.js +866 -7
  64. package/src/xstate-runtime.ts +1397 -7
@@ -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).
@@ -361,6 +455,97 @@ function sortJson(value) {
361
455
  function stableJson(value, path) {
362
456
  return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
363
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
+ }
364
549
  /**
365
550
  * Default direct-Captain prompt composer (slc/link.md §Captain prompt
366
551
  * composition). Placeholder substitution is presence-based: string fields
@@ -430,6 +615,43 @@ export function defaultBuildJudgePrompt(input, finalText) {
430
615
  }
431
616
  return lines.join('\n');
432
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
+ }
433
655
  const NO_VERBATIM_FIELDS = new Set();
434
656
  /**
435
657
  * LLM-judge adjudicator for delegated players. Coerces the player's
@@ -462,7 +684,12 @@ export async function adjudicatePlayerOutput(spec, input, finalText, ports, sign
462
684
  const verbatim = finalText.trim();
463
685
  for (const field of extractFields(input.result[guard])) {
464
686
  if (verbatimFields.has(field)) {
465
- obj[field] = verbatim;
687
+ Object.defineProperty(obj, field, {
688
+ value: verbatim,
689
+ enumerable: true,
690
+ configurable: true,
691
+ writable: true,
692
+ });
466
693
  continue;
467
694
  }
468
695
  if (typeof obj[field] !== 'string') {
@@ -490,6 +717,7 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
490
717
  let roleId;
491
718
  let prompt;
492
719
  try {
720
+ spec.validateInput?.(input);
493
721
  roleId = spec.resolveRoleId(input);
494
722
  prompt = spec.composePlayerPrompt(input);
495
723
  }
@@ -503,7 +731,9 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
503
731
  ? boundary.callPlayer(input, roleId, prompt, activeSignal)
504
732
  : ports.callPlayer(roleId, prompt, activeSignal, { resume });
505
733
  let result = await callPlayer(false);
506
- if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
734
+ if (result.status === 'ok' &&
735
+ isEmptyFinalText(result.finalText) &&
736
+ (spec.allowsCorrectiveReplay?.(result) ?? true)) {
507
737
  // An abort that lands between the empty first result and the
508
738
  // corrective call ends the turn as ordinary abort settlement with
509
739
  // no second host call — aborts are never retried (DR-028 via
@@ -531,12 +761,20 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
531
761
  throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
532
762
  }
533
763
  try {
534
- 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);
535
772
  validateBossReplyOutput(input, output, spec.resumableStateIds);
536
773
  return output;
537
774
  }
538
775
  catch (error) {
539
- if (!isAbortFailure(error, activeSignal)) {
776
+ if (!isAbortFailure(error, activeSignal) &&
777
+ !isFsmResultFailure(error)) {
540
778
  onControlPlaneError?.(error);
541
779
  }
542
780
  throw error;
@@ -850,7 +1088,7 @@ function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
850
1088
  }
851
1089
  function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
852
1090
  if (value === undefined) {
853
- throw new TypeError(`${label} roleStates must be supplied for schema 2`);
1091
+ throw new TypeError(`${label} roleStates must be supplied for schema 3`);
854
1092
  }
855
1093
  const captured = snapshotJsonValue(value, `${label} roleStates`);
856
1094
  if (!isPlainObject(captured)) {
@@ -894,23 +1132,189 @@ function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
894
1132
  }
895
1133
  return statuses;
896
1134
  }
897
- function settlingGuard(event) {
898
- if (!isPlainObject(event) || !isPlainObject(event.output))
899
- return undefined;
900
- const guard = event.output.guard;
901
- return typeof guard === 'string' && guard.trim().length > 0
902
- ? guard
903
- : 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
+ }
904
1311
  }
905
1312
  function askerLabel(asker) {
906
1313
  return asker.kind === 'captain' ? 'Captain' : asker.roleId;
907
1314
  }
908
1315
  function makeDefaultStatusesForState(roleStates) {
909
- return (state, context, event) => {
1316
+ return (state, context) => {
910
1317
  const statuses = [];
911
- const guard = settlingGuard(event);
912
- if (guard !== undefined)
913
- statuses.push({ message: `→ ${guard}` });
914
1318
  const stateId = state.stateId;
915
1319
  if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) {
916
1320
  return statuses;
@@ -1294,29 +1698,44 @@ function assertFlatStateIdentity(machine, label) {
1294
1698
  }
1295
1699
  }
1296
1700
  }
1297
- /**
1298
- * Build a `PlaybookRuntimeFactory` that interprets the given FSM artifact
1299
- * under the slc/link.md contract. The factory provides every actor kind the
1300
- * machine declares — `player`, `script`, `captain`, and nested `playbook`
1301
- * (literal and dynamic) — and implements the full runtime lifecycle including
1302
- * the optional parked-session snapshot capability (DR-014).
1303
- *
1304
- * Scope: flat single-region machines — no parallel state, no compound
1305
- * child states, and every root state's `meta.playbook.stateId` equal to its
1306
- * state key — so each snapshot exposes exactly one playbook state id.
1307
- * Parallel-region FSMs keep their own linked runtimes.
1308
- */
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
+ }
1309
1728
  export function createXStatePlaybookRuntime(machine, spec) {
1310
1729
  const label = spec.label ?? 'playbook';
1311
1730
  // DR-022 / PBRT-50: reject an incompatible artifact declaration before any
1312
1731
  // machine interpretation, against this loaded engine's own self-report.
1313
- assertRuntimeCompat(spec.compat, label);
1732
+ const artifactSchema = assertRuntimeCompat(spec.compat, label);
1314
1733
  const specDescriptors = Object.getOwnPropertyDescriptors(spec);
1315
1734
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
1316
- throw new TypeError(`${label} schema-2 artifacts must supply roleStates, not playerStates`);
1735
+ throw new TypeError(`${label} artifacts must supply roleStates, not playerStates`);
1317
1736
  }
1318
1737
  if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
1319
- 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`);
1320
1739
  }
1321
1740
  if (machineDeclaresParallelState(machine)) {
1322
1741
  throw new Error(`${label} uses a parallel state; the shared runtime supports only single-region FSMs`);
@@ -1325,6 +1744,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
1325
1744
  throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
1326
1745
  }
1327
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
+ });
1328
1755
  const declaredActors = collectInvokeSources(machine);
1329
1756
  const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
1330
1757
  // DR-029: source state descriptions label the control actions the
@@ -1357,18 +1784,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
1357
1784
  ((input) => defaultComposePlayerPrompt(input, spec.placeholderFields));
1358
1785
  const composeCaptainPrompt = spec.composeCaptainPrompt ??
1359
1786
  ((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
1787
+ const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
1788
+ const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
1360
1789
  const adjudication = {
1361
1790
  ...(spec.buildJudgePrompt !== undefined
1362
1791
  ? { buildJudgePrompt: spec.buildJudgePrompt }
1363
1792
  : {}),
1364
- ...(spec.extractRequiredFields !== undefined
1365
- ? { extractRequiredFields: spec.extractRequiredFields }
1366
- : {}),
1367
- ...(spec.verbatimPayloadFields !== undefined
1368
- ? { verbatimPayloadFields: spec.verbatimPayloadFields }
1369
- : {}),
1793
+ extractRequiredFields: extractFields,
1794
+ verbatimPayloadFields,
1370
1795
  };
1371
- 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
+ }
1372
1803
  // Build the derived classifier unconditionally: it is the sole validator of
1373
1804
  // supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
1374
1805
  // fail factory construction whether or not this spec overrides the
@@ -1379,6 +1810,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1379
1810
  makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
1380
1811
  const statusesForState = spec.statusesForState ??
1381
1812
  makeDefaultStatusesForState(roleStates);
1813
+ const usesDefaultStatuses = spec.statusesForState === undefined;
1382
1814
  const classificationStatus = spec.classificationStatus ??
1383
1815
  ((event) => event.type);
1384
1816
  const machineInput = spec.machineInput ?? ((options) => options);
@@ -1387,8 +1819,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
1387
1819
  const cwd = options?.cwd;
1388
1820
  return typeof cwd === 'string' ? cwd : undefined;
1389
1821
  });
1390
- return function createPlaybookRuntime(options) {
1391
- 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);
1392
1855
  const boundScriptCwd = scriptCwd(boundOptions);
1393
1856
  let actor;
1394
1857
  let session;
@@ -1417,6 +1880,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
1417
1880
  // its accepted receipt.
1418
1881
  let activeAbortEmission;
1419
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 = [];
1420
1899
  let controlPlaneError;
1421
1900
  // Previous root-machine state for the inspect-driven telemetry /
1422
1901
  // status emitter. undefined before the first inspect firing.
@@ -1444,6 +1923,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1444
1923
  const privateResumeTokens = new Map();
1445
1924
  const activePlayerKeys = new Set();
1446
1925
  const playbookCallTurnIds = new Map();
1926
+ const playbookCallEffectPrefixes = new Map();
1447
1927
  // Captain and judge work share one serialized lane (slc/link.md
1448
1928
  // §Session lifecycle).
1449
1929
  const judgeQueue = new PQueue({ concurrency: 1 });
@@ -1453,6 +1933,427 @@ export function createXStatePlaybookRuntime(machine, spec) {
1453
1933
  // Inspection callbacks enqueue a complete ordered batch synchronously;
1454
1934
  // imperative boundaries await their queued work directly.
1455
1935
  let emissionFailure;
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;
1959
+ }
1960
+ }
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;
1987
+ }
1988
+ }
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;
1997
+ }
1998
+ function runtimeBoundaryIsOwned(boundary) {
1999
+ if (session === undefined || boundary.playbookId !== session.playbookId) {
2000
+ return false;
2001
+ }
2002
+ return (boundary.runtimeSessionId === session.sessionId ||
2003
+ boundary.runtimeSessionId === retainedEffectSourceSessionId);
2004
+ }
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;
2019
+ }
2020
+ }
2021
+ const expectedDispositions = [
2022
+ ...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
2023
+ ];
2024
+ if (!sameStringSet(candidate.dispositions, expectedDispositions)) {
2025
+ return undefined;
2026
+ }
2027
+ return outcomes;
2028
+ }
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;
2047
+ }
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;
2058
+ }
2059
+ }
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;
2070
+ }
2071
+ return {
2072
+ reconciliation,
2073
+ historicalDeferred,
2074
+ };
2075
+ }
2076
+ catch {
2077
+ return undefined;
2078
+ }
2079
+ }
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;
2097
+ }
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';
2110
+ }
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
+ }
1456
2357
  function bindSession(nextSession) {
1457
2358
  const bound = snapshotPlaybookSession(nextSession);
1458
2359
  if (bound.roleBindings === undefined)
@@ -1655,7 +2556,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1655
2556
  const currentSession = requireSession();
1656
2557
  const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
1657
2558
  return {
1658
- schemaVersion: 3,
2559
+ schemaVersion: 4,
1659
2560
  sessionId: currentSession.sessionId,
1660
2561
  playbookId: currentSession.playbookId,
1661
2562
  rootSessionId: currentSession.rootSessionId,
@@ -1769,31 +2670,605 @@ export function createXStatePlaybookRuntime(machine, spec) {
1769
2670
  throw error;
1770
2671
  }
1771
2672
  }
1772
- const boundary = {
1773
- async callPlayer(input, roleId, prompt, signal) {
1774
- // State-entry telemetry/status must precede the call they describe.
1775
- await drainEmissions();
1776
- const turnId = activeTurnId;
1777
- const stateId = input.stateId;
1778
- const playerId = resolvedPlayerId(roleId);
1779
- let resume;
1780
- try {
1781
- signal.throwIfAborted();
1782
- resume = selectPlayerResume(roleId, playerId);
1783
- }
1784
- catch (error) {
1785
- if (!isAbortFailure(error, signal))
1786
- controlPlaneError ??= error;
1787
- throw error;
1788
- }
1789
- const callId = `player-${++playerCallSequence}`;
1790
- const identity = {
1791
- ...stateIdentity(stateId),
1792
- sourceItem: input.sourceItem,
1793
- roleId,
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
+ }
3235
+ const boundary = {
3236
+ async callPlayer(input, roleId, prompt, signal) {
3237
+ // State-entry telemetry/status must precede the call they describe.
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
+ }
3248
+ const turnId = activeTurnId;
3249
+ const stateId = input.stateId;
3250
+ const playerId = resolvedPlayerId(roleId);
3251
+ let selectedResume;
3252
+ try {
3253
+ signal.throwIfAborted();
3254
+ selectedResume =
3255
+ deferredContinuation?.playerContinuation ??
3256
+ selectPlayerResume(roleId, playerId);
3257
+ }
3258
+ catch (error) {
3259
+ if (!isAbortFailure(error, signal))
3260
+ controlPlaneError ??= error;
3261
+ throw error;
3262
+ }
3263
+ const callId = deferredContinuation?.effectBoundary.callId ??
3264
+ `player-${++playerCallSequence}`;
3265
+ const callIdentity = (resume) => ({
3266
+ ...stateIdentity(stateId),
3267
+ sourceItem: input.sourceItem,
3268
+ roleId,
1794
3269
  ...(playerId === undefined ? {} : { playerId }),
1795
3270
  resume,
1796
- };
3271
+ });
1797
3272
  const position = {
1798
3273
  ...(turnId !== undefined ? { turnId } : {}),
1799
3274
  callId,
@@ -1801,92 +3276,182 @@ export function createXStatePlaybookRuntime(machine, spec) {
1801
3276
  const playerKey = continuationKey(roleId, playerId);
1802
3277
  if (activePlayerKeys.has(playerKey)) {
1803
3278
  const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
1804
- await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position, signal);
1805
- 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);
1806
3285
  throw error;
1807
3286
  }
1808
3287
  activePlayerKeys.add(playerKey);
1809
3288
  try {
1810
- await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position, signal);
1811
- let rawResult;
1812
- try {
1813
- // An abort may land while the awaited started emission drains
1814
- // (e.g. fired from the trace sink itself); the host call must
1815
- // never start after abort, so settle the already-started pair
1816
- // as `aborted` through the catch below.
1817
- signal.throwIfAborted();
1818
- rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
1819
- // A host promise is not required to honor cancellation. Do not let
1820
- // a late result mutate continuity or publish a successful finish.
1821
- signal.throwIfAborted();
1822
- }
1823
- catch (error) {
1824
- if (!isAbortFailure(error, signal))
1825
- 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;
1826
3293
  try {
1827
- await emitTrace('player.call.finished', {
1828
- ...identity,
1829
- status: isAbortFailure(error, signal) ? 'aborted' : 'error',
1830
- error: normalizeError(error),
1831
- }, 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();
1832
3304
  }
1833
- catch {
1834
- // 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;
1835
3321
  }
1836
- // A thrown port call carries no authoritative result, so the
1837
- // prior token remains available for a later explicit resume.
1838
- throw error;
1839
- }
1840
- let result;
1841
- try {
1842
- result = validatePlayerResult(rawResult);
1843
- }
1844
- catch (error) {
1845
- if (!isAbortFailure(error, signal))
1846
- controlPlaneError ??= error;
3322
+ let result;
1847
3323
  try {
1848
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
3324
+ result = validatePlayerResult(rawResult);
1849
3325
  }
1850
- catch {
1851
- // 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;
1852
3336
  }
1853
- throw error;
3337
+ try {
3338
+ updatePlayerResume(roleId, playerId, result);
3339
+ }
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;
1854
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;
1855
3404
  try {
1856
- updatePlayerResume(roleId, playerId, result);
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);
1857
3412
  }
1858
3413
  catch (error) {
3414
+ if (expectedBoundPendingQuestion !== undefined) {
3415
+ closeAfterIndeterminateDeferredSettlement(undefined, error);
3416
+ }
3417
+ expectedBoundPendingQuestion = undefined;
3418
+ governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
3419
+ governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
3420
+ refreshGovernedBoundaryStart(effectBoundary.boundaryId);
1859
3421
  if (!isAbortFailure(error, signal))
1860
3422
  controlPlaneError ??= error;
1861
- try {
1862
- await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
1863
- }
1864
- catch {
1865
- // The continuation-store failure remains authoritative.
1866
- }
1867
3423
  throw error;
1868
3424
  }
1869
- await emitTrace('player.call.finished', {
1870
- ...identity,
1871
- status: result.status,
1872
- ...(result.finalText !== undefined
1873
- ? { finalText: result.finalText }
1874
- : {}),
1875
- ...(result.error !== undefined
1876
- ? { error: normalizeError(result.error) }
1877
- : {}),
1878
- ...(result.resumeToken !== undefined
1879
- ? { resumeToken: result.resumeToken }
1880
- : {}),
1881
- }, position);
1882
- return result;
1883
3425
  }
1884
3426
  finally {
1885
3427
  activePlayerKeys.delete(playerKey);
1886
3428
  }
1887
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
+ },
1888
3450
  async callJudge(purpose, stateId, prompt, signal) {
1889
3451
  return judgeQueue.add(async () => {
3452
+ const governedSemanticJudge = purpose === 'player-output-adjudication' &&
3453
+ stateId !== undefined &&
3454
+ outcomeAuthority.governedPlayerStates[stateId] !== undefined;
1890
3455
  signal.throwIfAborted();
1891
3456
  // A transition/status queued synchronously by XState must reach
1892
3457
  // the host before the judge call that follows it.
@@ -1911,7 +3476,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1911
3476
  signal.throwIfAborted();
1912
3477
  }
1913
3478
  catch (error) {
1914
- if (!isAbortFailure(error, signal)) {
3479
+ if (!isAbortFailure(error, signal) && !governedSemanticJudge) {
1915
3480
  controlPlaneError ??= error;
1916
3481
  }
1917
3482
  await emitTrace('judge.call.finished', {
@@ -1923,7 +3488,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
1923
3488
  }
1924
3489
  if (typeof reply !== 'string') {
1925
3490
  const error = new TypeError('judge reply must be a string');
1926
- controlPlaneError ??= error;
3491
+ if (!governedSemanticJudge)
3492
+ controlPlaneError ??= error;
1927
3493
  await emitTrace('judge.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
1928
3494
  throw error;
1929
3495
  }
@@ -2050,9 +3616,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2050
3616
  function playerActor(ports) {
2051
3617
  return createPlayerBridge({
2052
3618
  resolveRoleId: requireRoleId,
3619
+ validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
2053
3620
  composePlayerPrompt: composeBoundPlayerPrompt,
2054
3621
  adjudication,
2055
3622
  resumableStateIds,
3623
+ allowsCorrectiveReplay: acknowledgedBoundaryIsUnchanged,
2056
3624
  }, ports, () => activeSignal, boundary, (error) => {
2057
3625
  if (activeSignal === undefined || !isAbortFailure(error, activeSignal)) {
2058
3626
  controlPlaneError ??= error;
@@ -2316,6 +3884,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2316
3884
  callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
2317
3885
  emitStarted: async (event, aborts) => {
2318
3886
  playbookCallTurnIds.set(event.callId, activeTurnId);
3887
+ if (hasGovernedPlayerStates) {
3888
+ playbookCallEffectPrefixes.set(event.callId, activeEffectLedgerPrefixSequence);
3889
+ }
2319
3890
  await emitTrace('playbook.call.started', {
2320
3891
  stateId: event.stateId,
2321
3892
  playbookId: event.playbookId,
@@ -2340,6 +3911,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2340
3911
  }
2341
3912
  finally {
2342
3913
  playbookCallTurnIds.delete(event.callId);
3914
+ playbookCallEffectPrefixes.delete(event.callId);
2343
3915
  }
2344
3916
  },
2345
3917
  drain: drainEmissions,
@@ -2378,7 +3950,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2378
3950
  state,
2379
3951
  };
2380
3952
  const pendingBossQuestion = pendingBossQuestionForState(state, context);
2381
- if (pendingBossQuestion !== undefined) {
3953
+ if (pendingBossQuestion !== undefined &&
3954
+ !hasUnresolvedReconciliation()) {
2382
3955
  payload.pendingBossQuestion = pendingBossQuestion;
2383
3956
  }
2384
3957
  if (state.stateId === 'failed') {
@@ -2388,9 +3961,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
2388
3961
  }
2389
3962
  return snapshotJsonValue(payload, 'FSM telemetry payload');
2390
3963
  }
2391
- function enqueueTransitionEmission(payload, state, statuses, position, aborts) {
3964
+ function enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, position, aborts) {
2392
3965
  const currentSession = requireSession();
2393
3966
  const transitionTrace = createTraceEvent('fsm.transition', payload, position);
3967
+ const acceptedOutcomeTraces = acceptedOutcomes.map((acceptedOutcome) => createTraceEvent('outcome.accepted', acceptedOutcome, position));
2394
3968
  const statusEmissions = statuses.map(({ message, data }) => ({
2395
3969
  message,
2396
3970
  data,
@@ -2410,6 +3984,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2410
3984
  topic: 'playbook.fsm.state',
2411
3985
  payload,
2412
3986
  });
3987
+ for (const acceptedOutcome of acceptedOutcomeTraces) {
3988
+ await currentSession.ports.emitTelemetry({
3989
+ topic: 'playbook.trace',
3990
+ payload: acceptedOutcome,
3991
+ });
3992
+ }
2413
3993
  for (const status of statusEmissions) {
2414
3994
  await currentSession.ports.emitTelemetry({
2415
3995
  topic: 'playbook.trace',
@@ -2462,10 +4042,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2462
4042
  if (!actor)
2463
4043
  return;
2464
4044
  suppressInspectionEmissions = true;
4045
+ acceptedOutcomeConsumer.reset();
2465
4046
  actor.stop();
2466
4047
  }
2467
4048
  function buildActor(ports, machineSnapshot) {
2468
4049
  priorState = undefined;
4050
+ acceptedOutcomeConsumer.reset();
2469
4051
  const actors = {};
2470
4052
  if (declaredActors.has('player'))
2471
4053
  actors.player = playerActor(ports);
@@ -2488,11 +4070,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
2488
4070
  ? {}
2489
4071
  : { snapshot: machineSnapshot }),
2490
4072
  inspect: (inspectionEvent) => {
2491
- if (inspectionEvent.type !== '@xstate.snapshot')
2492
- return;
2493
4073
  if (inspectionEvent.actorRef !== builtActor)
2494
4074
  return;
2495
- if (suppressInspectionEmissions)
4075
+ if (suppressInspectionEmissions)
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')
2496
4087
  return;
2497
4088
  const settlementAborts = consumeActorSettlementAborts(true);
2498
4089
  try {
@@ -2502,14 +4093,56 @@ export function createXStatePlaybookRuntime(machine, spec) {
2502
4093
  throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
2503
4094
  }
2504
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
+ }
2505
4115
  const context = (snap.context ??
2506
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
+ }
2507
4127
  const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
2508
- const statuses = statusesForState(state, context, inspectionEvent.event);
2509
- enqueueTransitionEmission(payload, state, statuses, tracePositionForActiveTurn(), settlementAborts);
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
+ }
2510
4142
  priorState = state;
2511
4143
  }
2512
4144
  catch (error) {
4145
+ acceptedOutcomeConsumer.reset();
2513
4146
  latchRuntimeError(error, settlementAborts);
2514
4147
  }
2515
4148
  },
@@ -2531,6 +4164,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2531
4164
  if (outcome === 'quiescent' || outcome === 'no-action') {
2532
4165
  return { outcome, state };
2533
4166
  }
4167
+ if (outcome === 'unresolved-effect') {
4168
+ return { outcome, state };
4169
+ }
2534
4170
  if (outcome === 'suspended') {
2535
4171
  const pendingCall = nestedBridge.getPendingCall();
2536
4172
  if (!pendingCall) {
@@ -2540,7 +4176,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
2540
4176
  }
2541
4177
  if (outcome === 'terminal') {
2542
4178
  const output = actor?.getSnapshot()?.output;
2543
- const stateDescription = stateDescriptionFor(state);
4179
+ const stateDescription = !hasUnresolvedReconciliation()
4180
+ ? stateDescriptionFor(state)
4181
+ : undefined;
2544
4182
  return {
2545
4183
  outcome,
2546
4184
  state,
@@ -2595,12 +4233,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
2595
4233
  ...stateIdentity(result.state.stateId),
2596
4234
  };
2597
4235
  }
2598
- // Shared failed-start cleanup for init and restore: stop the actor,
2599
- // 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
2600
4238
  // session.disposed boundary, and unbind every closure field so dispose
2601
- // stays callable. The caller rethrows its original failure. A restore
2602
- // failure skips the disposal trace — the parked session was never
2603
- // 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
2604
4242
  // authoritative (DR-014 §2).
2605
4243
  async function cleanupFailedStart(cause, options) {
2606
4244
  let finalState;
@@ -2648,6 +4286,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
2648
4286
  privateResumeTokens.clear();
2649
4287
  activePlayerKeys.clear();
2650
4288
  playbookCallTurnIds.clear();
4289
+ playbookCallEffectPrefixes.clear();
2651
4290
  activeEmissionCalls.clear();
2652
4291
  emissionQueue.clear();
2653
4292
  judgeQueue.clear();
@@ -2662,9 +4301,30 @@ export function createXStatePlaybookRuntime(machine, spec) {
2662
4301
  actorSettlementErrorAborts = undefined;
2663
4302
  activeAbortEmission = undefined;
2664
4303
  activeTurnId = undefined;
4304
+ activeGovernedBoundarySeen = false;
4305
+ activeGovernedAttemptId = undefined;
4306
+ activeEffectLedgerPrefixSequence = undefined;
4307
+ failedGovernedAttemptUnknown = false;
4308
+ failedEffectBoundaryPrefix = undefined;
4309
+ failedGovernedAttemptId = undefined;
2665
4310
  controlPlaneError = undefined;
2666
4311
  emissionFailure = undefined;
2667
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 = [];
2668
4328
  lastBossEvent = undefined;
2669
4329
  suppressInspectionEmissions = false;
2670
4330
  initialized = false;
@@ -2715,6 +4375,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2715
4375
  function retryActionFor(snapshot, stateId) {
2716
4376
  if (stateId !== 'failed')
2717
4377
  return undefined;
4378
+ if (!failedAttemptAllowsReplay())
4379
+ return undefined;
2718
4380
  const retryEvent = retryEventFrom(snapshot);
2719
4381
  if (retryEvent === undefined)
2720
4382
  return undefined;
@@ -2771,6 +4433,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
2771
4433
  return [];
2772
4434
  }
2773
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
+ }
2774
4461
  const retry = retryActionFor(snapshot, state.stateId);
2775
4462
  if (retry !== undefined)
2776
4463
  derived.push(retry);
@@ -2856,7 +4543,517 @@ export function createXStatePlaybookRuntime(machine, spec) {
2856
4543
  ...(receipt.disposition === 'executed' ? { run: receipt.run } : {}),
2857
4544
  };
2858
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);
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;
4987
+ }
4988
+ }
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`);
5016
+ }
5017
+ }
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;
5052
+ }
2859
5053
  const runtime = {
5054
+ ...(retainedGenerationMetadata === undefined
5055
+ ? {}
5056
+ : { retainedGenerationMetadata }),
2860
5057
  async init(nextSession) {
2861
5058
  if (initialized || disposed || disposalPromise !== undefined) {
2862
5059
  throw new Error('createPlaybookRuntime.init: already initialized');
@@ -2870,6 +5067,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2870
5067
  initInFlight = initialization;
2871
5068
  const initTask = (async () => {
2872
5069
  session = boundSession;
5070
+ syncDeferredReconciliationOverlay();
5071
+ refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
2873
5072
  savedPorts = boundSession.ports;
2874
5073
  runtimePorts = createRuntimePorts(boundSession.ports);
2875
5074
  suppressInspectionEmissions = false;
@@ -2902,6 +5101,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
2902
5101
  }
2903
5102
  if (activeSignal !== undefined)
2904
5103
  return undefined;
5104
+ if (deferredSettlementClosure !== undefined)
5105
+ return undefined;
2905
5106
  const pendingCall = nestedBridge.getPendingCall();
2906
5107
  const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
2907
5108
  if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
@@ -2925,6 +5126,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
2925
5126
  suspendedCall = {
2926
5127
  ...bridgeSuspendedCall,
2927
5128
  ...(turnId === undefined ? {} : { turnId }),
5129
+ ...(hasGovernedPlayerStates
5130
+ ? {
5131
+ effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
5132
+ }
5133
+ : {}),
2928
5134
  };
2929
5135
  }
2930
5136
  const state = currentState();
@@ -2933,9 +5139,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
2933
5139
  const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
2934
5140
  const context = actor.getSnapshot()
2935
5141
  .context;
2936
- const pending = pendingBossQuestionForState(state, 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;
2937
5157
  return {
2938
- schemaVersion: 3,
5158
+ schemaVersion: 4,
2939
5159
  playbookId: session.playbookId,
2940
5160
  machine: machineSnapshot,
2941
5161
  roleResumeTokens: snapshotRoleResumeTokens(),
@@ -2960,6 +5180,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
2960
5180
  sourceItem: pending.sourceItem,
2961
5181
  },
2962
5182
  ],
5183
+ effectLedger: effectLedgerMirror,
5184
+ ...(retainedEffectSourceSessionId === undefined
5185
+ ? {}
5186
+ : { retainedEffectSourceSessionId }),
5187
+ ...(retainedEffectReconciliation === undefined
5188
+ ? {}
5189
+ : { retainedEffectReconciliation }),
5190
+ ...(failedEffectAttempt === undefined
5191
+ ? {}
5192
+ : { failedEffectAttempt }),
2963
5193
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
2964
5194
  };
2965
5195
  },
@@ -2969,114 +5199,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
2969
5199
  // the session already started; the next public boundary continues
2970
5200
  // the contiguous trace sequence.
2971
5201
  async restore(nextSession, snapshot) {
2972
- if (initialized || disposed || disposalPromise !== undefined) {
2973
- throw new Error('createPlaybookRuntime.restore: already initialized');
2974
- }
2975
- const boundSession = bindSession(nextSession);
2976
- const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
2977
- if (declaredActors.has('captain') &&
2978
- boundSnapshot.sequences.captainCall === undefined) {
2979
- throw new TypeError('runtime snapshot sequences.captainCall is required for a direct-Captain artifact');
2980
- }
2981
- const suspendedCall = boundSnapshot.suspendedCall;
2982
- let priorExternalPlayerTokens;
2983
- let externalStoreRestoreAttempted = false;
2984
- initialized = true;
2985
- let finishInitialization;
2986
- const initialization = new Promise((resolve) => {
2987
- finishInitialization = resolve;
2988
- });
2989
- initInFlight = initialization;
2990
- const initTask = (async () => {
2991
- session = boundSession;
2992
- savedPorts = boundSession.ports;
2993
- runtimePorts = createRuntimePorts(boundSession.ports);
2994
- traceSequence = boundSnapshot.sequences.trace;
2995
- turnSequence = boundSnapshot.sequences.turn;
2996
- judgeCallSequence = boundSnapshot.sequences.judgeCall;
2997
- playerCallSequence = boundSnapshot.sequences.playerCall;
2998
- playbookCallSequence = boundSnapshot.sequences.playbookCall;
2999
- captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
3000
- // The runtime snapshot carries no apply counter (PBRT-50); every
3001
- // apply boundary consumed trace numbers, so the persisted trace
3002
- // counter is a collision-safe id floor here too, keeping
3003
- // `apply-<n>` call ids unique across restore.
3004
- applyCallSequence = boundSnapshot.sequences.trace;
3005
- if (boundSession.playerSessions) {
3006
- priorExternalPlayerTokens = snapshotRoleResumeTokens();
3007
- externalStoreRestoreAttempted = true;
3008
- }
3009
- restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
3010
- nestedBridge.prepareRestore(suspendedCall);
3011
- if (suspendedCall !== undefined) {
3012
- playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
3013
- }
3014
- suppressInspectionEmissions = true;
3015
- actor = buildActor(runtimePorts, boundSnapshot.machine);
3016
- actor.start();
3017
- // A start-time actor error rides the startup emission channel
3018
- // (latchRuntimeError); consume both latches here so the original
3019
- // error outranks the derived status check below.
3020
- {
3021
- const startupFailure = emissionFailure;
3022
- if (controlPlaneError !== undefined ||
3023
- startupFailure !== undefined) {
3024
- const startupError = controlPlaneError !== undefined
3025
- ? controlPlaneError
3026
- : startupFailure.error;
3027
- controlPlaneError = undefined;
3028
- if (emissionFailure === startupFailure) {
3029
- emissionFailure = undefined;
3030
- }
3031
- throw startupError;
3032
- }
3033
- }
3034
- const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
3035
- ? {}
3036
- : {
3037
- pendingCall: {
3038
- callId: suspendedCall.callId,
3039
- playbookId: suspendedCall.playbookId,
3040
- childSessionId: suspendedCall.childSessionId,
3041
- },
3042
- });
3043
- if (restoredState.status !== 'active') {
3044
- throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
3045
- }
3046
- if (stableJson(restoredState, 'restored runtime state') !==
3047
- stableJson(boundSnapshot.state, 'runtime snapshot state')) {
3048
- throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
3049
- }
3050
- priorState = restoredState;
3051
- await drainEmissions();
3052
- suppressInspectionEmissions = false;
3053
- // Final fallible step: after this publication the authoritative
3054
- // child has rejoined ordinary resume/abort ownership, so no later
3055
- // restore validation may trigger failed-start rollback.
3056
- nestedBridge.confirmRestore();
3057
- })();
3058
- try {
3059
- await initTask;
3060
- }
3061
- catch (error) {
3062
- let failure = error;
3063
- if (externalStoreRestoreAttempted &&
3064
- priorExternalPlayerTokens !== undefined) {
3065
- try {
3066
- boundSession.playerSessions.restore(priorExternalPlayerTokens);
3067
- }
3068
- catch (rollbackError) {
3069
- failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
3070
- }
3071
- }
3072
- await cleanupFailedStart(failure, { emitDisposal: false });
3073
- throw failure;
3074
- }
3075
- finally {
3076
- finishInitialization();
3077
- if (initInFlight === initialization)
3078
- initInFlight = undefined;
3079
- }
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);
3080
5211
  },
3081
5212
  // DR-029 / PBRT-52: side-effect-free control view over the live
3082
5213
  // snapshot, valid at parked quiescence outside an active boundary.
@@ -3092,14 +5223,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
3092
5223
  if (activeSignal !== undefined) {
3093
5224
  throw new Error('createPlaybookRuntime.describe: another runtime turn is active');
3094
5225
  }
5226
+ assertDeferredSettlementOpen('describe');
5227
+ refreshRetainedEffectFenceFromHost();
3095
5228
  const snapshot = actor.getSnapshot();
3096
5229
  const state = currentState();
3097
5230
  const context = (snapshot.context ??
3098
5231
  {});
3099
- const pending = pendingBossQuestionForState(state, context);
5232
+ const pending = !hasUnresolvedReconciliation()
5233
+ ? pendingBossQuestionForState(state, context)
5234
+ : undefined;
3100
5235
  const lastError = normalizeErrorFull(context.lastError);
3101
5236
  const projectedContext = projectControlContext(context);
3102
- const stateDescription = stateDescriptionFor(state);
5237
+ const stateDescription = !hasUnresolvedReconciliation()
5238
+ ? stateDescriptionFor(state)
5239
+ : undefined;
3103
5240
  return deepFreeze({
3104
5241
  state,
3105
5242
  ...(stateDescription === undefined ? {} : { stateDescription }),
@@ -3132,6 +5269,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3132
5269
  if (input === null || typeof input !== 'object') {
3133
5270
  throw new TypeError('createPlaybookRuntime.apply: input must be an object');
3134
5271
  }
5272
+ assertDeferredSettlementOpen('apply');
3135
5273
  const { actionId, key, signal } = input;
3136
5274
  if (typeof actionId !== 'string' || actionId.length === 0) {
3137
5275
  throw new TypeError('createPlaybookRuntime.apply: actionId must be a non-empty string');
@@ -3159,10 +5297,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
3159
5297
  // An abort before acceptance ends the call with no receipt
3160
5298
  // recorded, like every other pre-acceptance failure.
3161
5299
  signal.throwIfAborted();
5300
+ refreshRetainedEffectFenceFromHost();
3162
5301
  const turnId = ++turnSequence;
3163
5302
  const callId = `apply-${++applyCallSequence}`;
3164
5303
  const position = { turnId, callId };
3165
5304
  activeTurnId = turnId;
5305
+ beginAutomaticReplayBoundary();
3166
5306
  activeSignal = signal;
3167
5307
  activeAborts = abortReasonClassifier(signal);
3168
5308
  activeAbortEmission = undefined;
@@ -3274,13 +5414,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
3274
5414
  // the key, so the action can never execute twice.
3275
5415
  accepted = true;
3276
5416
  try {
3277
- actor.send(candidate.event);
3278
- await waitForPlaybookQuiescence(actor, {
3279
- pendingCalls: nestedBridge,
3280
- });
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
+ }
3281
5444
  if (controlPlaneError !== undefined)
3282
5445
  throw controlPlaneError;
3283
- const run = runResultFor(settledOutcome(signal));
3284
5446
  receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
3285
5447
  ? {
3286
5448
  disposition: 'failed',
@@ -3365,6 +5527,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
3365
5527
  activeAborts = undefined;
3366
5528
  activeAbortEmission = undefined;
3367
5529
  activeTurnId = undefined;
5530
+ activeGovernedBoundarySeen = false;
5531
+ activeGovernedAttemptId = undefined;
5532
+ activeEffectLedgerPrefixSequence = undefined;
3368
5533
  controlPlaneError = undefined;
3369
5534
  }
3370
5535
  // Past acceptance every settlement failure has been folded into the
@@ -3386,6 +5551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3386
5551
  }
3387
5552
  return receipt;
3388
5553
  },
5554
+ unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
3389
5555
  async handleBossInput({ text, signal, }) {
3390
5556
  if (!actor || !savedPorts) {
3391
5557
  throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
@@ -3396,8 +5562,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
3396
5562
  if (activeSignal !== undefined) {
3397
5563
  throw new Error('createPlaybookRuntime.handleBossInput: another runtime turn is active');
3398
5564
  }
5565
+ assertDeferredSettlementOpen('handleBossInput');
5566
+ refreshRetainedEffectFenceFromHost();
3399
5567
  const turnId = ++turnSequence;
3400
5568
  activeTurnId = turnId;
5569
+ beginAutomaticReplayBoundary();
3401
5570
  activeSignal = signal;
3402
5571
  activeAborts = abortReasonClassifier(signal);
3403
5572
  activeAbortEmission = undefined;
@@ -3420,11 +5589,26 @@ export function createXStatePlaybookRuntime(machine, spec) {
3420
5589
  // where applicable (slc/link.md §Boss-event mapping), judge
3421
5590
  // classification otherwise.
3422
5591
  let event;
5592
+ let classifiedSnapshot;
5593
+ let deferredPending;
5594
+ let deferredOperation;
5595
+ let pendingRequiresDeferredBinding = false;
3423
5596
  const trimmed = text.trim();
3424
5597
  if (trimmed !== '') {
3425
5598
  const snapshot = actor.getSnapshot();
5599
+ classifiedSnapshot = snapshot;
3426
5600
  const terminal = snapshot.status === 'done';
3427
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);
3428
5612
  // PBRT-1 / slc/link.md §Boss-event mapping: the idle entry, the
3429
5613
  // recoverable failure state, and the reconstructed terminal all
3430
5614
  // accept exactly one ordinary textual entry event, so delivered
@@ -3432,7 +5616,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
3432
5616
  // classifier whim to settle a restart as no action. Every other
3433
5617
  // parked state — a reply wait or an authored mid-workflow
3434
5618
  // checkpoint — classifies under its own Boss-event contracts.
3435
- if (spec.entryEvent !== undefined &&
5619
+ if (hasUnresolvedReconciliation()) {
5620
+ event = undefined;
5621
+ }
5622
+ else if (stateId === 'failed' &&
5623
+ !failedAttemptAllowsReplay()) {
5624
+ event = undefined;
5625
+ }
5626
+ else if (spec.entryEvent !== undefined &&
3436
5627
  (stateId === 'ready' || stateId === 'failed' || terminal)) {
3437
5628
  event = {
3438
5629
  type: spec.entryEvent.type,
@@ -3444,9 +5635,61 @@ export function createXStatePlaybookRuntime(machine, spec) {
3444
5635
  }
3445
5636
  signal.throwIfAborted();
3446
5637
  }
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`);
5645
+ }
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
+ }
5685
+ }
3447
5686
  // Empty input, no-action classifier output, or invalid classifier
3448
5687
  // output — nothing to send.
3449
- if (event === undefined) {
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) {
3450
5693
  result = runResultFor('no-action');
3451
5694
  }
3452
5695
  else {
@@ -3506,15 +5749,24 @@ export function createXStatePlaybookRuntime(machine, spec) {
3506
5749
  ((operationError !== undefined &&
3507
5750
  isAbortFailure(operationError, signal)) ||
3508
5751
  (drainAbort && operationError === undefined));
3509
- const settlementResult = primaryError === undefined
3510
- ? (result ?? runResultFor('no-action'))
3511
- : runResultFor(abortError ? 'aborted' : 'failed', primaryError);
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);
3512
5762
  let settlementEmissionError;
3513
- try {
3514
- await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
3515
- }
3516
- catch (error) {
3517
- settlementEmissionError = error;
5763
+ if (settlementResult !== undefined) {
5764
+ try {
5765
+ await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
5766
+ }
5767
+ catch (error) {
5768
+ settlementEmissionError = error;
5769
+ }
3518
5770
  }
3519
5771
  try {
3520
5772
  await drainEmissions();
@@ -3536,6 +5788,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
3536
5788
  !(abortError && settlementEmissionError === undefined)) {
3537
5789
  throw failure;
3538
5790
  }
5791
+ if (settlementResult === undefined) {
5792
+ throw deferredSettlementClosure;
5793
+ }
3539
5794
  return settlementResult;
3540
5795
  }
3541
5796
  finally {
@@ -3543,6 +5798,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
3543
5798
  activeAborts = undefined;
3544
5799
  activeAbortEmission = undefined;
3545
5800
  activeTurnId = undefined;
5801
+ activeGovernedBoundarySeen = false;
5802
+ activeGovernedAttemptId = undefined;
5803
+ activeEffectLedgerPrefixSequence = undefined;
3546
5804
  controlPlaneError = undefined;
3547
5805
  }
3548
5806
  },
@@ -3556,7 +5814,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
3556
5814
  if (activeSignal !== undefined) {
3557
5815
  throw new Error('createPlaybookRuntime.resumePlaybookCall: another runtime turn is active');
3558
5816
  }
5817
+ refreshRetainedEffectFenceFromHost();
5818
+ if (hasUnresolvedReconciliation()) {
5819
+ return runResultFor('no-action');
5820
+ }
3559
5821
  activeTurnId = playbookCallTurnIds.get(input.callId);
5822
+ bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
5823
+ ? playbookCallEffectPrefixes.get(input.callId)
5824
+ : hasGovernedPlayerStates
5825
+ ? 0
5826
+ : undefined);
3560
5827
  activeSignal = input.signal;
3561
5828
  activeAborts = abortReasonClassifier(input.signal);
3562
5829
  activeAbortEmission = undefined;
@@ -3635,6 +5902,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
3635
5902
  activeAborts = undefined;
3636
5903
  activeAbortEmission = undefined;
3637
5904
  activeTurnId = undefined;
5905
+ activeGovernedBoundarySeen = false;
5906
+ activeGovernedAttemptId = undefined;
5907
+ activeEffectLedgerPrefixSequence = undefined;
3638
5908
  controlPlaneError = undefined;
3639
5909
  }
3640
5910
  },
@@ -3699,6 +5969,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
3699
5969
  }
3700
5970
  activePlayerKeys.clear();
3701
5971
  playbookCallTurnIds.clear();
5972
+ playbookCallEffectPrefixes.clear();
3702
5973
  activeEmissionCalls.clear();
3703
5974
  emissionQueue.clear();
3704
5975
  judgeQueue.clear();
@@ -3710,9 +5981,30 @@ export function createXStatePlaybookRuntime(machine, spec) {
3710
5981
  actorSettlementErrorAborts = undefined;
3711
5982
  activeAbortEmission = undefined;
3712
5983
  activeTurnId = undefined;
5984
+ activeGovernedBoundarySeen = false;
5985
+ activeGovernedAttemptId = undefined;
5986
+ activeEffectLedgerPrefixSequence = undefined;
5987
+ failedGovernedAttemptUnknown = false;
5988
+ failedEffectBoundaryPrefix = undefined;
5989
+ failedGovernedAttemptId = undefined;
3713
5990
  controlPlaneError = undefined;
3714
5991
  emissionFailure = undefined;
3715
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 = [];
3716
6008
  savedPorts = undefined;
3717
6009
  runtimePorts = undefined;
3718
6010
  session = undefined;
@@ -3742,4 +6034,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
3742
6034
  };
3743
6035
  return runtime;
3744
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;
3745
6044
  }