@sublang/playbook 9.0.0 → 11.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 (71) hide show
  1. package/README.md +1 -1
  2. package/docs/cli.md +109 -21
  3. package/docs/configuration.md +89 -40
  4. package/docs/embedding.md +126 -12
  5. package/package.json +14 -3
  6. package/reference/sdlc/captain.md +14 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  10. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
  14. package/reference/sdlc/code.md +35 -16
  15. package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
  16. package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
  17. package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
  18. package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
  19. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  20. package/reference/sdlc/code.playbook/bin/run.js +669 -215
  21. package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
  22. package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
  23. package/reference/sdlc/code.playbook/code.fsm.js +74 -25
  24. package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
  25. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  26. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  27. package/reference/sdlc/code.playbook/code.playbook.js +54 -2
  28. package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
  29. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  30. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  31. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  32. package/reference/sdlc/code.playbook/playbook-captain.d.ts +103 -8
  33. package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
  34. package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
  35. package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
  36. package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
  37. package/reference/sdlc/code.playbook/session-store.js +113 -0
  38. package/reference/sdlc/decide.md +24 -16
  39. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
  40. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  41. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  42. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  43. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
  44. package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
  45. package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
  46. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  47. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  48. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  49. package/reference/sdlc/review.md +36 -18
  50. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  51. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  52. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  53. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  54. package/reference/sdlc/review.playbook/review.playbook.js +65 -2
  55. package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
  56. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  57. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  58. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  59. package/slc/gears2fsm.md +6 -5
  60. package/slc/link.md +544 -41
  61. package/src/accepted-outcome.d.ts +18 -0
  62. package/src/accepted-outcome.js +94 -0
  63. package/src/accepted-outcome.ts +140 -0
  64. package/src/runtime.d.ts +164 -3
  65. package/src/runtime.ts +213 -2
  66. package/src/xstate-playbook-runtime.d.ts +149 -10
  67. package/src/xstate-playbook-runtime.js +2569 -270
  68. package/src/xstate-playbook-runtime.ts +4133 -490
  69. package/src/xstate-runtime.d.ts +59 -1
  70. package/src/xstate-runtime.js +866 -7
  71. package/src/xstate-runtime.ts +1397 -7
@@ -16,24 +16,37 @@
16
16
  // Output profile: bespoke parallel runtime with the shared nested-call
17
17
  // bridge (slc/link.md §Output)
18
18
 
19
+ import { randomUUID } from 'node:crypto';
20
+
19
21
  import PQueue from 'p-queue';
20
22
  import { createActor, fromPromise } from 'xstate';
21
23
  import type { InspectionEvent, SnapshotFrom } from 'xstate';
22
24
 
25
+ import {
26
+ createAcceptedOutcomeConsumer,
27
+ type AcceptedOutcomeReceipt,
28
+ } from '../../../src/accepted-outcome.js';
29
+
23
30
  import {
24
31
  assertJsonSafe,
32
+ assertPlaybookEffectLedger,
25
33
  assertPlaybookRuntimeSnapshot,
26
34
  combineAbortSignals,
27
35
  createNestedPlaybookBridge,
28
36
  detachPersistedMachineSnapshot,
29
37
  normalizeError,
30
38
  normalizePlaybookSnapshot,
39
+ PlaybookSemanticCandidateStructureError,
40
+ reconcilePlaybookSemanticEvidence,
31
41
  snapshotJsonValue,
32
42
  snapshotPlaybookSession,
33
43
  validatePlayerResult,
34
44
  waitForPlaybookQuiescence,
35
45
  } from '../../../src/xstate-runtime.js';
36
- import type { NestedPlaybookBridge } from '../../../src/xstate-runtime.js';
46
+ import type {
47
+ NestedPlaybookBridge,
48
+ PlaybookSemanticOutcomeSpec,
49
+ } from '../../../src/xstate-runtime.js';
37
50
 
38
51
  import decideMachine, {
39
52
  type PlayerInput,
@@ -44,6 +57,8 @@ import decideMachine, {
44
57
  type PlaybookInput,
45
58
  } from './decide.fsm.js';
46
59
 
60
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
61
+
47
62
  import type {
48
63
  CaptainCallOptions,
49
64
  CaptainResult,
@@ -58,8 +73,14 @@ import type {
58
73
  PlaybookControlAction,
59
74
  PlaybookControlReceipt,
60
75
  PlaybookControlView,
76
+ PlaybookEffectBoundary,
77
+ PlaybookEffectBoundaryStart,
78
+ PlaybookEffectLedger,
79
+ PlaybookEffectLedgerCapability,
80
+ PlaybookPendingBossQuestion,
61
81
  PlaybookPendingCall,
62
82
  PlaybookPorts,
83
+ PlaybookRepositoryReceipt,
63
84
  PlaybookRunResult,
64
85
  PlaybookRuntime,
65
86
  PlaybookRuntimeFactory,
@@ -102,6 +123,14 @@ type RoleId = 'coder' | 'reviewer';
102
123
 
103
124
  export type PlaybookRuntimeOptions = DecideInput;
104
125
 
126
+ export type DecidePlaybookHostCapabilities =
127
+ PlaybookHostConstructionCapabilities;
128
+
129
+ export interface DecidePlaybookRuntimeConstruction {
130
+ readonly configuredOptions: PlaybookRuntimeOptions;
131
+ readonly hostCapabilities: DecidePlaybookHostCapabilities;
132
+ }
133
+
105
134
  function snapshotDecideRuntimeOptions(value: unknown): PlaybookRuntimeOptions {
106
135
  const captured = snapshotJsonValue(value, 'DECIDE runtime options');
107
136
  if (!isPlainObject(captured)) {
@@ -174,6 +203,61 @@ const ROLE_STATE_IDS: ReadonlySet<string> = new Set(
174
203
  ROLE_STATES.map((state) => state.stateId),
175
204
  );
176
205
 
206
+ const ACCEPTED_OUTCOME_DECLARATIONS: Readonly<
207
+ Record<string, ReadonlySet<string>>
208
+ > = Object.freeze({
209
+ askCoderProposal: new Set(['proposed', 'needsBossReply']),
210
+ askReviewerProposal: new Set(['proposed', 'needsBossReply']),
211
+ commitCoderProposal: new Set(['committed', 'needsBossReply']),
212
+ });
213
+
214
+ const DECIDE_OUTCOME_AUTHORITY = Object.freeze({
215
+ governedPlayerStates: Object.freeze({
216
+ askCoderProposal: Object.freeze({
217
+ proposed: Object.freeze({
218
+ fields: Object.freeze({ coderProposal: 'presentation' as const }),
219
+ repositoryDisposition: 'unchanged' as const,
220
+ }),
221
+ needsBossReply: Object.freeze({
222
+ fields: Object.freeze({ question: 'presentation' as const }),
223
+ repositoryDisposition: 'unchanged' as const,
224
+ }),
225
+ }),
226
+ askReviewerProposal: Object.freeze({
227
+ proposed: Object.freeze({
228
+ fields: Object.freeze({ reviewerProposal: 'presentation' as const }),
229
+ repositoryDisposition: 'unchanged' as const,
230
+ }),
231
+ needsBossReply: Object.freeze({
232
+ fields: Object.freeze({ question: 'presentation' as const }),
233
+ repositoryDisposition: 'unchanged' as const,
234
+ }),
235
+ }),
236
+ commitCoderProposal: Object.freeze({
237
+ committed: Object.freeze({
238
+ fields: Object.freeze({
239
+ coderOutput: 'presentation' as const,
240
+ latestCommit: 'effect' as const,
241
+ }),
242
+ repositoryDisposition: 'one-descendant-commit' as const,
243
+ }),
244
+ needsBossReply: Object.freeze({
245
+ fields: Object.freeze({ question: 'presentation' as const }),
246
+ repositoryDisposition: 'deferred' as const,
247
+ }),
248
+ }),
249
+ }),
250
+ }) satisfies {
251
+ readonly governedPlayerStates: Readonly<
252
+ Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
253
+ >;
254
+ };
255
+
256
+ const PROPOSAL_STATE_BY_ROLE = Object.freeze({
257
+ coder: 'askCoderProposal',
258
+ reviewer: 'askReviewerProposal',
259
+ } as const);
260
+
177
261
  const ROLE_IDS = ['coder', 'reviewer'] as const;
178
262
  const ROLE_ID_SET: ReadonlySet<string> = new Set(ROLE_IDS);
179
263
 
@@ -185,9 +269,16 @@ const BOSS_INTERRUPT_TARGETS = ['independentProposals'] as const;
185
269
  const BOSS_INTERRUPT_TARGET_IDS: ReadonlySet<string> = new Set(
186
270
  BOSS_INTERRUPT_TARGETS,
187
271
  );
272
+ const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
273
+ 'reportedReviewFailure',
274
+ ]);
188
275
 
189
276
  const TELEMETRY_TOPIC = 'playbook.fsm.state';
190
277
  const TRACE_TOPIC = 'playbook.trace';
278
+ const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID =
279
+ 'reconcile:unresolved-effect';
280
+ const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID =
281
+ 'abandon:unresolved-effect';
191
282
 
192
283
  const CONTINUATION_PREAMBLE =
193
284
  'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
@@ -261,6 +352,44 @@ function requiredFieldsFor(description: string): string[] {
261
352
  return fields;
262
353
  }
263
354
 
355
+ function governedOutcomesFor(
356
+ input: PlayerInput,
357
+ ): Readonly<Record<string, PlaybookSemanticOutcomeSpec>> {
358
+ const outcomes = (
359
+ DECIDE_OUTCOME_AUTHORITY.governedPlayerStates as Readonly<
360
+ Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
361
+ >
362
+ )[input.stateId];
363
+ if (outcomes === undefined) {
364
+ throw new TypeError(
365
+ `DECIDE governed player state ${JSON.stringify(input.stateId)} has no outcome authority`,
366
+ );
367
+ }
368
+ const declaredGuards = Object.keys(outcomes).sort();
369
+ const authoredGuards = Object.keys(input.result).sort();
370
+ if (
371
+ declaredGuards.length !== authoredGuards.length ||
372
+ declaredGuards.some((guard, index) => guard !== authoredGuards[index])
373
+ ) {
374
+ throw new TypeError(
375
+ `DECIDE governed player state ${input.stateId} changed its authored outcome set`,
376
+ );
377
+ }
378
+ for (const guard of declaredGuards) {
379
+ const required = requiredFieldsFor(input.result[guard]!).sort();
380
+ const authoritative = Object.keys(outcomes[guard]!.fields).sort();
381
+ if (
382
+ required.length !== authoritative.length ||
383
+ required.some((field, index) => field !== authoritative[index])
384
+ ) {
385
+ throw new TypeError(
386
+ `DECIDE governed outcome ${input.stateId}.${guard} changed its payload fields`,
387
+ );
388
+ }
389
+ }
390
+ return outcomes;
391
+ }
392
+
264
393
  // LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
265
394
  // CODE's recovery contract: scan candidate starts in document order, prefer a
266
395
  // strict balanced value at each position, then repair trailing commas and
@@ -329,6 +458,14 @@ function stableJson(value: unknown, path: string): string {
329
458
  return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
330
459
  }
331
460
 
461
+ function deepFreeze<T>(value: T): T {
462
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
463
+ Object.freeze(value);
464
+ for (const member of Object.values(value)) deepFreeze(member);
465
+ }
466
+ return value;
467
+ }
468
+
332
469
  function stripCodeFence(text: string): string {
333
470
  const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
334
471
  return fence ? fence[1].trim() : text;
@@ -492,7 +629,9 @@ function parseClassification(
492
629
  function buildAdjudicatorPrompt(
493
630
  input: PlayerInput,
494
631
  playerOutput: string,
632
+ correction?: { readonly reply: string; readonly error: string },
495
633
  ): string {
634
+ const outcomes = governedOutcomesFor(input);
496
635
  const lines: string[] = [];
497
636
  lines.push('You are the guard adjudicator for a playbook state machine.');
498
637
  lines.push(
@@ -514,72 +653,44 @@ function buildAdjudicatorPrompt(
514
653
  'Guards (choose exactly one; the descriptions are authoritative and must be applied as written):',
515
654
  );
516
655
  for (const [guard, description] of Object.entries(input.result)) {
517
- lines.push(`- ${guard}: ${description}`);
518
- }
519
- const runtimeOwnedFields = new Set<string>();
520
- for (const description of Object.values(input.result)) {
521
- for (const field of requiredFieldsFor(description)) {
522
- if (VERBATIM_PAYLOAD_FIELDS.has(field)) runtimeOwnedFields.add(field);
523
- }
524
- }
525
- if (runtimeOwnedFields.size > 0) {
526
- lines.push('');
656
+ const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
657
+ .filter(([, authority]) => authority === 'semantic')
658
+ .map(([field]) => field);
527
659
  lines.push(
528
- `The runtime owns these verbatim fields; do not include them in your JSON: ${[...runtimeOwnedFields].join(', ')}.`,
660
+ `- ${guard}: semantic fields: ${
661
+ semanticFields.length === 0 ? '(none)' : semanticFields.join(', ')
662
+ }; ${description}`,
529
663
  );
530
664
  }
531
665
  lines.push('');
532
666
  lines.push(
533
- 'Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.',
667
+ 'Reply with exactly the chosen `guard` and every semantic-owned field for that guard, and no other field.',
668
+ );
669
+ lines.push(
670
+ 'Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from authoritative evidence.',
534
671
  );
672
+ if (correction !== undefined) {
673
+ lines.push('');
674
+ lines.push('Your first reply was structurally invalid:');
675
+ lines.push('"""');
676
+ lines.push(correction.reply);
677
+ lines.push('"""');
678
+ lines.push(`Validation error: ${correction.error}`);
679
+ lines.push(
680
+ 'Correct only that structure using the same player output and outcome schema.',
681
+ );
682
+ }
535
683
  return lines.join('\n');
536
684
  }
537
685
 
538
- function parseAdjudication(
539
- raw: string,
540
- input: PlayerInput,
541
- finalText: string,
542
- ): PlayerOutput {
543
- const obj = extractJson(raw);
544
- if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
545
- throw new Error('adjudicator returned empty or malformed JSON');
546
- }
547
-
548
- const guard = obj.guard;
549
- if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
550
- throw new Error(
551
- `adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`,
686
+ function parseGovernedSemanticCandidate(raw: string): unknown {
687
+ try {
688
+ return parseJudgeJson(raw);
689
+ } catch (error) {
690
+ throw new PlaybookSemanticCandidateStructureError(
691
+ error instanceof Error ? error.message : 'reply is not valid JSON',
552
692
  );
553
693
  }
554
-
555
- const requiredFields = requiredFieldsFor(input.result[guard]);
556
- const allowedFields = new Set(['guard', ...requiredFields]);
557
- for (const key of Reflect.ownKeys(obj)) {
558
- if (typeof key !== 'string' || !allowedFields.has(key)) {
559
- throw new Error(
560
- `adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`,
561
- );
562
- }
563
- }
564
- const output: Record<string, unknown> = { guard };
565
- for (const field of requiredFields) {
566
- if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
567
- output[field] = finalText;
568
- continue;
569
- }
570
- const value = obj[field];
571
- if (
572
- value === undefined ||
573
- value === null ||
574
- (typeof value === 'string' && value.trim() === '')
575
- ) {
576
- throw new Error(
577
- `adjudicator response for guard "${guard}" missing required field "${field}"`,
578
- );
579
- }
580
- output[field] = value;
581
- }
582
- return output as PlayerOutput;
583
694
  }
584
695
 
585
696
  function combineSignals(
@@ -611,6 +722,369 @@ function isEmptyFinalText(finalText: string | undefined): boolean {
611
722
  return finalText === undefined || finalText.trim().length === 0;
612
723
  }
613
724
 
725
+ interface AutomaticReplayPolicy {
726
+ allowsEmptyOkCorrection(runtimeSessionId: string, callId: string): boolean;
727
+ allowsFailureStateRetry(): boolean;
728
+ }
729
+
730
+ interface Schema3AutomaticReplayEvidence {
731
+ readonly effectLedger: PlaybookEffectLedgerCapability;
732
+ }
733
+
734
+ interface DecideRepositoryOperationSettlement<T> {
735
+ readonly status: 'fulfilled';
736
+ readonly value: T;
737
+ }
738
+
739
+ interface DecideRepositoryOperationRejection {
740
+ readonly status: 'rejected';
741
+ readonly reason: unknown;
742
+ }
743
+
744
+ interface DecideRepositoryCompletion<T> {
745
+ readonly boundary: PlaybookEffectBoundary;
746
+ readonly operation:
747
+ | DecideRepositoryOperationSettlement<T>
748
+ | DecideRepositoryOperationRejection;
749
+ readonly receipt: PlaybookRepositoryReceipt;
750
+ readonly outcomeReceipt: PlaybookRepositoryReceipt;
751
+ readonly roleId?: RoleId;
752
+ }
753
+
754
+ interface DecideDeferredBinding {
755
+ readonly operationId: string;
756
+ readonly pendingQuestion: PlaybookPendingBossQuestion;
757
+ readonly playerContinuation: JsonValue;
758
+ }
759
+
760
+ interface DecideRepositoryCompletionEvidence {
761
+ readonly finalText?: string;
762
+ readonly semanticCandidate?: JsonValue;
763
+ readonly deferred?: DecideDeferredBinding;
764
+ readonly unresolved?: true;
765
+ }
766
+
767
+ interface DecideRepositoryExclusiveResult<T> {
768
+ readonly operation:
769
+ | DecideRepositoryOperationSettlement<T>
770
+ | DecideRepositoryOperationRejection;
771
+ readonly receipt: PlaybookRepositoryReceipt;
772
+ readonly effectLedger: PlaybookEffectLedger;
773
+ readonly deferredStatus?: 'bound' | 'unresolved';
774
+ }
775
+
776
+ interface DecideRepositoryCohortResult<T> {
777
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
778
+ readonly invocationId: string;
779
+ readonly operations: Readonly<
780
+ Record<
781
+ RoleId,
782
+ | DecideRepositoryOperationSettlement<T>
783
+ | DecideRepositoryOperationRejection
784
+ >
785
+ >;
786
+ readonly receipts: Readonly<Record<RoleId, PlaybookRepositoryReceipt>>;
787
+ readonly effectLedger: PlaybookEffectLedger;
788
+ }
789
+
790
+ interface DecideRepositoryDeferredContinuationResult<T>
791
+ extends DecideRepositoryExclusiveResult<T> {
792
+ readonly status: 'continued';
793
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
794
+ readonly logicalReceipt?: PlaybookRepositoryReceipt;
795
+ }
796
+
797
+ interface DecideRepositoryDeferredCheckpointMismatch {
798
+ readonly status: 'checkpoint-mismatch' | 'ineligible';
799
+ readonly effectLedger: PlaybookEffectLedger;
800
+ }
801
+
802
+ interface DecideRepositoryDeferredParked {
803
+ readonly status: 'parked';
804
+ readonly effectLedger: PlaybookEffectLedger;
805
+ }
806
+
807
+ interface DecideRepositoryDeferredRestoreResult {
808
+ readonly status: 'restored' | 'checkpoint-mismatch' | 'ineligible';
809
+ readonly effectLedger: PlaybookEffectLedger;
810
+ }
811
+
812
+ type DecideEffectBoundarySeed = Omit<
813
+ PlaybookEffectBoundaryStart,
814
+ 'playbookId' | 'canonicalWorktree' | 'baseline' | 'cohortId'
815
+ >;
816
+
817
+ interface DecideRepositoryCapability {
818
+ runExclusive<T>(options: {
819
+ readonly signal: AbortSignal;
820
+ readonly effectBoundary: DecideEffectBoundarySeed;
821
+ readonly operation: (context: {
822
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
823
+ readonly identity: unknown;
824
+ }) => Promise<T>;
825
+ readonly completeEffectBoundary: (
826
+ completion: DecideRepositoryCompletion<T>,
827
+ ) =>
828
+ | DecideRepositoryCompletionEvidence
829
+ | Promise<DecideRepositoryCompletionEvidence>;
830
+ }): Promise<DecideRepositoryExclusiveResult<T>>;
831
+ runCohort<T>(options: {
832
+ readonly signal: AbortSignal;
833
+ readonly invocationId: string;
834
+ readonly roleIds: readonly [RoleId, RoleId];
835
+ readonly dispositionsByRole: Readonly<
836
+ Record<RoleId, readonly ['unchanged']>
837
+ >;
838
+ readonly effectBoundaries: Readonly<
839
+ Record<RoleId, DecideEffectBoundarySeed>
840
+ >;
841
+ readonly operations: Readonly<
842
+ Record<
843
+ RoleId,
844
+ (context: {
845
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
846
+ readonly identity: unknown;
847
+ readonly invocationId: string;
848
+ readonly roleId: RoleId;
849
+ }) => Promise<T>
850
+ >
851
+ >;
852
+ readonly completeEffectBoundary: (
853
+ completion: DecideRepositoryCompletion<T> & { readonly roleId: RoleId },
854
+ ) =>
855
+ | DecideRepositoryCompletionEvidence
856
+ | Promise<DecideRepositoryCompletionEvidence>;
857
+ }): Promise<DecideRepositoryCohortResult<T>>;
858
+ runDeferred<T>(options: {
859
+ readonly mode: 'continue';
860
+ readonly signal: AbortSignal;
861
+ readonly operationId: string;
862
+ readonly effectBoundary: DecideEffectBoundarySeed;
863
+ readonly operation: (context: {
864
+ readonly baseline: PlaybookRepositoryReceipt['baseline'];
865
+ readonly identity: unknown;
866
+ readonly playerContinuation: JsonValue;
867
+ }) => Promise<T>;
868
+ readonly completeEffectBoundary: (
869
+ completion: DecideRepositoryCompletion<T>,
870
+ ) =>
871
+ | DecideRepositoryCompletionEvidence
872
+ | Promise<DecideRepositoryCompletionEvidence>;
873
+ }): Promise<
874
+ | DecideRepositoryDeferredContinuationResult<T>
875
+ | DecideRepositoryDeferredCheckpointMismatch
876
+ >;
877
+ runDeferred(options: {
878
+ readonly mode: 'park' | 'restore';
879
+ readonly signal: AbortSignal;
880
+ readonly operationId: string;
881
+ }): Promise<
882
+ DecideRepositoryDeferredParked | DecideRepositoryDeferredRestoreResult
883
+ >;
884
+ }
885
+
886
+ interface Schema3DeferredEffectEvidence extends Schema3AutomaticReplayEvidence {
887
+ readonly authority: DecidePlaybookHostCapabilities['authority'];
888
+ readonly repository: DecideRepositoryCapability;
889
+ }
890
+
891
+ function schema3Construction(
892
+ value: unknown,
893
+ ): {
894
+ readonly configuredOptions: PlaybookRuntimeOptions;
895
+ readonly hostCapabilities: Schema3DeferredEffectEvidence;
896
+ } {
897
+ if (!isPlainObject(value)) {
898
+ throw new TypeError('DECIDE schema-3 factory input must be a plain object');
899
+ }
900
+ const descriptors = Object.getOwnPropertyDescriptors(value);
901
+ const keys = Reflect.ownKeys(value);
902
+ if (
903
+ keys.length !== 2 ||
904
+ !keys.includes('configuredOptions') ||
905
+ !keys.includes('hostCapabilities') ||
906
+ keys.some((key) => {
907
+ const descriptor = descriptors[key as keyof typeof descriptors];
908
+ return (
909
+ descriptor?.get !== undefined ||
910
+ descriptor?.set !== undefined ||
911
+ descriptor?.enumerable !== true ||
912
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value')
913
+ );
914
+ })
915
+ ) {
916
+ throw new TypeError(
917
+ 'DECIDE schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties',
918
+ );
919
+ }
920
+ const configuredOptions = descriptors.configuredOptions!.value;
921
+ if (
922
+ configuredOptions !== null &&
923
+ typeof configuredOptions === 'object' &&
924
+ Object.prototype.hasOwnProperty.call(
925
+ configuredOptions,
926
+ 'hostCapabilities',
927
+ )
928
+ ) {
929
+ throw new TypeError(
930
+ 'DECIDE configured options must not contain hostCapabilities',
931
+ );
932
+ }
933
+ const hostCapabilities = descriptors.hostCapabilities!.value;
934
+ if (
935
+ hostCapabilities === null ||
936
+ typeof hostCapabilities !== 'object' ||
937
+ Array.isArray(hostCapabilities)
938
+ ) {
939
+ throw new TypeError(
940
+ 'DECIDE schema-3 factory input hostCapabilities must be a live object',
941
+ );
942
+ }
943
+ const repositoryDescriptor = Object.getOwnPropertyDescriptor(
944
+ hostCapabilities,
945
+ 'repository',
946
+ );
947
+ const authorityDescriptor = Object.getOwnPropertyDescriptor(
948
+ hostCapabilities,
949
+ 'authority',
950
+ );
951
+ const effectLedgerDescriptor = Object.getOwnPropertyDescriptor(
952
+ hostCapabilities,
953
+ 'effectLedger',
954
+ );
955
+ const repository = repositoryDescriptor?.value;
956
+ const authority = authorityDescriptor?.value;
957
+ const effectLedger = effectLedgerDescriptor?.value;
958
+ if (
959
+ authorityDescriptor === undefined ||
960
+ authorityDescriptor.get !== undefined ||
961
+ authorityDescriptor.set !== undefined ||
962
+ !Object.prototype.hasOwnProperty.call(authorityDescriptor, 'value') ||
963
+ !isPlainObject(authority) ||
964
+ authority.artifactSchema !== 3 ||
965
+ authority.playbookId !== 'decide' ||
966
+ typeof authority.sessionId !== 'string' ||
967
+ authority.sessionId.length === 0 ||
968
+ !Array.isArray(authority.requiredRoleIds) ||
969
+ stableJson(
970
+ [...authority.requiredRoleIds].sort(),
971
+ 'DECIDE host authority required roles',
972
+ ) !== stableJson([...ROLE_IDS].sort(), 'DECIDE required roles') ||
973
+ !Array.isArray(authority.concurrentRoleSets) ||
974
+ authority.concurrentRoleSets.length !== 1 ||
975
+ !Array.isArray(authority.concurrentRoleSets[0]) ||
976
+ stableJson(
977
+ [...authority.concurrentRoleSets[0]].sort(),
978
+ 'DECIDE host authority concurrent roles',
979
+ ) !== stableJson([...ROLE_IDS].sort(), 'DECIDE concurrent roles')
980
+ ) {
981
+ throw new TypeError(
982
+ 'DECIDE schema-3 factory input hostCapabilities.authority must identify one schema-3 playbook session',
983
+ );
984
+ }
985
+ if (
986
+ repositoryDescriptor === undefined ||
987
+ repositoryDescriptor.get !== undefined ||
988
+ repositoryDescriptor.set !== undefined ||
989
+ !Object.prototype.hasOwnProperty.call(repositoryDescriptor, 'value') ||
990
+ !isPlainObject(repository) ||
991
+ typeof repository.runExclusive !== 'function' ||
992
+ typeof repository.runCohort !== 'function' ||
993
+ typeof repository.runDeferred !== 'function'
994
+ ) {
995
+ throw new TypeError(
996
+ 'DECIDE schema-3 factory input hostCapabilities.repository must expose runExclusive, runCohort, and runDeferred functions',
997
+ );
998
+ }
999
+ if (
1000
+ effectLedgerDescriptor === undefined ||
1001
+ effectLedgerDescriptor.get !== undefined ||
1002
+ effectLedgerDescriptor.set !== undefined ||
1003
+ !Object.prototype.hasOwnProperty.call(effectLedgerDescriptor, 'value') ||
1004
+ !isPlainObject(effectLedger) ||
1005
+ typeof effectLedger.snapshot !== 'function' ||
1006
+ typeof effectLedger.writeAhead !== 'function'
1007
+ ) {
1008
+ throw new TypeError(
1009
+ 'DECIDE schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions',
1010
+ );
1011
+ }
1012
+ return {
1013
+ configuredOptions: configuredOptions as PlaybookRuntimeOptions,
1014
+ hostCapabilities: {
1015
+ authority:
1016
+ authority as unknown as DecidePlaybookHostCapabilities['authority'],
1017
+ repository: repository as unknown as DecideRepositoryCapability,
1018
+ effectLedger: effectLedger as unknown as PlaybookEffectLedgerCapability,
1019
+ },
1020
+ };
1021
+ }
1022
+
1023
+ interface DeferredValue<T> {
1024
+ readonly promise: Promise<T>;
1025
+ resolve(value: T): void;
1026
+ reject(reason: unknown): void;
1027
+ }
1028
+
1029
+ function deferredValue<T>(): DeferredValue<T> {
1030
+ let resolve!: (value: T) => void;
1031
+ let reject!: (reason: unknown) => void;
1032
+ const promise = new Promise<T>((onResolve, onReject) => {
1033
+ resolve = onResolve;
1034
+ reject = onReject;
1035
+ });
1036
+ return { promise, resolve, reject };
1037
+ }
1038
+
1039
+ function hasCompleteUnchangedReceipt(
1040
+ boundary: PlaybookEffectBoundary,
1041
+ ): boolean {
1042
+ return boundary.physicalReceipt?.classification === 'unchanged';
1043
+ }
1044
+
1045
+ // A corrective call is bound to the exact physical boundary it would repeat.
1046
+ // A failed-state restart replays the whole entry event. Cooperative host
1047
+ // attempts are serialized in ledger order, so the latest durable boundary
1048
+ // identifies the causal host attempt even when a nested or sibling runtime
1049
+ // wrote it; every boundary in that attempt must have a complete unchanged
1050
+ // receipt.
1051
+ // The durable ledger remains the authority in both cases; no process-local
1052
+ // player result or presentation text can make a replay safe.
1053
+ function createAutomaticReplayPolicy(
1054
+ evidence: Schema3AutomaticReplayEvidence,
1055
+ ): AutomaticReplayPolicy {
1056
+ const readLedger = (): PlaybookEffectLedger =>
1057
+ assertPlaybookEffectLedger(
1058
+ evidence.effectLedger.snapshot(),
1059
+ 'DECIDE automatic-replay effect ledger',
1060
+ );
1061
+
1062
+ return Object.freeze({
1063
+ allowsEmptyOkCorrection(runtimeSessionId: string, callId: string) {
1064
+ const matching = readLedger().boundaries.filter(
1065
+ (boundary) =>
1066
+ boundary.runtimeSessionId === runtimeSessionId &&
1067
+ boundary.callId === callId,
1068
+ );
1069
+ return (
1070
+ matching.length === 1 && hasCompleteUnchangedReceipt(matching[0]!)
1071
+ );
1072
+ },
1073
+
1074
+ allowsFailureStateRetry() {
1075
+ const ledger = readLedger();
1076
+ const latest = ledger.boundaries.at(-1);
1077
+ if (latest === undefined) return false;
1078
+ const attempt = ledger.boundaries.filter(
1079
+ (boundary) => boundary.attemptId === latest.attemptId,
1080
+ );
1081
+ return (
1082
+ attempt.length > 0 && attempt.every(hasCompleteUnchangedReceipt)
1083
+ );
1084
+ },
1085
+ });
1086
+ }
1087
+
614
1088
  function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
615
1089
  return signal.aborted && Object.is(error, signal.reason);
616
1090
  }
@@ -766,8 +1240,12 @@ function telemetryPayload(
766
1240
  state: PlaybookState,
767
1241
  event: unknown,
768
1242
  context: Record<string, unknown>,
1243
+ hiddenQuestionId?: string,
769
1244
  ): JsonValue {
770
- const pendingBossQuestions = pendingQuestionsForState(state, context);
1245
+ const pendingBossQuestions = pendingQuestionsForState(
1246
+ state,
1247
+ context,
1248
+ ).filter(({ questionId }) => questionId !== hiddenQuestionId);
771
1249
  const prior = previousState ?? state;
772
1250
  const payload = {
773
1251
  from: prior.value,
@@ -784,10 +1262,56 @@ function telemetryPayload(
784
1262
  return payload;
785
1263
  }
786
1264
 
787
- export const createPlaybookRuntime: PlaybookRuntimeFactory<
788
- PlaybookRuntimeOptions
789
- > = (options) => {
1265
+ type DecidePlaybookRuntime = PlaybookRuntime & {
1266
+ _getNestedBridge(): NestedPlaybookBridge<PlaybookInput>;
1267
+ };
1268
+
1269
+ type DeferredContinuationEnvelope = {
1270
+ readonly roleId: RoleId;
1271
+ readonly playerId?: string;
1272
+ readonly callId: string;
1273
+ readonly result: PlayerResult;
1274
+ };
1275
+
1276
+ interface ActiveDeferredContinuation {
1277
+ readonly operationId: string;
1278
+ readonly effectBoundary: DecideEffectBoundarySeed;
1279
+ readonly result: DeferredValue<PlayerResult>;
1280
+ readonly acknowledged: DeferredValue<PlayerResult>;
1281
+ playerContinuation?: string | false;
1282
+ input?: PlayerInput;
1283
+ playerId?: string;
1284
+ }
1285
+
1286
+ interface PendingProposalCohortMember {
1287
+ readonly input: PlayerInput;
1288
+ readonly signal: AbortSignal;
1289
+ readonly callId: string;
1290
+ readonly effectBoundary: DecideEffectBoundarySeed;
1291
+ readonly result: DeferredValue<DeferredContinuationEnvelope>;
1292
+ envelope?: DeferredContinuationEnvelope;
1293
+ }
1294
+
1295
+ function createDecidePlaybookRuntime(
1296
+ options: PlaybookRuntimeOptions,
1297
+ deferredEffects: Schema3DeferredEffectEvidence,
1298
+ ): DecidePlaybookRuntime {
1299
+ const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
790
1300
  const fsmInput = snapshotDecideRuntimeOptions(options);
1301
+ const readEffectLedger = (): PlaybookEffectLedger =>
1302
+ assertPlaybookEffectLedger(
1303
+ deferredEffects.effectLedger.snapshot(),
1304
+ 'DECIDE current host effect ledger',
1305
+ );
1306
+ let effectLedgerMirror = readEffectLedger();
1307
+ const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer(
1308
+ (source, acceptedOutcome) =>
1309
+ Object.prototype.hasOwnProperty.call(
1310
+ ACCEPTED_OUTCOME_DECLARATIONS,
1311
+ source,
1312
+ ) &&
1313
+ ACCEPTED_OUTCOME_DECLARATIONS[source]?.has(acceptedOutcome) === true,
1314
+ );
791
1315
 
792
1316
  type SessionIdentity = Readonly<PlaybookSession>;
793
1317
 
@@ -807,6 +1331,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
807
1331
  let judgeCallSequence = 0;
808
1332
  let playerCallSequence = 0;
809
1333
  let playbookCallSequence = 0;
1334
+ let applyCallSequence = 0;
810
1335
  let lifecycleStarted = false;
811
1336
  let initInFlight: Promise<void> | undefined;
812
1337
  let disposed = false;
@@ -820,6 +1345,35 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
820
1345
  const activeEmissionCalls = new Set<Promise<void>>();
821
1346
  const emissionQueue = new PQueue({ concurrency: 1 });
822
1347
  const judgeQueue = new PQueue({ concurrency: 1 });
1348
+ // A proposal cohort completes both semantic callbacks concurrently at the
1349
+ // repository seam. Serialize each whole adjudication/correction transaction
1350
+ // so their durable correction-budget compare-and-swaps cannot race.
1351
+ const semanticCompletionQueue = new PQueue({ concurrency: 1 });
1352
+ const governedOutputsByBoundaryId = new Map<string, PlayerOutput>();
1353
+ const governedFailuresByBoundaryId = new Map<string, Error>();
1354
+ const governedEvidenceByBoundaryId = new Map<
1355
+ string,
1356
+ { readonly finalText: string; readonly semanticCandidate?: JsonValue }
1357
+ >();
1358
+ const governedReceiptsByBoundaryId = new Map<
1359
+ string,
1360
+ {
1361
+ readonly physicalReceipt: PlaybookRepositoryReceipt;
1362
+ readonly outcomeReceipt: PlaybookRepositoryReceipt;
1363
+ }
1364
+ >();
1365
+ const governedPlayerOutputs = new WeakMap<object, PlayerOutput>();
1366
+ const unresolvedSemanticBoundaryIds = new Set<string>();
1367
+ const appliedControlReceipts = new Map<string, PlaybookControlReceipt>();
1368
+ const pendingProposalCohort = new Map<
1369
+ RoleId,
1370
+ PendingProposalCohortMember
1371
+ >();
1372
+ let activeProposalCohort: Promise<void> | undefined;
1373
+ let completedProposalCohortTurnId: number | undefined;
1374
+ let deferredOperationId: string | undefined;
1375
+ let hiddenDeferredOperationId: string | undefined;
1376
+ let activeDeferredContinuation: ActiveDeferredContinuation | undefined;
823
1377
 
824
1378
  const collectFailure = (failures: unknown[], error: unknown): void => {
825
1379
  if (error instanceof AggregateError) {
@@ -922,6 +1476,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
922
1476
 
923
1477
  const bindSession = (nextSession: PlaybookSession): SessionIdentity => {
924
1478
  const bound = snapshotPlaybookSession(nextSession);
1479
+ if (bound.playbookId !== deferredEffects.authority.playbookId) {
1480
+ throw new TypeError(
1481
+ 'DECIDE runtime playbook identity must match its bound schema-3 host authority',
1482
+ );
1483
+ }
925
1484
  if (bound.roleBindings === undefined) return bound;
926
1485
  const actual = Object.keys(bound.roleBindings).sort();
927
1486
  const expected = [...ROLE_IDS].sort();
@@ -1102,6 +1661,273 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1102
1661
  pendingCall,
1103
1662
  });
1104
1663
  };
1664
+ const visiblePendingQuestionsForState = (
1665
+ state: PlaybookState,
1666
+ context: Record<string, unknown>,
1667
+ ): PendingBossQuestion[] =>
1668
+ pendingQuestionsForState(state, context).filter(
1669
+ ({ questionId }) =>
1670
+ questionId !== 'commitCoderProposal' ||
1671
+ hiddenDeferredOperationId === undefined,
1672
+ );
1673
+ const openDeferredOperation = (
1674
+ ledger: PlaybookEffectLedger = effectLedgerMirror,
1675
+ ) => {
1676
+ if (sessionIdentity === undefined) return undefined;
1677
+ const open = ledger.logicalOperations.filter(
1678
+ (operation) =>
1679
+ operation.playbookId === sessionIdentity!.playbookId &&
1680
+ operation.runtimeSessionId === sessionIdentity!.sessionId &&
1681
+ operation.logicalReceipt === undefined,
1682
+ );
1683
+ if (open.length > 1) {
1684
+ throw new TypeError(
1685
+ 'DECIDE runtime has multiple open deferred logical operations',
1686
+ );
1687
+ }
1688
+ return open[0];
1689
+ };
1690
+ const runtimeBoundaryIsOwned = (
1691
+ boundary: PlaybookEffectBoundary,
1692
+ ): boolean =>
1693
+ sessionIdentity !== undefined &&
1694
+ boundary.playbookId === sessionIdentity.playbookId &&
1695
+ boundary.runtimeSessionId === sessionIdentity.sessionId;
1696
+ const governedOutcomesForBoundary = (
1697
+ boundary: PlaybookEffectBoundary,
1698
+ ): Readonly<Record<string, PlaybookSemanticOutcomeSpec>> | undefined => {
1699
+ const outcomes = (
1700
+ DECIDE_OUTCOME_AUTHORITY.governedPlayerStates as Readonly<
1701
+ Record<string, Readonly<Record<string, PlaybookSemanticOutcomeSpec>>>
1702
+ >
1703
+ )[boundary.sourceStateId];
1704
+ if (
1705
+ outcomes === undefined ||
1706
+ !isPlainObject(boundary.sourceOutcomeSchema)
1707
+ ) {
1708
+ return undefined;
1709
+ }
1710
+ const authoredGuards = Object.keys(boundary.sourceOutcomeSchema).sort();
1711
+ const governedGuards = Object.keys(outcomes).sort();
1712
+ if (
1713
+ authoredGuards.length !== governedGuards.length ||
1714
+ authoredGuards.some((guard, index) => guard !== governedGuards[index])
1715
+ ) {
1716
+ return undefined;
1717
+ }
1718
+ for (const guard of governedGuards) {
1719
+ const description = boundary.sourceOutcomeSchema[guard];
1720
+ if (typeof description !== 'string') return undefined;
1721
+ const authoredFields = [
1722
+ ...new Set(requiredFieldsFor(description)),
1723
+ ].sort();
1724
+ const governedFields = Object.keys(outcomes[guard]!.fields).sort();
1725
+ if (
1726
+ authoredFields.length !== governedFields.length ||
1727
+ authoredFields.some(
1728
+ (field, index) => field !== governedFields[index],
1729
+ )
1730
+ ) {
1731
+ return undefined;
1732
+ }
1733
+ }
1734
+ const dispositions = [
1735
+ ...new Set(
1736
+ Object.values(outcomes).map(
1737
+ ({ repositoryDisposition }) => repositoryDisposition,
1738
+ ),
1739
+ ),
1740
+ ];
1741
+ const actualDispositions = new Set(boundary.dispositions);
1742
+ return actualDispositions.size === boundary.dispositions.length &&
1743
+ actualDispositions.size === dispositions.length &&
1744
+ dispositions.every((disposition) => actualDispositions.has(disposition))
1745
+ ? outcomes
1746
+ : undefined;
1747
+ };
1748
+ const persistedBoundaryReconciliation = (
1749
+ boundary: PlaybookEffectBoundary,
1750
+ ledger: PlaybookEffectLedger,
1751
+ ) => {
1752
+ const outcomes = governedOutcomesForBoundary(boundary);
1753
+ if (outcomes === undefined || boundary.semanticCandidate === undefined) {
1754
+ return undefined;
1755
+ }
1756
+ let receipt = boundary.physicalReceipt;
1757
+ let awaitingLogicalReceipt = false;
1758
+ let historicalDeferred = false;
1759
+ const logicalOperation =
1760
+ boundary.logicalOperationId === undefined
1761
+ ? undefined
1762
+ : ledger.logicalOperations.find(
1763
+ ({ operationId }) =>
1764
+ operationId === boundary.logicalOperationId,
1765
+ );
1766
+ if (
1767
+ boundary.logicalOperationId !== undefined &&
1768
+ logicalOperation === undefined
1769
+ ) {
1770
+ return undefined;
1771
+ }
1772
+ if (logicalOperation !== undefined) {
1773
+ if (logicalOperation.boundaryIds.at(-1) !== boundary.boundaryId) {
1774
+ historicalDeferred = true;
1775
+ } else if (logicalOperation.logicalReceipt !== undefined) {
1776
+ receipt = logicalOperation.logicalReceipt;
1777
+ } else if (
1778
+ logicalOperation.pendingQuestion === undefined ||
1779
+ logicalOperation.checkpoint === undefined ||
1780
+ !Object.prototype.hasOwnProperty.call(
1781
+ logicalOperation,
1782
+ 'playerContinuation',
1783
+ )
1784
+ ) {
1785
+ return undefined;
1786
+ } else {
1787
+ awaitingLogicalReceipt = true;
1788
+ }
1789
+ }
1790
+ try {
1791
+ const reconciliation = reconcilePlaybookSemanticEvidence({
1792
+ outcomes,
1793
+ semanticCandidate: boundary.semanticCandidate,
1794
+ finalText: boundary.finalText,
1795
+ receipt,
1796
+ });
1797
+ if (awaitingLogicalReceipt && reconciliation.status !== 'deferred') {
1798
+ return undefined;
1799
+ }
1800
+ return { reconciliation, historicalDeferred };
1801
+ } catch {
1802
+ return undefined;
1803
+ }
1804
+ };
1805
+ const boundaryNeedsSemanticReconciliation = (
1806
+ boundary: PlaybookEffectBoundary,
1807
+ ledger: PlaybookEffectLedger,
1808
+ ): boolean => {
1809
+ if (!runtimeBoundaryIsOwned(boundary)) return false;
1810
+ if (governedOutcomesForBoundary(boundary) === undefined) return true;
1811
+ const persisted = persistedBoundaryReconciliation(boundary, ledger);
1812
+ if (persisted !== undefined) {
1813
+ if (persisted.reconciliation.status === 'unresolved') return true;
1814
+ if (persisted.historicalDeferred) {
1815
+ return persisted.reconciliation.status !== 'deferred';
1816
+ }
1817
+ if (
1818
+ persisted.reconciliation.status === 'deferred' &&
1819
+ boundary.logicalOperationId === undefined
1820
+ ) {
1821
+ return true;
1822
+ }
1823
+ return false;
1824
+ }
1825
+ if (boundary.physicalReceipt === undefined) return true;
1826
+ if (
1827
+ typeof boundary.finalText === 'string' &&
1828
+ boundary.finalText.trim().length > 0
1829
+ ) {
1830
+ return true;
1831
+ }
1832
+ return boundary.physicalReceipt.classification !== 'unchanged';
1833
+ };
1834
+ const refreshUnresolvedSemanticReconciliation = (
1835
+ ledger: PlaybookEffectLedger = effectLedgerMirror,
1836
+ ): void => {
1837
+ unresolvedSemanticBoundaryIds.clear();
1838
+ for (const boundary of ledger.boundaries) {
1839
+ if (boundaryNeedsSemanticReconciliation(boundary, ledger)) {
1840
+ unresolvedSemanticBoundaryIds.add(boundary.boundaryId);
1841
+ }
1842
+ }
1843
+ };
1844
+ const synchronizeDeferredProjection = (
1845
+ ledger: PlaybookEffectLedger = effectLedgerMirror,
1846
+ ): void => {
1847
+ if (deferredEffects === undefined) return;
1848
+ effectLedgerMirror = ledger;
1849
+ refreshUnresolvedSemanticReconciliation(ledger);
1850
+ const operation = openDeferredOperation(ledger);
1851
+ if (operation === undefined) {
1852
+ deferredOperationId = undefined;
1853
+ hiddenDeferredOperationId = undefined;
1854
+ return;
1855
+ }
1856
+ deferredOperationId = operation.operationId;
1857
+ hiddenDeferredOperationId =
1858
+ operation.logicalReceipt === undefined &&
1859
+ (operation.checkpointRestorationEligible ||
1860
+ operation.pendingQuestion === undefined)
1861
+ ? operation.operationId
1862
+ : undefined;
1863
+ };
1864
+ const hasUnresolvedReconciliation = (): boolean =>
1865
+ hiddenDeferredOperationId !== undefined ||
1866
+ unresolvedSemanticBoundaryIds.size > 0;
1867
+ const refreshReconciliationProjection = (): void => {
1868
+ synchronizeDeferredProjection(readEffectLedger());
1869
+ };
1870
+ const unresolvedEffectEnvelopeIdentities = (): readonly (
1871
+ | { readonly kind: 'boundary'; readonly boundaryId: string }
1872
+ | { readonly kind: 'logical-operation'; readonly operationId: string }
1873
+ )[] => {
1874
+ if (sessionIdentity === undefined) return [];
1875
+ refreshReconciliationProjection();
1876
+ if (!hasUnresolvedReconciliation()) return [];
1877
+ const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
1878
+ const operationIds = new Set<string>();
1879
+ if (hiddenDeferredOperationId !== undefined) {
1880
+ operationIds.add(hiddenDeferredOperationId);
1881
+ }
1882
+ for (const boundaryId of [...boundaryIds]) {
1883
+ const boundary = effectLedgerMirror.boundaries.find(
1884
+ (candidate) => candidate.boundaryId === boundaryId,
1885
+ );
1886
+ const operation =
1887
+ boundary?.logicalOperationId === undefined
1888
+ ? undefined
1889
+ : effectLedgerMirror.logicalOperations.find(
1890
+ ({ operationId }) =>
1891
+ operationId === boundary.logicalOperationId,
1892
+ );
1893
+ if (operation === undefined) continue;
1894
+ operationIds.add(operation.operationId);
1895
+ for (const memberId of operation.boundaryIds) {
1896
+ boundaryIds.delete(memberId);
1897
+ }
1898
+ }
1899
+ const ordered = [
1900
+ ...[...boundaryIds].map((boundaryId) => ({
1901
+ order:
1902
+ effectLedgerMirror.boundaries.find(
1903
+ (candidate) => candidate.boundaryId === boundaryId,
1904
+ )?.sequence ?? Number.MAX_SAFE_INTEGER,
1905
+ value: { kind: 'boundary' as const, boundaryId },
1906
+ })),
1907
+ ...[...operationIds].map((operationId) => {
1908
+ const operation = effectLedgerMirror.logicalOperations.find(
1909
+ (candidate) => candidate.operationId === operationId,
1910
+ );
1911
+ return {
1912
+ order:
1913
+ effectLedgerMirror.boundaries.find(
1914
+ ({ boundaryId }) =>
1915
+ boundaryId === operation?.boundaryIds[0],
1916
+ )?.sequence ?? Number.MAX_SAFE_INTEGER,
1917
+ value: { kind: 'logical-operation' as const, operationId },
1918
+ };
1919
+ }),
1920
+ ].sort((left, right) => left.order - right.order);
1921
+ return deepFreeze(
1922
+ snapshotJsonValue(
1923
+ ordered.map(({ value }) => value),
1924
+ 'DECIDE unresolved effect envelope identities',
1925
+ ) as unknown as (
1926
+ | { readonly kind: 'boundary'; readonly boundaryId: string }
1927
+ | { readonly kind: 'logical-operation'; readonly operationId: string }
1928
+ )[],
1929
+ );
1930
+ };
1105
1931
  const stateIdentity = (state: PlaybookState): { stateId?: string } => {
1106
1932
  return state.stateId === undefined ? {} : { stateId: state.stateId };
1107
1933
  };
@@ -1112,11 +1938,25 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1112
1938
  describedEmission?: (runtimePorts: PlaybookPorts) => Promise<void>,
1113
1939
  aborts?: AbortReasonClassifier,
1114
1940
  ): Promise<void> => {
1115
- const runtimePorts = requirePorts();
1941
+ const trace = createTraceEvent(type, payload, meta);
1942
+ return enqueue(
1943
+ async () => {
1944
+ const runtimePorts = requirePorts();
1945
+ await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
1946
+ await describedEmission?.(runtimePorts);
1947
+ },
1948
+ aborts,
1949
+ );
1950
+ };
1951
+ const createTraceEvent = (
1952
+ type: PlaybookTraceType,
1953
+ payload: unknown,
1954
+ meta: { turnId?: number; callId?: string } = {},
1955
+ ): PlaybookTraceEvent => {
1116
1956
  const identity = requireSessionIdentity();
1117
1957
  const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
1118
- const trace: PlaybookTraceEvent = Object.freeze({
1119
- schemaVersion: 3,
1958
+ return Object.freeze({
1959
+ schemaVersion: 4,
1120
1960
  sessionId: identity.sessionId,
1121
1961
  playbookId: identity.playbookId,
1122
1962
  rootSessionId: identity.rootSessionId,
@@ -1134,13 +1974,6 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1134
1974
  ...(meta.callId !== undefined ? { callId: meta.callId } : {}),
1135
1975
  payload: jsonPayload,
1136
1976
  });
1137
- return enqueue(
1138
- async () => {
1139
- await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
1140
- await describedEmission?.(runtimePorts);
1141
- },
1142
- aborts,
1143
- );
1144
1977
  };
1145
1978
  const emitTrace = (
1146
1979
  type: PlaybookTraceType,
@@ -1148,19 +1981,51 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1148
1981
  meta: { turnId?: number; callId?: string } = {},
1149
1982
  aborts?: AbortReasonClassifier,
1150
1983
  ): Promise<void> => enqueueTracedEmission(type, payload, meta, undefined, aborts);
1151
- const emitBoundaryStatus = async (
1152
- message: string,
1984
+ const enqueueAcceptedOutcomeEmission = (
1985
+ acceptedOutcome: AcceptedOutcomeReceipt,
1153
1986
  state: PlaybookState,
1987
+ aborts?: AbortReasonClassifier,
1154
1988
  ): Promise<void> => {
1155
- const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
1156
- STATUS_STATE_IDS.has(stateId),
1989
+ const message = `→ ${acceptedOutcome.acceptedOutcome}`;
1990
+ const acceptedTrace = createTraceEvent(
1991
+ 'outcome.accepted',
1992
+ acceptedOutcome,
1993
+ { turnId: currentTurnId },
1157
1994
  );
1158
- await enqueueTracedEmission(
1995
+ const statusTrace = createTraceEvent(
1159
1996
  'status.emitted',
1160
- {
1161
- ...(bossRelevantStateIds.length === 1
1162
- ? { stateId: bossRelevantStateIds[0] }
1163
- : {}),
1997
+ { stateId: acceptedOutcome.target, message, state },
1998
+ { turnId: currentTurnId },
1999
+ );
2000
+ return enqueue(
2001
+ async () => {
2002
+ const runtimePorts = requirePorts();
2003
+ await runtimePorts.emitTelemetry({
2004
+ topic: TRACE_TOPIC,
2005
+ payload: acceptedTrace,
2006
+ });
2007
+ await runtimePorts.emitTelemetry({
2008
+ topic: TRACE_TOPIC,
2009
+ payload: statusTrace,
2010
+ });
2011
+ await runtimePorts.emitStatus(message);
2012
+ },
2013
+ aborts,
2014
+ );
2015
+ };
2016
+ const emitBoundaryStatus = async (
2017
+ message: string,
2018
+ state: PlaybookState,
2019
+ ): Promise<void> => {
2020
+ const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
2021
+ STATUS_STATE_IDS.has(stateId),
2022
+ );
2023
+ await enqueueTracedEmission(
2024
+ 'status.emitted',
2025
+ {
2026
+ ...(bossRelevantStateIds.length === 1
2027
+ ? { stateId: bossRelevantStateIds[0] }
2028
+ : {}),
1164
2029
  message,
1165
2030
  state,
1166
2031
  },
@@ -1287,11 +2152,11 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1287
2152
  const runPlayerCall = async (
1288
2153
  input: PlayerInput,
1289
2154
  signal: AbortSignal,
1290
- ): Promise<{
1291
- roleId: RoleId;
1292
- playerId?: string;
1293
- result: PlayerResult;
1294
- }> => {
2155
+ continuation?: {
2156
+ readonly resume?: string | false;
2157
+ readonly callId: string;
2158
+ },
2159
+ ): Promise<DeferredContinuationEnvelope> => {
1295
2160
  const aborts = abortReasonClassifier(signal);
1296
2161
  if (!ROLE_ID_SET.has(input.role)) {
1297
2162
  throw new TypeError(
@@ -1305,12 +2170,17 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1305
2170
  let resume: PlayerCallOptions['resume'];
1306
2171
  try {
1307
2172
  signal.throwIfAborted();
1308
- resume = selectPlayerResume(roleId, playerId);
2173
+ resume =
2174
+ continuation !== undefined &&
2175
+ Object.prototype.hasOwnProperty.call(continuation, 'resume')
2176
+ ? continuation.resume!
2177
+ : selectPlayerResume(roleId, playerId);
1309
2178
  } catch (error) {
1310
2179
  latchControlPlaneError(error, signal);
1311
2180
  throw error;
1312
2181
  }
1313
- const callId = `player-${++playerCallSequence}`;
2182
+ const callId =
2183
+ continuation?.callId ?? `player-${++playerCallSequence}`;
1314
2184
  const identity = {
1315
2185
  stateId: input.stateId,
1316
2186
  sourceItem: input.sourceItem,
@@ -1434,6 +2304,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1434
2304
  return {
1435
2305
  roleId,
1436
2306
  ...(playerId === undefined ? {} : { playerId }),
2307
+ callId,
1437
2308
  result,
1438
2309
  };
1439
2310
  } finally {
@@ -1441,15 +2312,862 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1441
2312
  }
1442
2313
  };
1443
2314
 
2315
+ const governedBoundarySeed = (
2316
+ input: PlayerInput,
2317
+ callId: string,
2318
+ ): DecideEffectBoundarySeed => {
2319
+ const outcomes = governedOutcomesFor(input);
2320
+ if (
2321
+ currentTurnId === undefined ||
2322
+ !Number.isSafeInteger(currentTurnId) ||
2323
+ currentTurnId <= 0
2324
+ ) {
2325
+ throw new Error(
2326
+ 'DECIDE governed player call requires an active positive turn id',
2327
+ );
2328
+ }
2329
+ return {
2330
+ boundaryId: randomUUID(),
2331
+ runtimeSessionId: requireSessionIdentity().sessionId,
2332
+ turnId: currentTurnId,
2333
+ callId,
2334
+ roleId: input.role,
2335
+ sourceStateId: input.stateId,
2336
+ sourceOutcomeSchema: snapshotJsonValue(
2337
+ input.result,
2338
+ 'DECIDE governed player source outcome schema',
2339
+ ),
2340
+ dispositions: [
2341
+ ...new Set(
2342
+ Object.values(outcomes).map(
2343
+ ({ repositoryDisposition }) => repositoryDisposition,
2344
+ ),
2345
+ ),
2346
+ ],
2347
+ correctionBudget: { limit: 1, spent: false },
2348
+ };
2349
+ };
2350
+
2351
+ const unresolvedGovernedEvidence = (
2352
+ boundaryId: string,
2353
+ reason: string,
2354
+ error?: unknown,
2355
+ ): void => {
2356
+ governedFailuresByBoundaryId.set(
2357
+ boundaryId,
2358
+ error instanceof Error
2359
+ ? error
2360
+ : new Error(`DECIDE governed outcome remains unresolved: ${reason}`),
2361
+ );
2362
+ };
2363
+
2364
+ const spendSemanticCorrectionBudget = async (
2365
+ boundary: PlaybookEffectBoundary,
2366
+ receipt: PlaybookRepositoryReceipt,
2367
+ finalText: string,
2368
+ semanticCandidate: JsonValue | undefined,
2369
+ ): Promise<boolean> => {
2370
+ const ledger = readEffectLedger();
2371
+ const current = ledger.boundaries.find(
2372
+ ({ boundaryId }) => boundaryId === boundary.boundaryId,
2373
+ );
2374
+ if (
2375
+ current === undefined ||
2376
+ current.correctionBudget.limit !== 1 ||
2377
+ current.correctionBudget.spent
2378
+ ) {
2379
+ return false;
2380
+ }
2381
+ if (current.finalText !== undefined && current.finalText !== finalText) {
2382
+ throw new TypeError(
2383
+ 'DECIDE correction budget conflicts with retained finalText',
2384
+ );
2385
+ }
2386
+ if (
2387
+ current.physicalReceipt !== undefined &&
2388
+ stableJson(current.physicalReceipt, 'DECIDE retained receipt') !==
2389
+ stableJson(receipt, 'DECIDE correction receipt')
2390
+ ) {
2391
+ throw new TypeError(
2392
+ 'DECIDE correction budget conflicts with retained repository receipt',
2393
+ );
2394
+ }
2395
+ if (
2396
+ semanticCandidate !== undefined &&
2397
+ current.semanticCandidate !== undefined &&
2398
+ stableJson(current.semanticCandidate, 'DECIDE retained candidate') !==
2399
+ stableJson(semanticCandidate, 'DECIDE correction candidate')
2400
+ ) {
2401
+ throw new TypeError(
2402
+ 'DECIDE correction budget conflicts with retained semantic candidate',
2403
+ );
2404
+ }
2405
+ const next: PlaybookEffectBoundary = {
2406
+ ...current,
2407
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
2408
+ physicalReceipt: receipt,
2409
+ finalText,
2410
+ ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
2411
+ correctionBudget: { limit: 1, spent: true },
2412
+ };
2413
+ const cohortMembers =
2414
+ current.cohortId === undefined
2415
+ ? [current]
2416
+ : ledger.boundaries.filter(
2417
+ ({ cohortId }) => cohortId === current.cohortId,
2418
+ );
2419
+ if (cohortMembers.length === 0) {
2420
+ throw new TypeError(
2421
+ 'DECIDE correction boundary lost its repository cohort',
2422
+ );
2423
+ }
2424
+ const replacements = cohortMembers.map((member) => ({
2425
+ expected: member,
2426
+ next:
2427
+ member.boundaryId === current.boundaryId
2428
+ ? next
2429
+ : {
2430
+ ...member,
2431
+ ...(receipt.after === undefined
2432
+ ? {}
2433
+ : { after: receipt.after }),
2434
+ physicalReceipt: receipt,
2435
+ },
2436
+ })) as [
2437
+ {
2438
+ readonly expected: PlaybookEffectBoundary;
2439
+ readonly next: PlaybookEffectBoundary;
2440
+ },
2441
+ ...{
2442
+ readonly expected: PlaybookEffectBoundary;
2443
+ readonly next: PlaybookEffectBoundary;
2444
+ }[],
2445
+ ];
2446
+ const acknowledged = assertPlaybookEffectLedger(
2447
+ await deferredEffects.effectLedger.writeAhead([
2448
+ {
2449
+ kind: 'replace-boundaries',
2450
+ // A cohort's shared receipt must become visible for every member in
2451
+ // one ledger revision. Publishing only this member's correction
2452
+ // spend would transiently create an invalid half-complete cohort.
2453
+ replacements,
2454
+ },
2455
+ ]),
2456
+ 'DECIDE semantic correction budget acknowledgement',
2457
+ );
2458
+ synchronizeDeferredProjection(acknowledged);
2459
+ const spent = acknowledged.boundaries.find(
2460
+ ({ boundaryId }) => boundaryId === boundary.boundaryId,
2461
+ );
2462
+ if (
2463
+ spent === undefined ||
2464
+ stableJson(spent, 'DECIDE acknowledged correction boundary') !==
2465
+ stableJson(next, 'DECIDE expected correction boundary')
2466
+ ) {
2467
+ throw new TypeError(
2468
+ 'DECIDE semantic correction budget spend was not acknowledged exactly',
2469
+ );
2470
+ }
2471
+ return true;
2472
+ };
2473
+
2474
+ const completionEvidenceFor = (
2475
+ input: PlayerInput,
2476
+ roleId: RoleId,
2477
+ playerId: string | undefined,
2478
+ signal: AbortSignal,
2479
+ operationId?: string,
2480
+ ) =>
2481
+ async (
2482
+ completion: DecideRepositoryCompletion<PlayerResult>,
2483
+ ): Promise<DecideRepositoryCompletionEvidence> => {
2484
+ const queued = await semanticCompletionQueue.add(async () => {
2485
+ const { boundary, operation } = completion;
2486
+ const outcomes = governedOutcomesFor(input);
2487
+ const expectedDispositions = [
2488
+ ...new Set(
2489
+ Object.values(outcomes).map(
2490
+ ({ repositoryDisposition }) => repositoryDisposition,
2491
+ ),
2492
+ ),
2493
+ ];
2494
+ const session = requireSessionIdentity();
2495
+ if (
2496
+ boundary.playbookId !== session.playbookId ||
2497
+ boundary.runtimeSessionId !== session.sessionId ||
2498
+ boundary.turnId !== currentTurnId ||
2499
+ boundary.roleId !== roleId ||
2500
+ (completion.roleId !== undefined && completion.roleId !== roleId) ||
2501
+ boundary.sourceStateId !== input.stateId ||
2502
+ stableJson(
2503
+ boundary.dispositions,
2504
+ 'DECIDE boundary dispositions',
2505
+ ) !==
2506
+ stableJson(
2507
+ expectedDispositions,
2508
+ 'DECIDE authority dispositions',
2509
+ ) ||
2510
+ stableJson(
2511
+ boundary.sourceOutcomeSchema,
2512
+ 'DECIDE boundary source schema',
2513
+ ) !==
2514
+ stableJson(input.result, 'DECIDE authored source schema')
2515
+ ) {
2516
+ throw new TypeError(
2517
+ 'DECIDE governed semantic reconciliation source schema changed',
2518
+ );
2519
+ }
2520
+ governedReceiptsByBoundaryId.set(boundary.boundaryId, {
2521
+ physicalReceipt: completion.receipt,
2522
+ outcomeReceipt: completion.outcomeReceipt,
2523
+ });
2524
+ if (
2525
+ operation.status !== 'fulfilled' ||
2526
+ operation.value.status !== 'ok' ||
2527
+ isEmptyFinalText(operation.value.finalText)
2528
+ ) {
2529
+ const incomplete =
2530
+ operation.status === 'fulfilled' &&
2531
+ operation.value.status === 'ok' &&
2532
+ operation.value.finalText !== undefined
2533
+ ? { finalText: operation.value.finalText }
2534
+ : {};
2535
+ if (
2536
+ operation.status === 'fulfilled' &&
2537
+ operation.value.status === 'ok' &&
2538
+ operation.value.finalText !== undefined
2539
+ ) {
2540
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
2541
+ finalText: operation.value.finalText,
2542
+ });
2543
+ }
2544
+ return operationId === undefined
2545
+ ? incomplete
2546
+ : { ...incomplete, unresolved: true as const };
2547
+ }
2548
+ const finalText = operation.value.finalText!;
2549
+ let raw: string;
2550
+ try {
2551
+ raw = await callJudge(
2552
+ buildAdjudicatorPrompt(input, finalText),
2553
+ signal,
2554
+ 'player-output-adjudication',
2555
+ input.stateId,
2556
+ );
2557
+ } catch (error) {
2558
+ unresolvedGovernedEvidence(
2559
+ boundary.boundaryId,
2560
+ 'judge transport failed',
2561
+ error,
2562
+ );
2563
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, { finalText });
2564
+ return { finalText, unresolved: true as const };
2565
+ }
2566
+
2567
+ let candidate: unknown;
2568
+ let retainedCandidate: JsonValue | undefined;
2569
+ let reconciliation:
2570
+ | ReturnType<typeof reconcilePlaybookSemanticEvidence>
2571
+ | undefined;
2572
+ let structuralError:
2573
+ | PlaybookSemanticCandidateStructureError
2574
+ | undefined;
2575
+ const retainCandidate = (value: unknown): void => {
2576
+ try {
2577
+ retainedCandidate = snapshotJsonValue(
2578
+ value,
2579
+ 'DECIDE recoverable governed semantic candidate',
2580
+ );
2581
+ } catch {
2582
+ // A non-detachable reply still retains presentation and receipt.
2583
+ }
2584
+ };
2585
+ try {
2586
+ candidate = parseGovernedSemanticCandidate(raw);
2587
+ retainCandidate(candidate);
2588
+ reconciliation = reconcilePlaybookSemanticEvidence({
2589
+ outcomes,
2590
+ semanticCandidate: candidate,
2591
+ finalText,
2592
+ receipt: completion.outcomeReceipt,
2593
+ });
2594
+ } catch (error) {
2595
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
2596
+ throw error;
2597
+ }
2598
+ structuralError = error;
2599
+ }
2600
+
2601
+ if (structuralError !== undefined) {
2602
+ const spent = await spendSemanticCorrectionBudget(
2603
+ boundary,
2604
+ completion.receipt,
2605
+ finalText,
2606
+ retainedCandidate,
2607
+ );
2608
+ if (!spent || signal.aborted) {
2609
+ unresolvedGovernedEvidence(
2610
+ boundary.boundaryId,
2611
+ 'semantic correction budget is unavailable',
2612
+ signal.aborted ? signal.reason : undefined,
2613
+ );
2614
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
2615
+ finalText,
2616
+ ...(retainedCandidate === undefined
2617
+ ? {}
2618
+ : { semanticCandidate: retainedCandidate }),
2619
+ });
2620
+ return {
2621
+ finalText,
2622
+ ...(retainedCandidate === undefined
2623
+ ? {}
2624
+ : { semanticCandidate: retainedCandidate }),
2625
+ unresolved: true as const,
2626
+ };
2627
+ }
2628
+ let correctiveRaw: string;
2629
+ try {
2630
+ correctiveRaw = await callJudge(
2631
+ buildAdjudicatorPrompt(input, finalText, {
2632
+ reply: raw,
2633
+ error: structuralError.message,
2634
+ }),
2635
+ signal,
2636
+ 'player-output-adjudication',
2637
+ input.stateId,
2638
+ );
2639
+ } catch (error) {
2640
+ unresolvedGovernedEvidence(
2641
+ boundary.boundaryId,
2642
+ 'corrective judge failed',
2643
+ error,
2644
+ );
2645
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
2646
+ finalText,
2647
+ ...(retainedCandidate === undefined
2648
+ ? {}
2649
+ : { semanticCandidate: retainedCandidate }),
2650
+ });
2651
+ return {
2652
+ finalText,
2653
+ ...(retainedCandidate === undefined
2654
+ ? {}
2655
+ : { semanticCandidate: retainedCandidate }),
2656
+ unresolved: true as const,
2657
+ };
2658
+ }
2659
+ try {
2660
+ candidate = parseGovernedSemanticCandidate(correctiveRaw);
2661
+ retainCandidate(candidate);
2662
+ reconciliation = reconcilePlaybookSemanticEvidence({
2663
+ outcomes,
2664
+ semanticCandidate: candidate,
2665
+ finalText,
2666
+ receipt: completion.outcomeReceipt,
2667
+ });
2668
+ } catch (error) {
2669
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
2670
+ throw error;
2671
+ }
2672
+ unresolvedGovernedEvidence(
2673
+ boundary.boundaryId,
2674
+ 'corrective semantic candidate is invalid',
2675
+ );
2676
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
2677
+ finalText,
2678
+ ...(retainedCandidate === undefined
2679
+ ? {}
2680
+ : { semanticCandidate: retainedCandidate }),
2681
+ });
2682
+ return {
2683
+ finalText,
2684
+ ...(retainedCandidate === undefined
2685
+ ? {}
2686
+ : { semanticCandidate: retainedCandidate }),
2687
+ unresolved: true as const,
2688
+ };
2689
+ }
2690
+ }
2691
+
2692
+ if (reconciliation === undefined) {
2693
+ throw new Error(
2694
+ 'DECIDE semantic reconciliation produced no decision',
2695
+ );
2696
+ }
2697
+ const semanticCandidate = snapshotJsonValue(
2698
+ reconciliation.evidence.semanticCandidate,
2699
+ 'DECIDE governed semantic candidate',
2700
+ );
2701
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
2702
+ finalText,
2703
+ semanticCandidate,
2704
+ });
2705
+ if (reconciliation.status === 'unresolved') {
2706
+ unresolvedGovernedEvidence(
2707
+ boundary.boundaryId,
2708
+ reconciliation.reason,
2709
+ );
2710
+ return { finalText, semanticCandidate, unresolved: true as const };
2711
+ }
2712
+
2713
+ const output = reconciliation.output as unknown as PlayerOutput;
2714
+ governedOutputsByBoundaryId.set(boundary.boundaryId, output);
2715
+ if (reconciliation.status !== 'deferred') {
2716
+ return { finalText, semanticCandidate };
2717
+ }
2718
+ if (
2719
+ output.guard !== 'needsBossReply' ||
2720
+ typeof output.question !== 'string' ||
2721
+ output.question.trim() === ''
2722
+ ) {
2723
+ throw new TypeError(
2724
+ 'DECIDE deferred outcome must carry one exact Boss question',
2725
+ );
2726
+ }
2727
+ const pendingQuestion: PlaybookPendingBossQuestion = {
2728
+ questionId: input.stateId,
2729
+ asker: { kind: 'role', roleId },
2730
+ question: output.question,
2731
+ sourceItem: input.sourceItem,
2732
+ };
2733
+ return {
2734
+ finalText,
2735
+ semanticCandidate,
2736
+ deferred: {
2737
+ operationId: operationId ?? randomUUID(),
2738
+ pendingQuestion,
2739
+ playerContinuation: snapshotJsonValue(
2740
+ selectPlayerResume(roleId, playerId),
2741
+ 'DECIDE deferred player continuation',
2742
+ ),
2743
+ },
2744
+ };
2745
+ });
2746
+ if (queued === undefined) {
2747
+ throw new Error('DECIDE semantic completion produced no evidence');
2748
+ }
2749
+ return queued;
2750
+ };
2751
+
2752
+ const acknowledgeGovernedPlayerResult = (
2753
+ value: DecideRepositoryExclusiveResult<PlayerResult>,
2754
+ boundaryId: string,
2755
+ ): PlayerResult => {
2756
+ const ledger = assertPlaybookEffectLedger(
2757
+ value.effectLedger,
2758
+ 'DECIDE repository settlement effect ledger',
2759
+ );
2760
+ const completed = ledger.boundaries.find(
2761
+ (candidate) => candidate.boundaryId === boundaryId,
2762
+ );
2763
+ if (
2764
+ completed === undefined ||
2765
+ completed.physicalReceipt === undefined ||
2766
+ stableJson(completed.physicalReceipt, 'DECIDE completed receipt') !==
2767
+ stableJson(value.receipt, 'DECIDE acknowledged receipt')
2768
+ ) {
2769
+ throw new TypeError(
2770
+ 'DECIDE repository settlement did not acknowledge its completed boundary',
2771
+ );
2772
+ }
2773
+ const expectedEvidence = governedEvidenceByBoundaryId.get(boundaryId);
2774
+ const expectedReceipts = governedReceiptsByBoundaryId.get(boundaryId);
2775
+ if (
2776
+ expectedReceipts !== undefined &&
2777
+ stableJson(
2778
+ completed.physicalReceipt,
2779
+ 'DECIDE completed physical receipt',
2780
+ ) !==
2781
+ stableJson(
2782
+ expectedReceipts.physicalReceipt,
2783
+ 'DECIDE reconciled physical receipt',
2784
+ )
2785
+ ) {
2786
+ throw new TypeError(
2787
+ 'DECIDE repository settlement changed the physical receipt used during reconciliation',
2788
+ );
2789
+ }
2790
+ if (
2791
+ expectedEvidence !== undefined &&
2792
+ (completed.finalText !== expectedEvidence.finalText ||
2793
+ (expectedEvidence.semanticCandidate === undefined
2794
+ ? completed.semanticCandidate !== undefined
2795
+ : completed.semanticCandidate === undefined ||
2796
+ stableJson(
2797
+ completed.semanticCandidate,
2798
+ 'DECIDE completed semantic candidate',
2799
+ ) !==
2800
+ stableJson(
2801
+ expectedEvidence.semanticCandidate,
2802
+ 'DECIDE expected semantic candidate',
2803
+ )))
2804
+ ) {
2805
+ throw new TypeError(
2806
+ 'DECIDE repository settlement did not acknowledge its exact governed evidence',
2807
+ );
2808
+ }
2809
+ governedEvidenceByBoundaryId.delete(boundaryId);
2810
+ governedReceiptsByBoundaryId.delete(boundaryId);
2811
+ synchronizeDeferredProjection(ledger);
2812
+ if (value.operation.status === 'rejected') {
2813
+ throw value.operation.reason;
2814
+ }
2815
+ const result = validatePlayerResult(value.operation.value);
2816
+ const output = governedOutputsByBoundaryId.get(boundaryId);
2817
+ const failure = governedFailuresByBoundaryId.get(boundaryId);
2818
+ if (expectedReceipts !== undefined) {
2819
+ const continued =
2820
+ 'status' in value && value.status === 'continued'
2821
+ ? (value as DecideRepositoryDeferredContinuationResult<PlayerResult>)
2822
+ : undefined;
2823
+ const acknowledgedOutcomeReceipt =
2824
+ continued === undefined ? value.receipt : continued.logicalReceipt;
2825
+ if (
2826
+ acknowledgedOutcomeReceipt !== undefined &&
2827
+ stableJson(
2828
+ acknowledgedOutcomeReceipt,
2829
+ 'DECIDE acknowledged outcome receipt',
2830
+ ) !==
2831
+ stableJson(
2832
+ expectedReceipts.outcomeReceipt,
2833
+ 'DECIDE reconciled outcome receipt',
2834
+ )
2835
+ ) {
2836
+ throw new TypeError(
2837
+ 'DECIDE repository settlement changed the outcome receipt used during reconciliation',
2838
+ );
2839
+ }
2840
+ if (
2841
+ continued !== undefined &&
2842
+ output !== undefined &&
2843
+ output.guard !== 'needsBossReply' &&
2844
+ acknowledgedOutcomeReceipt === undefined
2845
+ ) {
2846
+ throw new TypeError(
2847
+ 'DECIDE completed deferred continuation omitted its reconciled logical receipt',
2848
+ );
2849
+ }
2850
+ }
2851
+ governedOutputsByBoundaryId.delete(boundaryId);
2852
+ governedFailuresByBoundaryId.delete(boundaryId);
2853
+ const linkedOperationId = completed.logicalOperationId;
2854
+ if (value.deferredStatus === 'unresolved') {
2855
+ if (linkedOperationId === undefined) {
2856
+ throw new TypeError(
2857
+ 'DECIDE unresolved deferred settlement omitted its logical operation',
2858
+ );
2859
+ }
2860
+ deferredOperationId = linkedOperationId;
2861
+ hiddenDeferredOperationId = linkedOperationId;
2862
+ } else if (value.deferredStatus === 'bound') {
2863
+ if (linkedOperationId === undefined) {
2864
+ throw new TypeError(
2865
+ 'DECIDE bound deferred settlement omitted its logical operation',
2866
+ );
2867
+ }
2868
+ deferredOperationId = linkedOperationId;
2869
+ hiddenDeferredOperationId = undefined;
2870
+ } else if (
2871
+ 'status' in value &&
2872
+ value.status === 'continued' &&
2873
+ (value as { readonly logicalReceipt?: PlaybookRepositoryReceipt })
2874
+ .logicalReceipt === undefined &&
2875
+ linkedOperationId !== undefined
2876
+ ) {
2877
+ deferredOperationId = linkedOperationId;
2878
+ hiddenDeferredOperationId = linkedOperationId;
2879
+ } else if (linkedOperationId !== undefined) {
2880
+ deferredOperationId = undefined;
2881
+ hiddenDeferredOperationId = undefined;
2882
+ }
2883
+ if (failure !== undefined) throw failure;
2884
+ if (value.deferredStatus === 'unresolved') {
2885
+ throw new Error(
2886
+ 'DECIDE deferred repository settlement remains unresolved',
2887
+ );
2888
+ }
2889
+ if (output !== undefined) {
2890
+ governedPlayerOutputs.set(result, output);
2891
+ } else if (
2892
+ result.status === 'ok' &&
2893
+ !isEmptyFinalText(result.finalText)
2894
+ ) {
2895
+ throw new Error(
2896
+ 'DECIDE governed player result has no reconciled semantic output',
2897
+ );
2898
+ }
2899
+ return result;
2900
+ };
2901
+
2902
+ const shouldRunProposalCohort = (input: PlayerInput): boolean => {
2903
+ if (
2904
+ input.stateId !== PROPOSAL_STATE_BY_ROLE[input.role] ||
2905
+ completedProposalCohortTurnId === currentTurnId
2906
+ ) {
2907
+ return false;
2908
+ }
2909
+ const state = currentState();
2910
+ return Object.values(PROPOSAL_STATE_BY_ROLE).every((stateId) =>
2911
+ state.activeStateIds.includes(stateId),
2912
+ );
2913
+ };
2914
+
2915
+ const settleProposalCohort = async (): Promise<void> => {
2916
+ const members = Object.fromEntries(
2917
+ ROLE_IDS.map((roleId) => [roleId, pendingProposalCohort.get(roleId)]),
2918
+ ) as Record<RoleId, PendingProposalCohortMember | undefined>;
2919
+ const turnId = currentTurnId;
2920
+ const cohortSignal = currentSignal;
2921
+ const cohortOperations = new AbortController();
2922
+ const completionOrder: RoleId[] = [];
2923
+ try {
2924
+ if (
2925
+ turnId === undefined ||
2926
+ cohortSignal === undefined ||
2927
+ members.coder === undefined ||
2928
+ members.reviewer === undefined
2929
+ ) {
2930
+ throw new Error(
2931
+ 'DECIDE proposal cohort started without both governed members',
2932
+ );
2933
+ }
2934
+ for (const roleId of ROLE_IDS) members[roleId]!.signal.throwIfAborted();
2935
+ const operations = Object.fromEntries(
2936
+ ROLE_IDS.map((roleId) => [
2937
+ roleId,
2938
+ async () => {
2939
+ const member = members[roleId]!;
2940
+ try {
2941
+ const operationSignal = combineSignals(
2942
+ member.signal,
2943
+ cohortOperations.signal,
2944
+ );
2945
+ const envelope = await runPlayerCall(
2946
+ member.input,
2947
+ operationSignal,
2948
+ { callId: member.callId },
2949
+ );
2950
+ member.envelope = envelope;
2951
+ if (
2952
+ envelope.result.status !== 'ok' &&
2953
+ !cohortOperations.signal.aborted
2954
+ ) {
2955
+ cohortOperations.abort(
2956
+ new Error(
2957
+ `DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`,
2958
+ ),
2959
+ );
2960
+ }
2961
+ return envelope.result;
2962
+ } catch (error) {
2963
+ if (!cohortOperations.signal.aborted) {
2964
+ cohortOperations.abort(error);
2965
+ }
2966
+ throw error;
2967
+ } finally {
2968
+ completionOrder.push(roleId);
2969
+ }
2970
+ },
2971
+ ]),
2972
+ ) as unknown as Readonly<
2973
+ Record<RoleId, () => Promise<PlayerResult>>
2974
+ >;
2975
+ const invocationId = randomUUID();
2976
+ const settled = await deferredEffects.repository.runCohort({
2977
+ signal: cohortSignal,
2978
+ invocationId,
2979
+ roleIds: ROLE_IDS,
2980
+ dispositionsByRole: {
2981
+ coder: ['unchanged'],
2982
+ reviewer: ['unchanged'],
2983
+ },
2984
+ effectBoundaries: {
2985
+ coder: members.coder.effectBoundary,
2986
+ reviewer: members.reviewer.effectBoundary,
2987
+ },
2988
+ operations,
2989
+ completeEffectBoundary: (completion) => {
2990
+ const member = members[completion.roleId];
2991
+ if (member === undefined) {
2992
+ throw new TypeError(
2993
+ `DECIDE proposal cohort completed unknown role ${String(completion.roleId)}`,
2994
+ );
2995
+ }
2996
+ return completionEvidenceFor(
2997
+ member.input,
2998
+ completion.roleId,
2999
+ resolvedPlayerId(completion.roleId),
3000
+ member.signal,
3001
+ )(completion);
3002
+ },
3003
+ });
3004
+ if (settled.invocationId !== invocationId) {
3005
+ throw new TypeError(
3006
+ 'DECIDE repository cohort changed its invocation identity',
3007
+ );
3008
+ }
3009
+ const releaseOrder = [
3010
+ ...completionOrder,
3011
+ ...ROLE_IDS.filter((roleId) => !completionOrder.includes(roleId)),
3012
+ ];
3013
+ const acknowledgedResults = new Map<RoleId, PlayerResult>();
3014
+ const acknowledgementFailures: unknown[] = [];
3015
+ for (const roleId of releaseOrder) {
3016
+ const member = members[roleId]!;
3017
+ try {
3018
+ const result = acknowledgeGovernedPlayerResult(
3019
+ {
3020
+ operation: settled.operations[roleId],
3021
+ receipt: settled.receipts[roleId],
3022
+ effectLedger: settled.effectLedger,
3023
+ },
3024
+ member.effectBoundary.boundaryId,
3025
+ );
3026
+ if (member.envelope === undefined) {
3027
+ throw new TypeError(
3028
+ `DECIDE repository cohort omitted its ${roleId} invocation`,
3029
+ );
3030
+ }
3031
+ acknowledgedResults.set(roleId, result);
3032
+ } catch (error) {
3033
+ acknowledgementFailures.push(error);
3034
+ }
3035
+ }
3036
+ if (acknowledgementFailures.length > 0) {
3037
+ for (const result of acknowledgedResults.values()) {
3038
+ governedPlayerOutputs.delete(result);
3039
+ }
3040
+ throw acknowledgementFailures.length === 1
3041
+ ? acknowledgementFailures[0]
3042
+ : new AggregateError(
3043
+ acknowledgementFailures,
3044
+ 'DECIDE proposal cohort reconciliation failed',
3045
+ );
3046
+ }
3047
+ for (const roleId of releaseOrder) {
3048
+ const member = members[roleId]!;
3049
+ const result = acknowledgedResults.get(roleId);
3050
+ if (member.envelope === undefined || result === undefined) {
3051
+ throw new Error(
3052
+ `DECIDE proposal cohort lost its ${roleId} acknowledgement`,
3053
+ );
3054
+ }
3055
+ member.result.resolve({ ...member.envelope, result });
3056
+ }
3057
+ } catch (error) {
3058
+ for (const member of Object.values(members)) member?.result.reject(error);
3059
+ } finally {
3060
+ completedProposalCohortTurnId = turnId;
3061
+ pendingProposalCohort.clear();
3062
+ activeProposalCohort = undefined;
3063
+ }
3064
+ };
3065
+
3066
+ const queueProposalCohortMember = (
3067
+ input: PlayerInput,
3068
+ signal: AbortSignal,
3069
+ ): Promise<DeferredContinuationEnvelope> => {
3070
+ if (pendingProposalCohort.has(input.role)) {
3071
+ throw new Error(
3072
+ `DECIDE proposal cohort already registered role ${input.role}`,
3073
+ );
3074
+ }
3075
+ const callId = `player-${++playerCallSequence}`;
3076
+ const member: PendingProposalCohortMember = {
3077
+ input,
3078
+ signal,
3079
+ callId,
3080
+ effectBoundary: governedBoundarySeed(input, callId),
3081
+ result: deferredValue<DeferredContinuationEnvelope>(),
3082
+ };
3083
+ pendingProposalCohort.set(input.role, member);
3084
+ if (pendingProposalCohort.size === ROLE_IDS.length) {
3085
+ if (activeProposalCohort !== undefined) {
3086
+ throw new Error('DECIDE proposal cohort was started more than once');
3087
+ }
3088
+ activeProposalCohort = settleProposalCohort();
3089
+ }
3090
+ return member.result.promise;
3091
+ };
3092
+
1444
3093
  const callPlayer = (
1445
3094
  input: PlayerInput,
1446
3095
  signal: AbortSignal,
1447
- ): Promise<{
1448
- roleId: RoleId;
1449
- playerId?: string;
1450
- result: PlayerResult;
1451
- }> => {
1452
- return trackBoundaryCall(runPlayerCall(input, signal));
3096
+ ): Promise<DeferredContinuationEnvelope> => {
3097
+ const invocation = async (): Promise<DeferredContinuationEnvelope> => {
3098
+ const active = activeDeferredContinuation;
3099
+ if (active === undefined && hasUnresolvedReconciliation()) {
3100
+ throw new Error(
3101
+ 'DECIDE governed semantic reconciliation remains unresolved',
3102
+ );
3103
+ }
3104
+ if (active !== undefined) {
3105
+ if (
3106
+ active.effectBoundary.runtimeSessionId !==
3107
+ requireSessionIdentity().sessionId ||
3108
+ active.effectBoundary.turnId !== currentTurnId ||
3109
+ active.effectBoundary.roleId !== input.role ||
3110
+ active.effectBoundary.sourceStateId !== input.stateId ||
3111
+ active.playerContinuation === undefined
3112
+ ) {
3113
+ throw new TypeError(
3114
+ 'DECIDE deferred continuation did not invoke its bound player boundary',
3115
+ );
3116
+ }
3117
+ active.input = input;
3118
+ active.playerId = resolvedPlayerId(input.role);
3119
+ try {
3120
+ const envelope = await runPlayerCall(input, signal, {
3121
+ callId: active.effectBoundary.callId,
3122
+ resume: active.playerContinuation,
3123
+ });
3124
+ active.result.resolve(envelope.result);
3125
+ const acknowledgedResult = await active.acknowledged.promise;
3126
+ return { ...envelope, result: acknowledgedResult };
3127
+ } catch (error) {
3128
+ active.result.reject(error);
3129
+ try {
3130
+ await active.acknowledged.promise;
3131
+ } catch (acknowledgementError) {
3132
+ throw acknowledgementError;
3133
+ }
3134
+ throw error;
3135
+ }
3136
+ }
3137
+
3138
+ if (shouldRunProposalCohort(input)) {
3139
+ return queueProposalCohortMember(input, signal);
3140
+ }
3141
+
3142
+ const callId = `player-${++playerCallSequence}`;
3143
+ const effectBoundary = governedBoundarySeed(input, callId);
3144
+ let envelope: DeferredContinuationEnvelope | undefined;
3145
+ const settled = await deferredEffects.repository.runExclusive({
3146
+ signal,
3147
+ effectBoundary,
3148
+ operation: async () => {
3149
+ envelope = await runPlayerCall(input, signal, { callId });
3150
+ return envelope.result;
3151
+ },
3152
+ completeEffectBoundary: completionEvidenceFor(
3153
+ input,
3154
+ input.role,
3155
+ resolvedPlayerId(input.role),
3156
+ signal,
3157
+ ),
3158
+ });
3159
+ const result = acknowledgeGovernedPlayerResult(
3160
+ settled,
3161
+ effectBoundary.boundaryId,
3162
+ );
3163
+ if (envelope === undefined) {
3164
+ throw new TypeError(
3165
+ 'DECIDE repository settlement omitted its player invocation',
3166
+ );
3167
+ }
3168
+ return { ...envelope, result };
3169
+ };
3170
+ return trackBoundaryCall(invocation());
1453
3171
  };
1454
3172
 
1455
3173
  const player = fromPromise<PlayerOutput, PlayerInput>(
@@ -1469,8 +3187,18 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1469
3187
  }
1470
3188
  combined.throwIfAborted();
1471
3189
 
1472
- let { roleId, playerId, result } = await callPlayer(input, combined);
1473
- if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
3190
+ let { roleId, playerId, callId, result } = await callPlayer(
3191
+ input,
3192
+ combined,
3193
+ );
3194
+ if (
3195
+ result.status === 'ok' &&
3196
+ isEmptyFinalText(result.finalText) &&
3197
+ automaticReplayPolicy.allowsEmptyOkCorrection(
3198
+ requireSessionIdentity().sessionId,
3199
+ callId,
3200
+ )
3201
+ ) {
1474
3202
  // DR-028: an `ok` result whose finalText is missing, empty, or
1475
3203
  // whitespace-only earns exactly one corrective re-ask — the same
1476
3204
  // composed call repeated, traced by runPlayerCall as its own
@@ -1480,7 +3208,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1480
3208
  // are never retried), and a rejecting finish emission rejects
1481
3209
  // `callPlayer` itself, so it never reaches this branch (PBRT-47).
1482
3210
  combined.throwIfAborted();
1483
- ({ roleId, playerId, result } = await callPlayer(input, combined));
3211
+ ({ roleId, playerId, callId, result } = await callPlayer(
3212
+ input,
3213
+ combined,
3214
+ ));
1484
3215
  }
1485
3216
  if (result.status !== 'ok') {
1486
3217
  throw new Error(
@@ -1501,22 +3232,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1501
3232
  }
1502
3233
  combined.throwIfAborted();
1503
3234
 
1504
- try {
1505
- const prompt = buildAdjudicatorPrompt(input, finalText);
1506
- return parseAdjudication(
1507
- await callJudge(
1508
- prompt,
1509
- combined,
1510
- 'player-output-adjudication',
1511
- input.stateId,
1512
- ),
1513
- input,
1514
- finalText,
1515
- );
1516
- } catch (error) {
1517
- latchControlPlaneError(error, combined);
1518
- throw error;
3235
+ const governedOutput = governedPlayerOutputs.get(result);
3236
+ if (governedOutput !== undefined) {
3237
+ governedPlayerOutputs.delete(result);
3238
+ return governedOutput;
1519
3239
  }
3240
+ throw new Error(
3241
+ 'DECIDE governed player result was not reconciled against repository evidence',
3242
+ );
1520
3243
  } finally {
1521
3244
  actorSettlementAborts.push(settlementAborts);
1522
3245
  }
@@ -1611,16 +3334,38 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1611
3334
  };
1612
3335
 
1613
3336
  const inspect = (event: InspectionEvent): void => {
1614
- if (event.type !== '@xstate.snapshot') return;
1615
3337
  if (actor === undefined || event.actorRef !== actor) return;
1616
3338
  if (suppressInspectionEmissions) return;
3339
+ if (event.type === '@xstate.action') {
3340
+ try {
3341
+ acceptedOutcomeConsumer.capture(event.action);
3342
+ } catch (error) {
3343
+ latchInspectionError(error);
3344
+ }
3345
+ return;
3346
+ }
3347
+ if (event.type !== '@xstate.snapshot') return;
1617
3348
  const settlementAborts = consumeActorSettlementAborts(true);
1618
3349
  try {
1619
3350
  const snapshot = event.snapshot as SnapshotFrom<typeof decideMachine>;
1620
3351
  const state = normalizePlaybookSnapshot(snapshot);
1621
3352
  const prior = previousState ?? state;
3353
+ let acceptedOutcomes: readonly AcceptedOutcomeReceipt[] = [];
3354
+ try {
3355
+ acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
3356
+ } catch (error) {
3357
+ latchInspectionError(error);
3358
+ }
1622
3359
  const context = snapshot.context as unknown as Record<string, unknown>;
1623
- const fsmPayload = telemetryPayload(prior, state, event.event, context);
3360
+ const fsmPayload = telemetryPayload(
3361
+ prior,
3362
+ state,
3363
+ event.event,
3364
+ context,
3365
+ hiddenDeferredOperationId === undefined
3366
+ ? undefined
3367
+ : 'commitCoderProposal',
3368
+ );
1624
3369
  const describedFsmPayload = snapshotJsonValue(
1625
3370
  fsmPayload,
1626
3371
  'described FSM telemetry',
@@ -1640,7 +3385,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1640
3385
 
1641
3386
  const priorIds = new Set(previousState?.activeStateIds ?? []);
1642
3387
  previousState = state;
1643
- const pendingQuestions = pendingQuestionsFromContext(context);
3388
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
1644
3389
  const bossRelevantStateIds = state.activeStateIds.filter((stateId) =>
1645
3390
  STATUS_STATE_IDS.has(stateId),
1646
3391
  );
@@ -1665,6 +3410,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1665
3410
  ).catch(() => undefined);
1666
3411
  };
1667
3412
 
3413
+ for (const acceptedOutcome of acceptedOutcomes) {
3414
+ void enqueueAcceptedOutcomeEmission(
3415
+ acceptedOutcome,
3416
+ state,
3417
+ settlementAborts,
3418
+ ).catch(() => undefined);
3419
+ }
3420
+
1668
3421
  for (const activeStateId of state.activeStateIds) {
1669
3422
  if (
1670
3423
  priorIds.has(activeStateId) ||
@@ -1707,12 +3460,14 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1707
3460
  );
1708
3461
  }
1709
3462
  } catch (error) {
3463
+ acceptedOutcomeConsumer.reset();
1710
3464
  latchInspectionError(error, settlementAborts);
1711
3465
  }
1712
3466
  };
1713
3467
 
1714
3468
  const createRuntimeActor = (machineSnapshot?: JsonValue): void => {
1715
3469
  previousState = undefined;
3470
+ acceptedOutcomeConsumer.reset();
1716
3471
  // DR-014 §1: a restore rehydrates the persisted machine snapshot;
1717
3472
  // XState derives context/value from it and ignores `input` then.
1718
3473
  actor = createActor(providedMachine, {
@@ -1748,6 +3503,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1748
3503
  const stopActor = (): void => {
1749
3504
  if (!actor) return;
1750
3505
  suppressInspectionEmissions = true;
3506
+ acceptedOutcomeConsumer.reset();
1751
3507
  actor.stop();
1752
3508
  };
1753
3509
 
@@ -1770,17 +3526,21 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1770
3526
  ): Promise<DecideEvent | { type: 'NO_ACTION' } | null> => {
1771
3527
  const live = actor;
1772
3528
  if (!live) throw new Error('decide runtime: actor is not initialized');
3529
+ refreshReconciliationProjection();
3530
+ if (hasUnresolvedReconciliation()) return null;
1773
3531
  const snapshot = live.getSnapshot() as SnapshotFrom<typeof decideMachine>;
1774
3532
  const context = snapshot.context as unknown as Record<string, unknown>;
1775
3533
  const state = normalizePlaybookSnapshot(snapshot, {
1776
3534
  pendingCall: nestedBridge.getPendingCall(),
1777
3535
  });
1778
- const pendingQuestions = pendingQuestionsForState(state, context);
3536
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
3537
+ const failed = state.activeStateIds.includes('failed');
1779
3538
  if (
1780
3539
  pendingQuestions.length === 0 &&
1781
3540
  (snapshot.status === 'done' ||
1782
3541
  state.activeStateIds.includes('ready') ||
1783
- state.activeStateIds.includes('failed'))
3542
+ (failed &&
3543
+ automaticReplayPolicy.allowsFailureStateRetry()))
1784
3544
  ) {
1785
3545
  return { type: 'START_DECIDE', callerTopic: text };
1786
3546
  }
@@ -1802,6 +3562,640 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1802
3562
  );
1803
3563
  };
1804
3564
 
3565
+ const deferredBoundarySeed = (
3566
+ operationId: string,
3567
+ callId: string,
3568
+ ): DecideEffectBoundarySeed => {
3569
+ const operation = effectLedgerMirror.logicalOperations.find(
3570
+ (candidate) => candidate.operationId === operationId,
3571
+ );
3572
+ const latestBoundaryId = operation?.boundaryIds.at(-1);
3573
+ const priorBoundary = effectLedgerMirror.boundaries.find(
3574
+ (candidate) => candidate.boundaryId === latestBoundaryId,
3575
+ );
3576
+ if (operation === undefined || priorBoundary === undefined) {
3577
+ throw new TypeError(
3578
+ 'DECIDE deferred logical operation has no linked physical boundary',
3579
+ );
3580
+ }
3581
+ if (
3582
+ currentTurnId === undefined ||
3583
+ !Number.isSafeInteger(currentTurnId) ||
3584
+ currentTurnId <= 0
3585
+ ) {
3586
+ throw new Error(
3587
+ 'DECIDE deferred continuation requires an active positive turn id',
3588
+ );
3589
+ }
3590
+ return {
3591
+ boundaryId: randomUUID(),
3592
+ runtimeSessionId: requireSessionIdentity().sessionId,
3593
+ turnId: currentTurnId,
3594
+ callId,
3595
+ roleId: priorBoundary.roleId,
3596
+ sourceStateId: priorBoundary.sourceStateId,
3597
+ sourceOutcomeSchema: snapshotJsonValue(
3598
+ priorBoundary.sourceOutcomeSchema,
3599
+ 'DECIDE deferred source outcome schema',
3600
+ ),
3601
+ dispositions: [...priorBoundary.dispositions],
3602
+ correctionBudget: { limit: 1, spent: false },
3603
+ };
3604
+ };
3605
+
3606
+ type PreparedDeferredContinuation =
3607
+ | { readonly proceed: false }
3608
+ | { readonly proceed: true; readonly acknowledgement: Promise<void> };
3609
+
3610
+ const prepareDeferredContinuation = async (
3611
+ signal: AbortSignal,
3612
+ resumeEvent: Extract<DecideEvent, { readonly type: 'BOSS_REPLY' }>,
3613
+ ): Promise<PreparedDeferredContinuation> => {
3614
+ if (deferredEffects === undefined || deferredOperationId === undefined) {
3615
+ throw new TypeError(
3616
+ 'DECIDE deferred Boss reply has no host-bound logical operation',
3617
+ );
3618
+ }
3619
+ const operationId = deferredOperationId;
3620
+ const callId = `player-${++playerCallSequence}`;
3621
+ const effectBoundary = deferredBoundarySeed(operationId, callId);
3622
+ const active: ActiveDeferredContinuation = {
3623
+ operationId,
3624
+ effectBoundary,
3625
+ result: deferredValue<PlayerResult>(),
3626
+ acknowledged: deferredValue<PlayerResult>(),
3627
+ };
3628
+ type Readiness =
3629
+ | { readonly status: 'ready' }
3630
+ | {
3631
+ readonly status: 'settled';
3632
+ readonly value:
3633
+ | DecideRepositoryDeferredContinuationResult<PlayerResult>
3634
+ | DecideRepositoryDeferredCheckpointMismatch;
3635
+ }
3636
+ | { readonly status: 'rejected'; readonly reason: unknown };
3637
+ const readiness = deferredValue<Readiness>();
3638
+ const repositoryCall = deferredEffects.repository.runDeferred({
3639
+ mode: 'continue',
3640
+ signal,
3641
+ operationId,
3642
+ effectBoundary,
3643
+ operation: async ({ playerContinuation }) => {
3644
+ if (
3645
+ playerContinuation !== false &&
3646
+ (typeof playerContinuation !== 'string' ||
3647
+ playerContinuation.trim() === '')
3648
+ ) {
3649
+ throw new TypeError(
3650
+ 'DECIDE bound deferred player continuation is invalid',
3651
+ );
3652
+ }
3653
+ active.playerContinuation = playerContinuation;
3654
+ readiness.resolve({ status: 'ready' });
3655
+ return active.result.promise;
3656
+ },
3657
+ completeEffectBoundary: async (completion) => {
3658
+ if (active.input === undefined) {
3659
+ throw new TypeError(
3660
+ 'DECIDE deferred continuation omitted its authored player input',
3661
+ );
3662
+ }
3663
+ return completionEvidenceFor(
3664
+ active.input,
3665
+ active.input.role,
3666
+ active.playerId,
3667
+ signal,
3668
+ operationId,
3669
+ )(completion);
3670
+ },
3671
+ });
3672
+ void repositoryCall.then(
3673
+ (value) => readiness.resolve({ status: 'settled', value }),
3674
+ (reason: unknown) => readiness.resolve({ status: 'rejected', reason }),
3675
+ );
3676
+ const prepared = await readiness.promise;
3677
+ if (prepared.status === 'rejected') throw prepared.reason;
3678
+ if (prepared.status === 'settled') {
3679
+ synchronizeDeferredProjection(
3680
+ assertPlaybookEffectLedger(
3681
+ prepared.value.effectLedger,
3682
+ 'DECIDE deferred checkpoint-mismatch effect ledger',
3683
+ ),
3684
+ );
3685
+ hiddenDeferredOperationId = operationId;
3686
+ return { proceed: false };
3687
+ }
3688
+
3689
+ activeDeferredContinuation = active;
3690
+ const acknowledgement = repositoryCall.then(
3691
+ (value) => {
3692
+ if (value.status !== 'continued') {
3693
+ throw new TypeError(
3694
+ 'DECIDE deferred repository changed status after starting its operation',
3695
+ );
3696
+ }
3697
+ const result = acknowledgeGovernedPlayerResult(
3698
+ value,
3699
+ effectBoundary.boundaryId,
3700
+ );
3701
+ if (hiddenDeferredOperationId === undefined) {
3702
+ suppressInspectionEmissions = false;
3703
+ const live = actor;
3704
+ if (live === undefined) {
3705
+ throw new Error(
3706
+ 'DECIDE deferred acknowledgement lost its runtime actor',
3707
+ );
3708
+ }
3709
+ // Entry into the governed continuation was hidden until its durable
3710
+ // effect acknowledgement. Publish that exact current root snapshot
3711
+ // before releasing the actor output, so an accepted-outcome marker
3712
+ // is confirmed against commitCoderProposal rather than the earlier
3713
+ // awaitBossReply snapshot.
3714
+ inspect({
3715
+ type: '@xstate.snapshot',
3716
+ actorRef: live,
3717
+ event: resumeEvent,
3718
+ snapshot: live.getSnapshot(),
3719
+ } as unknown as InspectionEvent);
3720
+ }
3721
+ active.acknowledged.resolve(result);
3722
+ },
3723
+ (error: unknown) => {
3724
+ active.acknowledged.reject(error);
3725
+ throw error;
3726
+ },
3727
+ ).catch((error: unknown) => {
3728
+ active.acknowledged.reject(error);
3729
+ throw error;
3730
+ });
3731
+ return { proceed: true, acknowledgement };
3732
+ };
3733
+
3734
+ const parkDeferredContinuation = async (
3735
+ signal: AbortSignal,
3736
+ ): Promise<void> => {
3737
+ if (deferredEffects === undefined || deferredOperationId === undefined) {
3738
+ return;
3739
+ }
3740
+ const operationId = deferredOperationId;
3741
+ const parked = await deferredEffects.repository.runDeferred({
3742
+ mode: 'park',
3743
+ signal,
3744
+ operationId,
3745
+ });
3746
+ synchronizeDeferredProjection(
3747
+ assertPlaybookEffectLedger(
3748
+ parked.effectLedger,
3749
+ 'DECIDE deferred park effect ledger',
3750
+ ),
3751
+ );
3752
+ hiddenDeferredOperationId = operationId;
3753
+ };
3754
+
3755
+ const restoreDeferredReconciliation = async (
3756
+ operationId: string,
3757
+ signal: AbortSignal,
3758
+ publishQuestion: boolean,
3759
+ ): Promise<'restored' | 'checkpoint-mismatch' | 'ineligible'> => {
3760
+ const restored = await deferredEffects.repository.runDeferred({
3761
+ mode: 'restore',
3762
+ signal,
3763
+ operationId,
3764
+ });
3765
+ synchronizeDeferredProjection(
3766
+ assertPlaybookEffectLedger(
3767
+ restored.effectLedger,
3768
+ 'DECIDE deferred restoration effect ledger',
3769
+ ),
3770
+ );
3771
+ if (restored.status === 'parked') {
3772
+ throw new TypeError(
3773
+ 'DECIDE deferred restoration returned an invalid parked status',
3774
+ );
3775
+ }
3776
+ if (restored.status !== 'restored') {
3777
+ if (hiddenDeferredOperationId !== operationId) {
3778
+ throw new TypeError(
3779
+ 'DECIDE unresolved deferred restoration lost its operation identity',
3780
+ );
3781
+ }
3782
+ return restored.status;
3783
+ }
3784
+ if (hiddenDeferredOperationId !== undefined) {
3785
+ throw new TypeError(
3786
+ 'DECIDE restored deferred operation remained unresolved',
3787
+ );
3788
+ }
3789
+ if (openDeferredOperation()?.operationId !== operationId) {
3790
+ throw new TypeError(
3791
+ 'DECIDE restored deferred operation is not the current bound wait',
3792
+ );
3793
+ }
3794
+ const live = actor;
3795
+ if (live === undefined) {
3796
+ throw new Error('decide runtime: init(session) must be called first');
3797
+ }
3798
+ const state = currentState();
3799
+ const context = (
3800
+ live.getSnapshot() as SnapshotFrom<typeof decideMachine>
3801
+ ).context as unknown as Record<string, unknown>;
3802
+ const pending = questionForWaitState(
3803
+ 'awaitBossReply',
3804
+ visiblePendingQuestionsForState(state, context),
3805
+ );
3806
+ const operation = openDeferredOperation();
3807
+ const projectedPending =
3808
+ pending === undefined
3809
+ ? undefined
3810
+ : {
3811
+ questionId: pending.questionId,
3812
+ asker: pending.asker,
3813
+ question: pending.question,
3814
+ sourceItem: pending.sourceItem,
3815
+ };
3816
+ if (
3817
+ pending === undefined ||
3818
+ operation?.pendingQuestion === undefined ||
3819
+ stableJson(
3820
+ projectedPending,
3821
+ 'DECIDE restored FSM pending question',
3822
+ ) !==
3823
+ stableJson(
3824
+ operation.pendingQuestion,
3825
+ 'DECIDE restored deferred pending question',
3826
+ )
3827
+ ) {
3828
+ throw new TypeError(
3829
+ 'DECIDE restored deferred wait does not equal its FSM question',
3830
+ );
3831
+ }
3832
+ if (publishQuestion) {
3833
+ await emitBoundaryStatus(
3834
+ `${pending.asker.roleId} asks: ${pending.question}`,
3835
+ state,
3836
+ );
3837
+ await emitBoundaryStatus(
3838
+ `◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`,
3839
+ state,
3840
+ );
3841
+ await flush();
3842
+ }
3843
+ return 'restored';
3844
+ };
3845
+
3846
+ type UnresolvedControlCandidate = {
3847
+ readonly action: PlaybookControlAction;
3848
+ readonly kind: 'reconcile' | 'abandon';
3849
+ readonly deferredRestoreOperationId?: string;
3850
+ };
3851
+ type UnresolvedControlCandidates = readonly UnresolvedControlCandidate[];
3852
+
3853
+ const unresolvedControlCandidates = (): UnresolvedControlCandidates => {
3854
+ if (!hasUnresolvedReconciliation()) return [];
3855
+ const operation =
3856
+ hiddenDeferredOperationId === undefined
3857
+ ? undefined
3858
+ : effectLedgerMirror.logicalOperations.find(
3859
+ ({ operationId }) =>
3860
+ operationId === hiddenDeferredOperationId,
3861
+ );
3862
+ return [
3863
+ {
3864
+ action: {
3865
+ id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
3866
+ label: 'Retry unresolved effect reconciliation',
3867
+ },
3868
+ kind: 'reconcile',
3869
+ ...(operation?.checkpointRestorationEligible === true
3870
+ ? { deferredRestoreOperationId: operation.operationId }
3871
+ : {}),
3872
+ },
3873
+ {
3874
+ action: {
3875
+ id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
3876
+ label: 'Abandon unresolved workflow attempt',
3877
+ },
3878
+ kind: 'abandon',
3879
+ },
3880
+ ];
3881
+ };
3882
+
3883
+ const deriveControlCandidates = (): UnresolvedControlCandidates => {
3884
+ const state = currentState();
3885
+ if (
3886
+ state.status !== 'active' ||
3887
+ !state.quiescent ||
3888
+ nestedBridge.getPendingCall() !== undefined
3889
+ ) {
3890
+ return [];
3891
+ }
3892
+ return unresolvedControlCandidates();
3893
+ };
3894
+
3895
+ const describeControlView = (): PlaybookControlView => {
3896
+ if (disposed || disposalPromise !== undefined) {
3897
+ throw new Error('decide runtime: runtime is disposing or disposed');
3898
+ }
3899
+ const live = actor;
3900
+ if (live === undefined || sessionIdentity === undefined) {
3901
+ throw new Error(
3902
+ 'decide runtime: init(session) must be called before describe',
3903
+ );
3904
+ }
3905
+ if (currentSignal !== undefined || currentTurnId !== undefined) {
3906
+ throw new Error('decide runtime: another runtime turn is active');
3907
+ }
3908
+ refreshReconciliationProjection();
3909
+ const state = currentState();
3910
+ const context = (
3911
+ live.getSnapshot() as SnapshotFrom<typeof decideMachine>
3912
+ ).context as unknown as Record<string, unknown>;
3913
+ const unresolved = hasUnresolvedReconciliation();
3914
+ const pendingQuestions = unresolved
3915
+ ? []
3916
+ : visiblePendingQuestionsForState(state, context).map((pending) =>
3917
+ Object.freeze({
3918
+ questionId: pending.questionId,
3919
+ asker: pending.asker,
3920
+ question: pending.question,
3921
+ sourceItem: pending.sourceItem,
3922
+ }),
3923
+ );
3924
+ const actions = deriveControlCandidates().map(({ action }) => ({
3925
+ ...action,
3926
+ }));
3927
+ const lastError = normalizeErrorFull(context.lastError);
3928
+ const stateDescription =
3929
+ unresolved || state.stateId === undefined
3930
+ ? undefined
3931
+ : STATE_DESCRIPTIONS[state.stateId];
3932
+ return deepFreeze(
3933
+ snapshotJsonValue(
3934
+ {
3935
+ state,
3936
+ ...(stateDescription === undefined ? {} : { stateDescription }),
3937
+ pendingQuestions,
3938
+ ...(lastError === undefined ? {} : { lastError }),
3939
+ actions,
3940
+ },
3941
+ 'DECIDE control view',
3942
+ ) as unknown as PlaybookControlView,
3943
+ );
3944
+ };
3945
+
3946
+ const normalizedControlError = (error: unknown): NormalizedError =>
3947
+ normalizeErrorFull(error) ?? {
3948
+ name: error instanceof Error ? error.name : 'Error',
3949
+ message: error instanceof Error ? error.message : String(error),
3950
+ };
3951
+
3952
+ const frozenControlReceipt = (
3953
+ receipt: PlaybookControlReceipt,
3954
+ ): PlaybookControlReceipt =>
3955
+ deepFreeze(
3956
+ snapshotJsonValue(
3957
+ receipt,
3958
+ 'DECIDE apply receipt',
3959
+ ) as unknown as PlaybookControlReceipt,
3960
+ );
3961
+
3962
+ const applyControlAction = async (input: {
3963
+ actionId: string;
3964
+ key: string;
3965
+ signal: AbortSignal;
3966
+ }): Promise<PlaybookControlReceipt> => {
3967
+ if (input === null || typeof input !== 'object') {
3968
+ throw new TypeError('decide runtime: apply input must be an object');
3969
+ }
3970
+ const { actionId, key, signal } = input;
3971
+ if (typeof actionId !== 'string' || actionId.length === 0) {
3972
+ throw new TypeError(
3973
+ 'decide runtime: apply actionId must be a non-empty string',
3974
+ );
3975
+ }
3976
+ if (typeof key !== 'string' || key.length === 0) {
3977
+ throw new TypeError(
3978
+ 'decide runtime: apply key must be a non-empty string',
3979
+ );
3980
+ }
3981
+ if (!(signal instanceof AbortSignal)) {
3982
+ throw new TypeError(
3983
+ 'decide runtime: apply signal must be an AbortSignal',
3984
+ );
3985
+ }
3986
+ if (disposed || disposalPromise !== undefined) {
3987
+ throw new Error('decide runtime: runtime is disposing or disposed');
3988
+ }
3989
+ if (actor === undefined || sessionIdentity === undefined) {
3990
+ throw new Error(
3991
+ 'decide runtime: init(session) must be called before apply',
3992
+ );
3993
+ }
3994
+ if (currentSignal !== undefined || currentTurnId !== undefined) {
3995
+ throw new Error('decide runtime: another runtime turn is active');
3996
+ }
3997
+ const recorded = appliedControlReceipts.get(key);
3998
+ if (recorded !== undefined) return recorded;
3999
+ signal.throwIfAborted();
4000
+ refreshReconciliationProjection();
4001
+
4002
+ const turnId = ++turnSequence;
4003
+ const callId = `apply-${++applyCallSequence}`;
4004
+ currentTurnId = turnId;
4005
+ currentSignal = signal;
4006
+ currentAborts = abortReasonClassifier(signal);
4007
+ controlPlaneError = undefined;
4008
+ let accepted = false;
4009
+ let receipt: PlaybookControlReceipt | undefined;
4010
+ let operationError: unknown;
4011
+ let settlementError: unknown;
4012
+ let lateDeliveryError: unknown;
4013
+ const position = { turnId, callId };
4014
+ const preAcceptanceFinish = (
4015
+ reason: string,
4016
+ ): Record<string, unknown> => ({
4017
+ actionId,
4018
+ key,
4019
+ disposition: 'rejected',
4020
+ reason,
4021
+ });
4022
+ try {
4023
+ try {
4024
+ try {
4025
+ await emitTrace(
4026
+ 'apply.started',
4027
+ {
4028
+ actionId,
4029
+ key,
4030
+ ...stateIdentity(currentState()),
4031
+ },
4032
+ position,
4033
+ currentAborts,
4034
+ );
4035
+ } catch (error) {
4036
+ latchControlPlaneError(error, signal);
4037
+ try {
4038
+ await emitTrace(
4039
+ 'apply.finished',
4040
+ {
4041
+ ...preAcceptanceFinish('apply.started trace sink rejected'),
4042
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
4043
+ error: normalizedControlError(error),
4044
+ },
4045
+ position,
4046
+ currentAborts,
4047
+ );
4048
+ } catch {
4049
+ // Preserve the start failure after one best-effort finish attempt.
4050
+ }
4051
+ throw error;
4052
+ }
4053
+ await flush();
4054
+ if (signal.aborted) {
4055
+ try {
4056
+ await emitTrace(
4057
+ 'apply.finished',
4058
+ {
4059
+ ...preAcceptanceFinish('aborted before acceptance'),
4060
+ status: 'aborted',
4061
+ error: normalizedControlError(signal.reason),
4062
+ },
4063
+ position,
4064
+ currentAborts,
4065
+ );
4066
+ } catch (error) {
4067
+ // A rejecting abort-finish sink outranks the abort at settlement.
4068
+ settlementError ??= error;
4069
+ }
4070
+ }
4071
+ signal.throwIfAborted();
4072
+ const candidate = deriveControlCandidates().find(
4073
+ ({ action }) => action.id === actionId,
4074
+ );
4075
+ if (candidate === undefined) {
4076
+ receipt = frozenControlReceipt({
4077
+ disposition: 'rejected',
4078
+ reason: `action ${JSON.stringify(actionId)} is not currently advertised`,
4079
+ });
4080
+ } else {
4081
+ // Acceptance is the line after which this boundary always records
4082
+ // and returns a receipt: the requested effect may now exist.
4083
+ accepted = true;
4084
+ try {
4085
+ let run: PlaybookRunResult;
4086
+ if (candidate.kind === 'abandon') {
4087
+ signal.throwIfAborted();
4088
+ run = {
4089
+ outcome: 'unresolved-effect',
4090
+ state: currentState(),
4091
+ };
4092
+ } else {
4093
+ if (candidate.deferredRestoreOperationId !== undefined) {
4094
+ await restoreDeferredReconciliation(
4095
+ candidate.deferredRestoreOperationId,
4096
+ signal,
4097
+ true,
4098
+ );
4099
+ } else {
4100
+ // The host owns receipt reconstruction. Reconciliation only
4101
+ // re-reads its authoritative ledger; it never calls a player
4102
+ // or judge to manufacture missing semantic evidence.
4103
+ refreshReconciliationProjection();
4104
+ }
4105
+ signal.throwIfAborted();
4106
+ run = {
4107
+ outcome: hasUnresolvedReconciliation()
4108
+ ? 'no-action'
4109
+ : 'quiescent',
4110
+ state: currentState(),
4111
+ };
4112
+ }
4113
+ if (controlPlaneError !== undefined) throw controlPlaneError;
4114
+ receipt = frozenControlReceipt({
4115
+ disposition: 'executed',
4116
+ run,
4117
+ });
4118
+ } catch (error) {
4119
+ receipt = frozenControlReceipt({
4120
+ disposition: 'failed',
4121
+ error: normalizedControlError(error),
4122
+ });
4123
+ }
4124
+ appliedControlReceipts.set(key, receipt);
4125
+ }
4126
+ } catch (error) {
4127
+ operationError = error;
4128
+ }
4129
+
4130
+ try {
4131
+ await flush();
4132
+ } catch (error) {
4133
+ settlementError = error;
4134
+ }
4135
+ settlementError ??= controlPlaneError;
4136
+ if (accepted && settlementError !== undefined) {
4137
+ receipt = frozenControlReceipt({
4138
+ disposition: 'failed',
4139
+ error: normalizedControlError(settlementError),
4140
+ });
4141
+ appliedControlReceipts.set(key, receipt);
4142
+ settlementError = undefined;
4143
+ }
4144
+
4145
+ if (receipt !== undefined) {
4146
+ const finishFailures: unknown[] = [];
4147
+ try {
4148
+ await emitTrace(
4149
+ 'apply.finished',
4150
+ { actionId, key, ...receipt },
4151
+ position,
4152
+ currentAborts,
4153
+ );
4154
+ } catch (error) {
4155
+ if (!accepted || !isAbortFailure(error, signal)) {
4156
+ collectFailure(finishFailures, error);
4157
+ }
4158
+ }
4159
+ try {
4160
+ await flush();
4161
+ } catch (error) {
4162
+ if (!accepted || !isAbortFailure(error, signal)) {
4163
+ collectFailure(finishFailures, error);
4164
+ }
4165
+ }
4166
+ if (finishFailures.length > 0) {
4167
+ const failure =
4168
+ finishFailures.length === 1
4169
+ ? finishFailures[0]
4170
+ : new AggregateError(
4171
+ finishFailures,
4172
+ 'DECIDE apply settlement emissions failed',
4173
+ );
4174
+ if (accepted) lateDeliveryError = failure;
4175
+ else settlementError = failure;
4176
+ }
4177
+ }
4178
+ } finally {
4179
+ currentSignal = undefined;
4180
+ currentAborts = undefined;
4181
+ currentTurnId = undefined;
4182
+ controlPlaneError = undefined;
4183
+ if (accepted && receipt !== undefined) {
4184
+ appliedControlReceipts.set(key, receipt);
4185
+ }
4186
+ }
4187
+ if (lateDeliveryError !== undefined) {
4188
+ collectFailure(emissionFailures, lateDeliveryError);
4189
+ }
4190
+ if (accepted && receipt !== undefined) return receipt;
4191
+ const failure = settlementError ?? operationError;
4192
+ if (failure !== undefined) throw failure;
4193
+ if (receipt === undefined) {
4194
+ throw new Error('decide runtime: apply produced no receipt');
4195
+ }
4196
+ return receipt;
4197
+ };
4198
+
1805
4199
  const resultForSnapshot = (signal?: AbortSignal): PlaybookRunResult => {
1806
4200
  const live = actor;
1807
4201
  if (!live) throw new Error('decide runtime: actor is not initialized');
@@ -1928,6 +4322,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1928
4322
  activeEmissionCalls.clear();
1929
4323
  emissionQueue.clear();
1930
4324
  judgeQueue.clear();
4325
+ semanticCompletionQueue.clear();
1931
4326
  actor = undefined;
1932
4327
  currentSignal = undefined;
1933
4328
  currentAborts = undefined;
@@ -1945,7 +4340,20 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1945
4340
  judgeCallSequence = 0;
1946
4341
  playerCallSequence = 0;
1947
4342
  playbookCallSequence = 0;
4343
+ applyCallSequence = 0;
1948
4344
  playbookCallTurnIds.clear();
4345
+ governedOutputsByBoundaryId.clear();
4346
+ governedFailuresByBoundaryId.clear();
4347
+ governedEvidenceByBoundaryId.clear();
4348
+ governedReceiptsByBoundaryId.clear();
4349
+ pendingProposalCohort.clear();
4350
+ activeProposalCohort = undefined;
4351
+ completedProposalCohortTurnId = undefined;
4352
+ deferredOperationId = undefined;
4353
+ hiddenDeferredOperationId = undefined;
4354
+ unresolvedSemanticBoundaryIds.clear();
4355
+ appliedControlReceipts.clear();
4356
+ activeDeferredContinuation = undefined;
1949
4357
  lifecycleStarted = false;
1950
4358
  };
1951
4359
 
@@ -1971,6 +4379,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1971
4379
  ports = identity.ports;
1972
4380
  sessionIdentity = identity;
1973
4381
  try {
4382
+ if (deferredEffects !== undefined) {
4383
+ effectLedgerMirror = readEffectLedger();
4384
+ synchronizeDeferredProjection(effectLedgerMirror);
4385
+ }
1974
4386
  suppressInspectionEmissions = false;
1975
4387
  createRuntimeActor();
1976
4388
  const state = currentState();
@@ -1989,6 +4401,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
1989
4401
  }
1990
4402
  },
1991
4403
 
4404
+ describe: describeControlView,
4405
+ unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
4406
+ apply: applyControlAction,
4407
+
1992
4408
  // DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
1993
4409
  // session, including one already-started suspended REVIEW call.
1994
4410
  // Defined only at a safe capture point — initialized, not disposing
@@ -2045,8 +4461,12 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2045
4461
  const context = (
2046
4462
  actor.getSnapshot() as SnapshotFrom<typeof decideMachine>
2047
4463
  ).context as unknown as Record<string, unknown>;
4464
+ if (deferredEffects !== undefined) {
4465
+ synchronizeDeferredProjection(readEffectLedger());
4466
+ }
4467
+ const unresolved = hasUnresolvedReconciliation();
2048
4468
  return {
2049
- schemaVersion: 3,
4469
+ schemaVersion: 4,
2050
4470
  playbookId: sessionIdentity.playbookId,
2051
4471
  machine,
2052
4472
  roleResumeTokens: snapshotRoleResumeTokens(),
@@ -2058,14 +4478,15 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2058
4478
  playbookCall: playbookCallSequence,
2059
4479
  },
2060
4480
  state,
2061
- pendingBossQuestions: pendingQuestionsForState(state, context).map(
2062
- (pending) => ({
2063
- questionId: pending.questionId,
2064
- asker: pending.asker,
2065
- question: pending.question,
2066
- sourceItem: pending.sourceItem,
2067
- }),
2068
- ),
4481
+ pendingBossQuestions: unresolved
4482
+ ? []
4483
+ : visiblePendingQuestionsForState(state, context).map((pending) => ({
4484
+ questionId: pending.questionId,
4485
+ asker: pending.asker,
4486
+ question: pending.question,
4487
+ sourceItem: pending.sourceItem,
4488
+ })),
4489
+ effectLedger: effectLedgerMirror,
2069
4490
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
2070
4491
  };
2071
4492
  },
@@ -2095,6 +4516,28 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2095
4516
  identity.playbookId,
2096
4517
  { allowSuspendedCall: true },
2097
4518
  );
4519
+ if (deferredEffects === undefined) {
4520
+ if (
4521
+ boundSnapshot.effectLedger.revision !== 0 ||
4522
+ boundSnapshot.effectLedger.boundaries.length !== 0 ||
4523
+ boundSnapshot.effectLedger.logicalOperations.length !== 0
4524
+ ) {
4525
+ throw new TypeError(
4526
+ 'decide runtime snapshot effectLedger must be the canonical empty ledger',
4527
+ );
4528
+ }
4529
+ } else {
4530
+ const current = readEffectLedger();
4531
+ if (
4532
+ stableJson(current, 'DECIDE current effect ledger') !==
4533
+ stableJson(boundSnapshot.effectLedger, 'DECIDE snapshot effect ledger')
4534
+ ) {
4535
+ throw new TypeError(
4536
+ 'decide runtime snapshot effectLedger must equal the current host mirror',
4537
+ );
4538
+ }
4539
+ effectLedgerMirror = current;
4540
+ }
2098
4541
  const suspendedCall = boundSnapshot.suspendedCall;
2099
4542
  let finishInitialization!: () => void;
2100
4543
  const initialization = new Promise<void>((resolve) => {
@@ -2113,6 +4556,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2113
4556
  judgeCallSequence = boundSnapshot.sequences.judgeCall;
2114
4557
  playerCallSequence = boundSnapshot.sequences.playerCall;
2115
4558
  playbookCallSequence = boundSnapshot.sequences.playbookCall;
4559
+ applyCallSequence = boundSnapshot.sequences.trace;
2116
4560
  if (identity.playerSessions) {
2117
4561
  priorExternalRoleTokens = snapshotRoleResumeTokens();
2118
4562
  }
@@ -2121,6 +4565,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2121
4565
  if (suspendedCall !== undefined) {
2122
4566
  playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
2123
4567
  }
4568
+ synchronizeDeferredProjection(effectLedgerMirror);
2124
4569
  suppressInspectionEmissions = true;
2125
4570
  createRuntimeActor(boundSnapshot.machine);
2126
4571
  actor?.start();
@@ -2203,23 +4648,91 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2203
4648
  const event = await classify(turn.text, turn.signal);
2204
4649
  if (!event) {
2205
4650
  const state = currentState();
2206
- await emitBoundaryStatus('No playbook action classified.', state);
4651
+ if (!hasUnresolvedReconciliation()) {
4652
+ await emitBoundaryStatus('No playbook action classified.', state);
4653
+ }
2207
4654
  result = { outcome: 'no-action', state };
2208
4655
  } else if (event.type === 'NO_ACTION') {
2209
4656
  result = { outcome: 'no-action', state: currentState() };
2210
4657
  } else {
2211
- await emitBoundaryStatus(event.type, currentState());
2212
- if (actor.getSnapshot().status === 'done') {
2213
- stopActor();
2214
- startActor();
2215
- }
4658
+ const before = currentState();
4659
+ const boundCommitWait =
4660
+ deferredEffects !== undefined &&
4661
+ deferredOperationId !== undefined &&
4662
+ before.activeStateIds.includes('awaitBossReply');
4663
+ if (boundCommitWait && event.type === 'BOSS_INTERRUPT') {
4664
+ await parkDeferredContinuation(turn.signal);
4665
+ result = { outcome: 'no-action', state: currentState() };
4666
+ } else {
4667
+ const prepared =
4668
+ boundCommitWait &&
4669
+ event.type === 'BOSS_REPLY' &&
4670
+ event.questionId === 'commitCoderProposal'
4671
+ ? await prepareDeferredContinuation(turn.signal, event)
4672
+ : undefined;
4673
+ if (prepared?.proceed === false) {
4674
+ result = { outcome: 'no-action', state: currentState() };
4675
+ } else {
4676
+ await emitBoundaryStatus(event.type, before);
4677
+ if (actor.getSnapshot().status === 'done') {
4678
+ stopActor();
4679
+ startActor();
4680
+ }
2216
4681
 
2217
- actor.send(event);
2218
- await driveToQuiescence();
2219
- await drainBoundaryCallsAndEmissions();
4682
+ const deferredMachineCheckpoint =
4683
+ prepared?.proceed === true
4684
+ ? detachPersistedMachineSnapshot(
4685
+ actor.getPersistedSnapshot(),
4686
+ )
4687
+ : undefined;
4688
+ if (deferredMachineCheckpoint !== undefined) {
4689
+ suppressInspectionEmissions = true;
4690
+ }
2220
4691
 
2221
- if (controlPlaneError !== undefined) throw controlPlaneError;
2222
- result = resultForSnapshot(turn.signal);
4692
+ try {
4693
+ actor.send(event);
4694
+ await driveToQuiescence();
4695
+ await drainBoundaryCallsAndEmissions();
4696
+ await prepared?.acknowledgement;
4697
+ if (
4698
+ deferredMachineCheckpoint !== undefined &&
4699
+ hiddenDeferredOperationId !== undefined
4700
+ ) {
4701
+ stopActor();
4702
+ createRuntimeActor(deferredMachineCheckpoint);
4703
+ actor.start();
4704
+ previousState = before;
4705
+ suppressInspectionEmissions = false;
4706
+ }
4707
+ } catch (error) {
4708
+ activeDeferredContinuation?.result.reject(error);
4709
+ if (deferredEffects !== undefined) {
4710
+ try {
4711
+ synchronizeDeferredProjection(readEffectLedger());
4712
+ hiddenDeferredOperationId ??= deferredOperationId;
4713
+ } catch {
4714
+ hiddenDeferredOperationId ??= deferredOperationId;
4715
+ }
4716
+ }
4717
+ if (deferredMachineCheckpoint !== undefined) {
4718
+ try {
4719
+ stopActor();
4720
+ createRuntimeActor(deferredMachineCheckpoint);
4721
+ actor.start();
4722
+ previousState = before;
4723
+ } finally {
4724
+ suppressInspectionEmissions = false;
4725
+ }
4726
+ }
4727
+ throw error;
4728
+ } finally {
4729
+ activeDeferredContinuation = undefined;
4730
+ }
4731
+
4732
+ if (controlPlaneError !== undefined) throw controlPlaneError;
4733
+ result = resultForSnapshot(turn.signal);
4734
+ }
4735
+ }
2223
4736
  }
2224
4737
  }
2225
4738
  settlement = {
@@ -2330,6 +4843,10 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2330
4843
  if (currentSignal !== undefined) {
2331
4844
  throw new Error('decide runtime: another runtime turn is active');
2332
4845
  }
4846
+ refreshReconciliationProjection();
4847
+ if (hasUnresolvedReconciliation()) {
4848
+ return { outcome: 'no-action', state: currentState() };
4849
+ }
2333
4850
  currentTurnId = playbookCallTurnIds.get(callId);
2334
4851
  currentSignal = signal;
2335
4852
  currentAborts = abortReasonClassifier(signal);
@@ -2462,6 +4979,7 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2462
4979
  activeEmissionCalls.clear();
2463
4980
  emissionQueue.clear();
2464
4981
  judgeQueue.clear();
4982
+ semanticCompletionQueue.clear();
2465
4983
  actor = undefined;
2466
4984
  currentSignal = undefined;
2467
4985
  currentAborts = undefined;
@@ -2472,6 +4990,19 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2472
4990
  sessionIdentity = undefined;
2473
4991
  previousState = undefined;
2474
4992
  controlPlaneError = undefined;
4993
+ governedOutputsByBoundaryId.clear();
4994
+ governedFailuresByBoundaryId.clear();
4995
+ governedEvidenceByBoundaryId.clear();
4996
+ governedReceiptsByBoundaryId.clear();
4997
+ pendingProposalCohort.clear();
4998
+ activeProposalCohort = undefined;
4999
+ completedProposalCohortTurnId = undefined;
5000
+ deferredOperationId = undefined;
5001
+ hiddenDeferredOperationId = undefined;
5002
+ unresolvedSemanticBoundaryIds.clear();
5003
+ appliedControlReceipts.clear();
5004
+ applyCallSequence = 0;
5005
+ activeDeferredContinuation = undefined;
2475
5006
  disposed = true;
2476
5007
  }
2477
5008
  if (failures.length === 1) throw failures[0];
@@ -2488,6 +5019,16 @@ export const createPlaybookRuntime: PlaybookRuntimeFactory<
2488
5019
  return nestedBridge;
2489
5020
  },
2490
5021
  };
5022
+ }
5023
+
5024
+ export const createPlaybookRuntime: PlaybookRuntimeFactory<
5025
+ DecidePlaybookRuntimeConstruction
5026
+ > = (factoryInput) => {
5027
+ const construction = schema3Construction(factoryInput);
5028
+ return createDecidePlaybookRuntime(
5029
+ construction.configuredOptions,
5030
+ construction.hostCapabilities,
5031
+ );
2491
5032
  };
2492
5033
 
2493
5034
  export const _internal = {
@@ -2497,7 +5038,6 @@ export const _internal = {
2497
5038
  buildClassifierPrompt,
2498
5039
  parseClassification,
2499
5040
  buildAdjudicatorPrompt,
2500
- parseAdjudication,
2501
5041
  combineSignals,
2502
5042
  pendingQuestionsFromContext,
2503
5043
  pendingQuestionsForState,
@@ -2508,6 +5048,7 @@ export const _internal = {
2508
5048
  ROLE_STATE_IDS,
2509
5049
  VERBATIM_PAYLOAD_FIELDS,
2510
5050
  BOSS_INTERRUPT_TARGETS,
5051
+ UNFINISHED_FINAL_STATE_IDS,
2511
5052
  CONTINUATION_PREAMBLE,
2512
5053
  TELEMETRY_TOPIC,
2513
5054
  };