@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
@@ -15,9 +15,11 @@
15
15
  // onError routes to the quiescent failed state.
16
16
  // Output profile: bespoke parallel runtime with the shared nested-call
17
17
  // bridge (slc/link.md §Output)
18
+ import { randomUUID } from 'node:crypto';
18
19
  import PQueue from 'p-queue';
19
20
  import { createActor, fromPromise } from 'xstate';
20
- import { assertJsonSafe, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
21
+ import { createAcceptedOutcomeConsumer, } from '../../../src/accepted-outcome.js';
22
+ import { assertJsonSafe, assertPlaybookEffectLedger, assertPlaybookRuntimeSnapshot, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, snapshotJsonValue, snapshotPlaybookSession, validatePlayerResult, waitForPlaybookQuiescence, } from '../../../src/xstate-runtime.js';
21
23
  import decideMachine from './decide.fsm.js';
22
24
  function snapshotDecideRuntimeOptions(value) {
23
25
  const captured = snapshotJsonValue(value, 'DECIDE runtime options');
@@ -62,13 +64,64 @@ const ROLE_STATES = [
62
64
  { stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
63
65
  ];
64
66
  const ROLE_STATE_IDS = new Set(ROLE_STATES.map((state) => state.stateId));
67
+ const ACCEPTED_OUTCOME_DECLARATIONS = Object.freeze({
68
+ askCoderProposal: new Set(['proposed', 'needsBossReply']),
69
+ askReviewerProposal: new Set(['proposed', 'needsBossReply']),
70
+ commitCoderProposal: new Set(['committed', 'needsBossReply']),
71
+ });
72
+ const DECIDE_OUTCOME_AUTHORITY = Object.freeze({
73
+ governedPlayerStates: Object.freeze({
74
+ askCoderProposal: Object.freeze({
75
+ proposed: Object.freeze({
76
+ fields: Object.freeze({ coderProposal: 'presentation' }),
77
+ repositoryDisposition: 'unchanged',
78
+ }),
79
+ needsBossReply: Object.freeze({
80
+ fields: Object.freeze({ question: 'presentation' }),
81
+ repositoryDisposition: 'unchanged',
82
+ }),
83
+ }),
84
+ askReviewerProposal: Object.freeze({
85
+ proposed: Object.freeze({
86
+ fields: Object.freeze({ reviewerProposal: 'presentation' }),
87
+ repositoryDisposition: 'unchanged',
88
+ }),
89
+ needsBossReply: Object.freeze({
90
+ fields: Object.freeze({ question: 'presentation' }),
91
+ repositoryDisposition: 'unchanged',
92
+ }),
93
+ }),
94
+ commitCoderProposal: Object.freeze({
95
+ committed: Object.freeze({
96
+ fields: Object.freeze({
97
+ coderOutput: 'presentation',
98
+ latestCommit: 'effect',
99
+ }),
100
+ repositoryDisposition: 'one-descendant-commit',
101
+ }),
102
+ needsBossReply: Object.freeze({
103
+ fields: Object.freeze({ question: 'presentation' }),
104
+ repositoryDisposition: 'deferred',
105
+ }),
106
+ }),
107
+ }),
108
+ });
109
+ const PROPOSAL_STATE_BY_ROLE = Object.freeze({
110
+ coder: 'askCoderProposal',
111
+ reviewer: 'askReviewerProposal',
112
+ });
65
113
  const ROLE_IDS = ['coder', 'reviewer'];
66
114
  const ROLE_ID_SET = new Set(ROLE_IDS);
67
115
  const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
68
116
  const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
69
117
  const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
118
+ const UNFINISHED_FINAL_STATE_IDS = new Set([
119
+ 'reportedReviewFailure',
120
+ ]);
70
121
  const TELEMETRY_TOPIC = 'playbook.fsm.state';
71
122
  const TRACE_TOPIC = 'playbook.trace';
123
+ const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
124
+ const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
72
125
  const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
73
126
  const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
74
127
  const VERBATIM_PAYLOAD_FIELDS = new Set([
@@ -123,6 +176,27 @@ function requiredFieldsFor(description) {
123
176
  }
124
177
  return fields;
125
178
  }
179
+ function governedOutcomesFor(input) {
180
+ const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[input.stateId];
181
+ if (outcomes === undefined) {
182
+ throw new TypeError(`DECIDE governed player state ${JSON.stringify(input.stateId)} has no outcome authority`);
183
+ }
184
+ const declaredGuards = Object.keys(outcomes).sort();
185
+ const authoredGuards = Object.keys(input.result).sort();
186
+ if (declaredGuards.length !== authoredGuards.length ||
187
+ declaredGuards.some((guard, index) => guard !== authoredGuards[index])) {
188
+ throw new TypeError(`DECIDE governed player state ${input.stateId} changed its authored outcome set`);
189
+ }
190
+ for (const guard of declaredGuards) {
191
+ const required = requiredFieldsFor(input.result[guard]).sort();
192
+ const authoritative = Object.keys(outcomes[guard].fields).sort();
193
+ if (required.length !== authoritative.length ||
194
+ required.some((field, index) => field !== authoritative[index])) {
195
+ throw new TypeError(`DECIDE governed outcome ${input.stateId}.${guard} changed its payload fields`);
196
+ }
197
+ }
198
+ return outcomes;
199
+ }
126
200
  // LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
127
201
  // CODE's recovery contract: scan candidate starts in document order, prefer a
128
202
  // strict balanced value at each position, then repair trailing commas and
@@ -194,6 +268,14 @@ function sortJson(value) {
194
268
  function stableJson(value, path) {
195
269
  return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
196
270
  }
271
+ function deepFreeze(value) {
272
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
273
+ Object.freeze(value);
274
+ for (const member of Object.values(value))
275
+ deepFreeze(member);
276
+ }
277
+ return value;
278
+ }
197
279
  function stripCodeFence(text) {
198
280
  const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
199
281
  return fence ? fence[1].trim() : text;
@@ -323,7 +405,8 @@ function parseClassification(raw, text, pendingQuestionIds = []) {
323
405
  }
324
406
  return null;
325
407
  }
326
- function buildAdjudicatorPrompt(input, playerOutput) {
408
+ function buildAdjudicatorPrompt(input, playerOutput, correction) {
409
+ const outcomes = governedOutcomesFor(input);
327
410
  const lines = [];
328
411
  lines.push('You are the guard adjudicator for a playbook state machine.');
329
412
  lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
@@ -339,54 +422,32 @@ function buildAdjudicatorPrompt(input, playerOutput) {
339
422
  lines.push('');
340
423
  lines.push('Guards (choose exactly one; the descriptions are authoritative and must be applied as written):');
341
424
  for (const [guard, description] of Object.entries(input.result)) {
342
- lines.push(`- ${guard}: ${description}`);
425
+ const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
426
+ .filter(([, authority]) => authority === 'semantic')
427
+ .map(([field]) => field);
428
+ lines.push(`- ${guard}: semantic fields: ${semanticFields.length === 0 ? '(none)' : semanticFields.join(', ')}; ${description}`);
343
429
  }
344
- const runtimeOwnedFields = new Set();
345
- for (const description of Object.values(input.result)) {
346
- for (const field of requiredFieldsFor(description)) {
347
- if (VERBATIM_PAYLOAD_FIELDS.has(field))
348
- runtimeOwnedFields.add(field);
349
- }
350
- }
351
- if (runtimeOwnedFields.size > 0) {
430
+ lines.push('');
431
+ lines.push('Reply with exactly the chosen `guard` and every semantic-owned field for that guard, and no other field.');
432
+ lines.push('Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from authoritative evidence.');
433
+ if (correction !== undefined) {
352
434
  lines.push('');
353
- lines.push(`The runtime owns these verbatim fields; do not include them in your JSON: ${[...runtimeOwnedFields].join(', ')}.`);
435
+ lines.push('Your first reply was structurally invalid:');
436
+ lines.push('"""');
437
+ lines.push(correction.reply);
438
+ lines.push('"""');
439
+ lines.push(`Validation error: ${correction.error}`);
440
+ lines.push('Correct only that structure using the same player output and outcome schema.');
354
441
  }
355
- lines.push('');
356
- lines.push('Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.');
357
442
  return lines.join('\n');
358
443
  }
359
- function parseAdjudication(raw, input, finalText) {
360
- const obj = extractJson(raw);
361
- if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
362
- throw new Error('adjudicator returned empty or malformed JSON');
363
- }
364
- const guard = obj.guard;
365
- if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
366
- throw new Error(`adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`);
367
- }
368
- const requiredFields = requiredFieldsFor(input.result[guard]);
369
- const allowedFields = new Set(['guard', ...requiredFields]);
370
- for (const key of Reflect.ownKeys(obj)) {
371
- if (typeof key !== 'string' || !allowedFields.has(key)) {
372
- throw new Error(`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`);
373
- }
444
+ function parseGovernedSemanticCandidate(raw) {
445
+ try {
446
+ return parseJudgeJson(raw);
374
447
  }
375
- const output = { guard };
376
- for (const field of requiredFields) {
377
- if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
378
- output[field] = finalText;
379
- continue;
380
- }
381
- const value = obj[field];
382
- if (value === undefined ||
383
- value === null ||
384
- (typeof value === 'string' && value.trim() === '')) {
385
- throw new Error(`adjudicator response for guard "${guard}" missing required field "${field}"`);
386
- }
387
- output[field] = value;
448
+ catch (error) {
449
+ throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
388
450
  }
389
- return output;
390
451
  }
391
452
  function combineSignals(a, b) {
392
453
  return combineAbortSignals(a, b);
@@ -407,6 +468,125 @@ function normalizeErrorFull(err) {
407
468
  function isEmptyFinalText(finalText) {
408
469
  return finalText === undefined || finalText.trim().length === 0;
409
470
  }
471
+ function schema3Construction(value) {
472
+ if (!isPlainObject(value)) {
473
+ throw new TypeError('DECIDE schema-3 factory input must be a plain object');
474
+ }
475
+ const descriptors = Object.getOwnPropertyDescriptors(value);
476
+ const keys = Reflect.ownKeys(value);
477
+ if (keys.length !== 2 ||
478
+ !keys.includes('configuredOptions') ||
479
+ !keys.includes('hostCapabilities') ||
480
+ keys.some((key) => {
481
+ const descriptor = descriptors[key];
482
+ return (descriptor?.get !== undefined ||
483
+ descriptor?.set !== undefined ||
484
+ descriptor?.enumerable !== true ||
485
+ !Object.prototype.hasOwnProperty.call(descriptor, 'value'));
486
+ })) {
487
+ throw new TypeError('DECIDE schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties');
488
+ }
489
+ const configuredOptions = descriptors.configuredOptions.value;
490
+ if (configuredOptions !== null &&
491
+ typeof configuredOptions === 'object' &&
492
+ Object.prototype.hasOwnProperty.call(configuredOptions, 'hostCapabilities')) {
493
+ throw new TypeError('DECIDE configured options must not contain hostCapabilities');
494
+ }
495
+ const hostCapabilities = descriptors.hostCapabilities.value;
496
+ if (hostCapabilities === null ||
497
+ typeof hostCapabilities !== 'object' ||
498
+ Array.isArray(hostCapabilities)) {
499
+ throw new TypeError('DECIDE schema-3 factory input hostCapabilities must be a live object');
500
+ }
501
+ const repositoryDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
502
+ const authorityDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority');
503
+ const effectLedgerDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'effectLedger');
504
+ const repository = repositoryDescriptor?.value;
505
+ const authority = authorityDescriptor?.value;
506
+ const effectLedger = effectLedgerDescriptor?.value;
507
+ if (authorityDescriptor === undefined ||
508
+ authorityDescriptor.get !== undefined ||
509
+ authorityDescriptor.set !== undefined ||
510
+ !Object.prototype.hasOwnProperty.call(authorityDescriptor, 'value') ||
511
+ !isPlainObject(authority) ||
512
+ authority.artifactSchema !== 3 ||
513
+ authority.playbookId !== 'decide' ||
514
+ typeof authority.sessionId !== 'string' ||
515
+ authority.sessionId.length === 0 ||
516
+ !Array.isArray(authority.requiredRoleIds) ||
517
+ stableJson([...authority.requiredRoleIds].sort(), 'DECIDE host authority required roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE required roles') ||
518
+ !Array.isArray(authority.concurrentRoleSets) ||
519
+ authority.concurrentRoleSets.length !== 1 ||
520
+ !Array.isArray(authority.concurrentRoleSets[0]) ||
521
+ stableJson([...authority.concurrentRoleSets[0]].sort(), 'DECIDE host authority concurrent roles') !== stableJson([...ROLE_IDS].sort(), 'DECIDE concurrent roles')) {
522
+ throw new TypeError('DECIDE schema-3 factory input hostCapabilities.authority must identify one schema-3 playbook session');
523
+ }
524
+ if (repositoryDescriptor === undefined ||
525
+ repositoryDescriptor.get !== undefined ||
526
+ repositoryDescriptor.set !== undefined ||
527
+ !Object.prototype.hasOwnProperty.call(repositoryDescriptor, 'value') ||
528
+ !isPlainObject(repository) ||
529
+ typeof repository.runExclusive !== 'function' ||
530
+ typeof repository.runCohort !== 'function' ||
531
+ typeof repository.runDeferred !== 'function') {
532
+ throw new TypeError('DECIDE schema-3 factory input hostCapabilities.repository must expose runExclusive, runCohort, and runDeferred functions');
533
+ }
534
+ if (effectLedgerDescriptor === undefined ||
535
+ effectLedgerDescriptor.get !== undefined ||
536
+ effectLedgerDescriptor.set !== undefined ||
537
+ !Object.prototype.hasOwnProperty.call(effectLedgerDescriptor, 'value') ||
538
+ !isPlainObject(effectLedger) ||
539
+ typeof effectLedger.snapshot !== 'function' ||
540
+ typeof effectLedger.writeAhead !== 'function') {
541
+ throw new TypeError('DECIDE schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions');
542
+ }
543
+ return {
544
+ configuredOptions: configuredOptions,
545
+ hostCapabilities: {
546
+ authority: authority,
547
+ repository: repository,
548
+ effectLedger: effectLedger,
549
+ },
550
+ };
551
+ }
552
+ function deferredValue() {
553
+ let resolve;
554
+ let reject;
555
+ const promise = new Promise((onResolve, onReject) => {
556
+ resolve = onResolve;
557
+ reject = onReject;
558
+ });
559
+ return { promise, resolve, reject };
560
+ }
561
+ function hasCompleteUnchangedReceipt(boundary) {
562
+ return boundary.physicalReceipt?.classification === 'unchanged';
563
+ }
564
+ // A corrective call is bound to the exact physical boundary it would repeat.
565
+ // A failed-state restart replays the whole entry event. Cooperative host
566
+ // attempts are serialized in ledger order, so the latest durable boundary
567
+ // identifies the causal host attempt even when a nested or sibling runtime
568
+ // wrote it; every boundary in that attempt must have a complete unchanged
569
+ // receipt.
570
+ // The durable ledger remains the authority in both cases; no process-local
571
+ // player result or presentation text can make a replay safe.
572
+ function createAutomaticReplayPolicy(evidence) {
573
+ const readLedger = () => assertPlaybookEffectLedger(evidence.effectLedger.snapshot(), 'DECIDE automatic-replay effect ledger');
574
+ return Object.freeze({
575
+ allowsEmptyOkCorrection(runtimeSessionId, callId) {
576
+ const matching = readLedger().boundaries.filter((boundary) => boundary.runtimeSessionId === runtimeSessionId &&
577
+ boundary.callId === callId);
578
+ return (matching.length === 1 && hasCompleteUnchangedReceipt(matching[0]));
579
+ },
580
+ allowsFailureStateRetry() {
581
+ const ledger = readLedger();
582
+ const latest = ledger.boundaries.at(-1);
583
+ if (latest === undefined)
584
+ return false;
585
+ const attempt = ledger.boundaries.filter((boundary) => boundary.attemptId === latest.attemptId);
586
+ return (attempt.length > 0 && attempt.every(hasCompleteUnchangedReceipt));
587
+ },
588
+ });
589
+ }
410
590
  function isAbortFailure(error, signal) {
411
591
  return signal.aborted && Object.is(error, signal.reason);
412
592
  }
@@ -510,8 +690,8 @@ function normalizedTransitionEvent(event) {
510
690
  }
511
691
  return snapshotJsonValue(descriptor, 'FSM event');
512
692
  }
513
- function telemetryPayload(previousState, state, event, context) {
514
- const pendingBossQuestions = pendingQuestionsForState(state, context);
693
+ function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
694
+ const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
515
695
  const prior = previousState ?? state;
516
696
  const payload = {
517
697
  from: prior.value,
@@ -527,8 +707,13 @@ function telemetryPayload(previousState, state, event, context) {
527
707
  assertJsonSafe(payload);
528
708
  return payload;
529
709
  }
530
- export const createPlaybookRuntime = (options) => {
710
+ function createDecidePlaybookRuntime(options, deferredEffects) {
711
+ const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
531
712
  const fsmInput = snapshotDecideRuntimeOptions(options);
713
+ const readEffectLedger = () => assertPlaybookEffectLedger(deferredEffects.effectLedger.snapshot(), 'DECIDE current host effect ledger');
714
+ let effectLedgerMirror = readEffectLedger();
715
+ const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer((source, acceptedOutcome) => Object.prototype.hasOwnProperty.call(ACCEPTED_OUTCOME_DECLARATIONS, source) &&
716
+ ACCEPTED_OUTCOME_DECLARATIONS[source]?.has(acceptedOutcome) === true);
532
717
  let ports;
533
718
  let sessionIdentity;
534
719
  let actor;
@@ -545,6 +730,7 @@ export const createPlaybookRuntime = (options) => {
545
730
  let judgeCallSequence = 0;
546
731
  let playerCallSequence = 0;
547
732
  let playbookCallSequence = 0;
733
+ let applyCallSequence = 0;
548
734
  let lifecycleStarted = false;
549
735
  let initInFlight;
550
736
  let disposed = false;
@@ -558,6 +744,23 @@ export const createPlaybookRuntime = (options) => {
558
744
  const activeEmissionCalls = new Set();
559
745
  const emissionQueue = new PQueue({ concurrency: 1 });
560
746
  const judgeQueue = new PQueue({ concurrency: 1 });
747
+ // A proposal cohort completes both semantic callbacks concurrently at the
748
+ // repository seam. Serialize each whole adjudication/correction transaction
749
+ // so their durable correction-budget compare-and-swaps cannot race.
750
+ const semanticCompletionQueue = new PQueue({ concurrency: 1 });
751
+ const governedOutputsByBoundaryId = new Map();
752
+ const governedFailuresByBoundaryId = new Map();
753
+ const governedEvidenceByBoundaryId = new Map();
754
+ const governedReceiptsByBoundaryId = new Map();
755
+ const governedPlayerOutputs = new WeakMap();
756
+ const unresolvedSemanticBoundaryIds = new Set();
757
+ const appliedControlReceipts = new Map();
758
+ const pendingProposalCohort = new Map();
759
+ let activeProposalCohort;
760
+ let completedProposalCohortTurnId;
761
+ let deferredOperationId;
762
+ let hiddenDeferredOperationId;
763
+ let activeDeferredContinuation;
561
764
  const collectFailure = (failures, error) => {
562
765
  if (error instanceof AggregateError) {
563
766
  for (const nested of error.errors)
@@ -647,6 +850,9 @@ export const createPlaybookRuntime = (options) => {
647
850
  };
648
851
  const bindSession = (nextSession) => {
649
852
  const bound = snapshotPlaybookSession(nextSession);
853
+ if (bound.playbookId !== deferredEffects.authority.playbookId) {
854
+ throw new TypeError('DECIDE runtime playbook identity must match its bound schema-3 host authority');
855
+ }
650
856
  if (bound.roleBindings === undefined)
651
857
  return bound;
652
858
  const actual = Object.keys(bound.roleBindings).sort();
@@ -779,15 +985,216 @@ export const createPlaybookRuntime = (options) => {
779
985
  pendingCall,
780
986
  });
781
987
  };
988
+ const visiblePendingQuestionsForState = (state, context) => pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== 'commitCoderProposal' ||
989
+ hiddenDeferredOperationId === undefined);
990
+ const openDeferredOperation = (ledger = effectLedgerMirror) => {
991
+ if (sessionIdentity === undefined)
992
+ return undefined;
993
+ const open = ledger.logicalOperations.filter((operation) => operation.playbookId === sessionIdentity.playbookId &&
994
+ operation.runtimeSessionId === sessionIdentity.sessionId &&
995
+ operation.logicalReceipt === undefined);
996
+ if (open.length > 1) {
997
+ throw new TypeError('DECIDE runtime has multiple open deferred logical operations');
998
+ }
999
+ return open[0];
1000
+ };
1001
+ const runtimeBoundaryIsOwned = (boundary) => sessionIdentity !== undefined &&
1002
+ boundary.playbookId === sessionIdentity.playbookId &&
1003
+ boundary.runtimeSessionId === sessionIdentity.sessionId;
1004
+ const governedOutcomesForBoundary = (boundary) => {
1005
+ const outcomes = DECIDE_OUTCOME_AUTHORITY.governedPlayerStates[boundary.sourceStateId];
1006
+ if (outcomes === undefined ||
1007
+ !isPlainObject(boundary.sourceOutcomeSchema)) {
1008
+ return undefined;
1009
+ }
1010
+ const authoredGuards = Object.keys(boundary.sourceOutcomeSchema).sort();
1011
+ const governedGuards = Object.keys(outcomes).sort();
1012
+ if (authoredGuards.length !== governedGuards.length ||
1013
+ authoredGuards.some((guard, index) => guard !== governedGuards[index])) {
1014
+ return undefined;
1015
+ }
1016
+ for (const guard of governedGuards) {
1017
+ const description = boundary.sourceOutcomeSchema[guard];
1018
+ if (typeof description !== 'string')
1019
+ return undefined;
1020
+ const authoredFields = [
1021
+ ...new Set(requiredFieldsFor(description)),
1022
+ ].sort();
1023
+ const governedFields = Object.keys(outcomes[guard].fields).sort();
1024
+ if (authoredFields.length !== governedFields.length ||
1025
+ authoredFields.some((field, index) => field !== governedFields[index])) {
1026
+ return undefined;
1027
+ }
1028
+ }
1029
+ const dispositions = [
1030
+ ...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
1031
+ ];
1032
+ const actualDispositions = new Set(boundary.dispositions);
1033
+ return actualDispositions.size === boundary.dispositions.length &&
1034
+ actualDispositions.size === dispositions.length &&
1035
+ dispositions.every((disposition) => actualDispositions.has(disposition))
1036
+ ? outcomes
1037
+ : undefined;
1038
+ };
1039
+ const persistedBoundaryReconciliation = (boundary, ledger) => {
1040
+ const outcomes = governedOutcomesForBoundary(boundary);
1041
+ if (outcomes === undefined || boundary.semanticCandidate === undefined) {
1042
+ return undefined;
1043
+ }
1044
+ let receipt = boundary.physicalReceipt;
1045
+ let awaitingLogicalReceipt = false;
1046
+ let historicalDeferred = false;
1047
+ const logicalOperation = boundary.logicalOperationId === undefined
1048
+ ? undefined
1049
+ : ledger.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
1050
+ if (boundary.logicalOperationId !== undefined &&
1051
+ logicalOperation === undefined) {
1052
+ return undefined;
1053
+ }
1054
+ if (logicalOperation !== undefined) {
1055
+ if (logicalOperation.boundaryIds.at(-1) !== boundary.boundaryId) {
1056
+ historicalDeferred = true;
1057
+ }
1058
+ else if (logicalOperation.logicalReceipt !== undefined) {
1059
+ receipt = logicalOperation.logicalReceipt;
1060
+ }
1061
+ else if (logicalOperation.pendingQuestion === undefined ||
1062
+ logicalOperation.checkpoint === undefined ||
1063
+ !Object.prototype.hasOwnProperty.call(logicalOperation, 'playerContinuation')) {
1064
+ return undefined;
1065
+ }
1066
+ else {
1067
+ awaitingLogicalReceipt = true;
1068
+ }
1069
+ }
1070
+ try {
1071
+ const reconciliation = reconcilePlaybookSemanticEvidence({
1072
+ outcomes,
1073
+ semanticCandidate: boundary.semanticCandidate,
1074
+ finalText: boundary.finalText,
1075
+ receipt,
1076
+ });
1077
+ if (awaitingLogicalReceipt && reconciliation.status !== 'deferred') {
1078
+ return undefined;
1079
+ }
1080
+ return { reconciliation, historicalDeferred };
1081
+ }
1082
+ catch {
1083
+ return undefined;
1084
+ }
1085
+ };
1086
+ const boundaryNeedsSemanticReconciliation = (boundary, ledger) => {
1087
+ if (!runtimeBoundaryIsOwned(boundary))
1088
+ return false;
1089
+ if (governedOutcomesForBoundary(boundary) === undefined)
1090
+ return true;
1091
+ const persisted = persistedBoundaryReconciliation(boundary, ledger);
1092
+ if (persisted !== undefined) {
1093
+ if (persisted.reconciliation.status === 'unresolved')
1094
+ return true;
1095
+ if (persisted.historicalDeferred) {
1096
+ return persisted.reconciliation.status !== 'deferred';
1097
+ }
1098
+ if (persisted.reconciliation.status === 'deferred' &&
1099
+ boundary.logicalOperationId === undefined) {
1100
+ return true;
1101
+ }
1102
+ return false;
1103
+ }
1104
+ if (boundary.physicalReceipt === undefined)
1105
+ return true;
1106
+ if (typeof boundary.finalText === 'string' &&
1107
+ boundary.finalText.trim().length > 0) {
1108
+ return true;
1109
+ }
1110
+ return boundary.physicalReceipt.classification !== 'unchanged';
1111
+ };
1112
+ const refreshUnresolvedSemanticReconciliation = (ledger = effectLedgerMirror) => {
1113
+ unresolvedSemanticBoundaryIds.clear();
1114
+ for (const boundary of ledger.boundaries) {
1115
+ if (boundaryNeedsSemanticReconciliation(boundary, ledger)) {
1116
+ unresolvedSemanticBoundaryIds.add(boundary.boundaryId);
1117
+ }
1118
+ }
1119
+ };
1120
+ const synchronizeDeferredProjection = (ledger = effectLedgerMirror) => {
1121
+ if (deferredEffects === undefined)
1122
+ return;
1123
+ effectLedgerMirror = ledger;
1124
+ refreshUnresolvedSemanticReconciliation(ledger);
1125
+ const operation = openDeferredOperation(ledger);
1126
+ if (operation === undefined) {
1127
+ deferredOperationId = undefined;
1128
+ hiddenDeferredOperationId = undefined;
1129
+ return;
1130
+ }
1131
+ deferredOperationId = operation.operationId;
1132
+ hiddenDeferredOperationId =
1133
+ operation.logicalReceipt === undefined &&
1134
+ (operation.checkpointRestorationEligible ||
1135
+ operation.pendingQuestion === undefined)
1136
+ ? operation.operationId
1137
+ : undefined;
1138
+ };
1139
+ const hasUnresolvedReconciliation = () => hiddenDeferredOperationId !== undefined ||
1140
+ unresolvedSemanticBoundaryIds.size > 0;
1141
+ const refreshReconciliationProjection = () => {
1142
+ synchronizeDeferredProjection(readEffectLedger());
1143
+ };
1144
+ const unresolvedEffectEnvelopeIdentities = () => {
1145
+ if (sessionIdentity === undefined)
1146
+ return [];
1147
+ refreshReconciliationProjection();
1148
+ if (!hasUnresolvedReconciliation())
1149
+ return [];
1150
+ const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
1151
+ const operationIds = new Set();
1152
+ if (hiddenDeferredOperationId !== undefined) {
1153
+ operationIds.add(hiddenDeferredOperationId);
1154
+ }
1155
+ for (const boundaryId of [...boundaryIds]) {
1156
+ const boundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
1157
+ const operation = boundary?.logicalOperationId === undefined
1158
+ ? undefined
1159
+ : effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
1160
+ if (operation === undefined)
1161
+ continue;
1162
+ operationIds.add(operation.operationId);
1163
+ for (const memberId of operation.boundaryIds) {
1164
+ boundaryIds.delete(memberId);
1165
+ }
1166
+ }
1167
+ const ordered = [
1168
+ ...[...boundaryIds].map((boundaryId) => ({
1169
+ order: effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
1170
+ value: { kind: 'boundary', boundaryId },
1171
+ })),
1172
+ ...[...operationIds].map((operationId) => {
1173
+ const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
1174
+ return {
1175
+ order: effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === operation?.boundaryIds[0])?.sequence ?? Number.MAX_SAFE_INTEGER,
1176
+ value: { kind: 'logical-operation', operationId },
1177
+ };
1178
+ }),
1179
+ ].sort((left, right) => left.order - right.order);
1180
+ return deepFreeze(snapshotJsonValue(ordered.map(({ value }) => value), 'DECIDE unresolved effect envelope identities'));
1181
+ };
782
1182
  const stateIdentity = (state) => {
783
1183
  return state.stateId === undefined ? {} : { stateId: state.stateId };
784
1184
  };
785
1185
  const enqueueTracedEmission = (type, payload, meta = {}, describedEmission, aborts) => {
786
- const runtimePorts = requirePorts();
1186
+ const trace = createTraceEvent(type, payload, meta);
1187
+ return enqueue(async () => {
1188
+ const runtimePorts = requirePorts();
1189
+ await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
1190
+ await describedEmission?.(runtimePorts);
1191
+ }, aborts);
1192
+ };
1193
+ const createTraceEvent = (type, payload, meta = {}) => {
787
1194
  const identity = requireSessionIdentity();
788
1195
  const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
789
- const trace = Object.freeze({
790
- schemaVersion: 3,
1196
+ return Object.freeze({
1197
+ schemaVersion: 4,
791
1198
  sessionId: identity.sessionId,
792
1199
  playbookId: identity.playbookId,
793
1200
  rootSessionId: identity.rootSessionId,
@@ -805,12 +1212,25 @@ export const createPlaybookRuntime = (options) => {
805
1212
  ...(meta.callId !== undefined ? { callId: meta.callId } : {}),
806
1213
  payload: jsonPayload,
807
1214
  });
1215
+ };
1216
+ const emitTrace = (type, payload, meta = {}, aborts) => enqueueTracedEmission(type, payload, meta, undefined, aborts);
1217
+ const enqueueAcceptedOutcomeEmission = (acceptedOutcome, state, aborts) => {
1218
+ const message = `→ ${acceptedOutcome.acceptedOutcome}`;
1219
+ const acceptedTrace = createTraceEvent('outcome.accepted', acceptedOutcome, { turnId: currentTurnId });
1220
+ const statusTrace = createTraceEvent('status.emitted', { stateId: acceptedOutcome.target, message, state }, { turnId: currentTurnId });
808
1221
  return enqueue(async () => {
809
- await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
810
- await describedEmission?.(runtimePorts);
1222
+ const runtimePorts = requirePorts();
1223
+ await runtimePorts.emitTelemetry({
1224
+ topic: TRACE_TOPIC,
1225
+ payload: acceptedTrace,
1226
+ });
1227
+ await runtimePorts.emitTelemetry({
1228
+ topic: TRACE_TOPIC,
1229
+ payload: statusTrace,
1230
+ });
1231
+ await runtimePorts.emitStatus(message);
811
1232
  }, aborts);
812
1233
  };
813
- const emitTrace = (type, payload, meta = {}, aborts) => enqueueTracedEmission(type, payload, meta, undefined, aborts);
814
1234
  const emitBoundaryStatus = async (message, state) => {
815
1235
  const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
816
1236
  await enqueueTracedEmission('status.emitted', {
@@ -896,7 +1316,7 @@ export const createPlaybookRuntime = (options) => {
896
1316
  return queued;
897
1317
  };
898
1318
  const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
899
- const runPlayerCall = async (input, signal) => {
1319
+ const runPlayerCall = async (input, signal, continuation) => {
900
1320
  const aborts = abortReasonClassifier(signal);
901
1321
  if (!ROLE_ID_SET.has(input.role)) {
902
1322
  throw new TypeError(`DECIDE player input role must name a declared local role`);
@@ -908,13 +1328,17 @@ export const createPlaybookRuntime = (options) => {
908
1328
  let resume;
909
1329
  try {
910
1330
  signal.throwIfAborted();
911
- resume = selectPlayerResume(roleId, playerId);
1331
+ resume =
1332
+ continuation !== undefined &&
1333
+ Object.prototype.hasOwnProperty.call(continuation, 'resume')
1334
+ ? continuation.resume
1335
+ : selectPlayerResume(roleId, playerId);
912
1336
  }
913
1337
  catch (error) {
914
1338
  latchControlPlaneError(error, signal);
915
1339
  throw error;
916
1340
  }
917
- const callId = `player-${++playerCallSequence}`;
1341
+ const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
918
1342
  const identity = {
919
1343
  stateId: input.stateId,
920
1344
  sourceItem: input.sourceItem,
@@ -1013,6 +1437,7 @@ export const createPlaybookRuntime = (options) => {
1013
1437
  return {
1014
1438
  roleId,
1015
1439
  ...(playerId === undefined ? {} : { playerId }),
1440
+ callId,
1016
1441
  result,
1017
1442
  };
1018
1443
  }
@@ -1020,8 +1445,590 @@ export const createPlaybookRuntime = (options) => {
1020
1445
  inFlightPlayerKeys.delete(playerKey);
1021
1446
  }
1022
1447
  };
1448
+ const governedBoundarySeed = (input, callId) => {
1449
+ const outcomes = governedOutcomesFor(input);
1450
+ if (currentTurnId === undefined ||
1451
+ !Number.isSafeInteger(currentTurnId) ||
1452
+ currentTurnId <= 0) {
1453
+ throw new Error('DECIDE governed player call requires an active positive turn id');
1454
+ }
1455
+ return {
1456
+ boundaryId: randomUUID(),
1457
+ runtimeSessionId: requireSessionIdentity().sessionId,
1458
+ turnId: currentTurnId,
1459
+ callId,
1460
+ roleId: input.role,
1461
+ sourceStateId: input.stateId,
1462
+ sourceOutcomeSchema: snapshotJsonValue(input.result, 'DECIDE governed player source outcome schema'),
1463
+ dispositions: [
1464
+ ...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
1465
+ ],
1466
+ correctionBudget: { limit: 1, spent: false },
1467
+ };
1468
+ };
1469
+ const unresolvedGovernedEvidence = (boundaryId, reason, error) => {
1470
+ governedFailuresByBoundaryId.set(boundaryId, error instanceof Error
1471
+ ? error
1472
+ : new Error(`DECIDE governed outcome remains unresolved: ${reason}`));
1473
+ };
1474
+ const spendSemanticCorrectionBudget = async (boundary, receipt, finalText, semanticCandidate) => {
1475
+ const ledger = readEffectLedger();
1476
+ const current = ledger.boundaries.find(({ boundaryId }) => boundaryId === boundary.boundaryId);
1477
+ if (current === undefined ||
1478
+ current.correctionBudget.limit !== 1 ||
1479
+ current.correctionBudget.spent) {
1480
+ return false;
1481
+ }
1482
+ if (current.finalText !== undefined && current.finalText !== finalText) {
1483
+ throw new TypeError('DECIDE correction budget conflicts with retained finalText');
1484
+ }
1485
+ if (current.physicalReceipt !== undefined &&
1486
+ stableJson(current.physicalReceipt, 'DECIDE retained receipt') !==
1487
+ stableJson(receipt, 'DECIDE correction receipt')) {
1488
+ throw new TypeError('DECIDE correction budget conflicts with retained repository receipt');
1489
+ }
1490
+ if (semanticCandidate !== undefined &&
1491
+ current.semanticCandidate !== undefined &&
1492
+ stableJson(current.semanticCandidate, 'DECIDE retained candidate') !==
1493
+ stableJson(semanticCandidate, 'DECIDE correction candidate')) {
1494
+ throw new TypeError('DECIDE correction budget conflicts with retained semantic candidate');
1495
+ }
1496
+ const next = {
1497
+ ...current,
1498
+ ...(receipt.after === undefined ? {} : { after: receipt.after }),
1499
+ physicalReceipt: receipt,
1500
+ finalText,
1501
+ ...(semanticCandidate === undefined ? {} : { semanticCandidate }),
1502
+ correctionBudget: { limit: 1, spent: true },
1503
+ };
1504
+ const cohortMembers = current.cohortId === undefined
1505
+ ? [current]
1506
+ : ledger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
1507
+ if (cohortMembers.length === 0) {
1508
+ throw new TypeError('DECIDE correction boundary lost its repository cohort');
1509
+ }
1510
+ const replacements = cohortMembers.map((member) => ({
1511
+ expected: member,
1512
+ next: member.boundaryId === current.boundaryId
1513
+ ? next
1514
+ : {
1515
+ ...member,
1516
+ ...(receipt.after === undefined
1517
+ ? {}
1518
+ : { after: receipt.after }),
1519
+ physicalReceipt: receipt,
1520
+ },
1521
+ }));
1522
+ const acknowledged = assertPlaybookEffectLedger(await deferredEffects.effectLedger.writeAhead([
1523
+ {
1524
+ kind: 'replace-boundaries',
1525
+ // A cohort's shared receipt must become visible for every member in
1526
+ // one ledger revision. Publishing only this member's correction
1527
+ // spend would transiently create an invalid half-complete cohort.
1528
+ replacements,
1529
+ },
1530
+ ]), 'DECIDE semantic correction budget acknowledgement');
1531
+ synchronizeDeferredProjection(acknowledged);
1532
+ const spent = acknowledged.boundaries.find(({ boundaryId }) => boundaryId === boundary.boundaryId);
1533
+ if (spent === undefined ||
1534
+ stableJson(spent, 'DECIDE acknowledged correction boundary') !==
1535
+ stableJson(next, 'DECIDE expected correction boundary')) {
1536
+ throw new TypeError('DECIDE semantic correction budget spend was not acknowledged exactly');
1537
+ }
1538
+ return true;
1539
+ };
1540
+ const completionEvidenceFor = (input, roleId, playerId, signal, operationId) => async (completion) => {
1541
+ const queued = await semanticCompletionQueue.add(async () => {
1542
+ const { boundary, operation } = completion;
1543
+ const outcomes = governedOutcomesFor(input);
1544
+ const expectedDispositions = [
1545
+ ...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
1546
+ ];
1547
+ const session = requireSessionIdentity();
1548
+ if (boundary.playbookId !== session.playbookId ||
1549
+ boundary.runtimeSessionId !== session.sessionId ||
1550
+ boundary.turnId !== currentTurnId ||
1551
+ boundary.roleId !== roleId ||
1552
+ (completion.roleId !== undefined && completion.roleId !== roleId) ||
1553
+ boundary.sourceStateId !== input.stateId ||
1554
+ stableJson(boundary.dispositions, 'DECIDE boundary dispositions') !==
1555
+ stableJson(expectedDispositions, 'DECIDE authority dispositions') ||
1556
+ stableJson(boundary.sourceOutcomeSchema, 'DECIDE boundary source schema') !==
1557
+ stableJson(input.result, 'DECIDE authored source schema')) {
1558
+ throw new TypeError('DECIDE governed semantic reconciliation source schema changed');
1559
+ }
1560
+ governedReceiptsByBoundaryId.set(boundary.boundaryId, {
1561
+ physicalReceipt: completion.receipt,
1562
+ outcomeReceipt: completion.outcomeReceipt,
1563
+ });
1564
+ if (operation.status !== 'fulfilled' ||
1565
+ operation.value.status !== 'ok' ||
1566
+ isEmptyFinalText(operation.value.finalText)) {
1567
+ const incomplete = operation.status === 'fulfilled' &&
1568
+ operation.value.status === 'ok' &&
1569
+ operation.value.finalText !== undefined
1570
+ ? { finalText: operation.value.finalText }
1571
+ : {};
1572
+ if (operation.status === 'fulfilled' &&
1573
+ operation.value.status === 'ok' &&
1574
+ operation.value.finalText !== undefined) {
1575
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
1576
+ finalText: operation.value.finalText,
1577
+ });
1578
+ }
1579
+ return operationId === undefined
1580
+ ? incomplete
1581
+ : { ...incomplete, unresolved: true };
1582
+ }
1583
+ const finalText = operation.value.finalText;
1584
+ let raw;
1585
+ try {
1586
+ raw = await callJudge(buildAdjudicatorPrompt(input, finalText), signal, 'player-output-adjudication', input.stateId);
1587
+ }
1588
+ catch (error) {
1589
+ unresolvedGovernedEvidence(boundary.boundaryId, 'judge transport failed', error);
1590
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, { finalText });
1591
+ return { finalText, unresolved: true };
1592
+ }
1593
+ let candidate;
1594
+ let retainedCandidate;
1595
+ let reconciliation;
1596
+ let structuralError;
1597
+ const retainCandidate = (value) => {
1598
+ try {
1599
+ retainedCandidate = snapshotJsonValue(value, 'DECIDE recoverable governed semantic candidate');
1600
+ }
1601
+ catch {
1602
+ // A non-detachable reply still retains presentation and receipt.
1603
+ }
1604
+ };
1605
+ try {
1606
+ candidate = parseGovernedSemanticCandidate(raw);
1607
+ retainCandidate(candidate);
1608
+ reconciliation = reconcilePlaybookSemanticEvidence({
1609
+ outcomes,
1610
+ semanticCandidate: candidate,
1611
+ finalText,
1612
+ receipt: completion.outcomeReceipt,
1613
+ });
1614
+ }
1615
+ catch (error) {
1616
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
1617
+ throw error;
1618
+ }
1619
+ structuralError = error;
1620
+ }
1621
+ if (structuralError !== undefined) {
1622
+ const spent = await spendSemanticCorrectionBudget(boundary, completion.receipt, finalText, retainedCandidate);
1623
+ if (!spent || signal.aborted) {
1624
+ unresolvedGovernedEvidence(boundary.boundaryId, 'semantic correction budget is unavailable', signal.aborted ? signal.reason : undefined);
1625
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
1626
+ finalText,
1627
+ ...(retainedCandidate === undefined
1628
+ ? {}
1629
+ : { semanticCandidate: retainedCandidate }),
1630
+ });
1631
+ return {
1632
+ finalText,
1633
+ ...(retainedCandidate === undefined
1634
+ ? {}
1635
+ : { semanticCandidate: retainedCandidate }),
1636
+ unresolved: true,
1637
+ };
1638
+ }
1639
+ let correctiveRaw;
1640
+ try {
1641
+ correctiveRaw = await callJudge(buildAdjudicatorPrompt(input, finalText, {
1642
+ reply: raw,
1643
+ error: structuralError.message,
1644
+ }), signal, 'player-output-adjudication', input.stateId);
1645
+ }
1646
+ catch (error) {
1647
+ unresolvedGovernedEvidence(boundary.boundaryId, 'corrective judge failed', error);
1648
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
1649
+ finalText,
1650
+ ...(retainedCandidate === undefined
1651
+ ? {}
1652
+ : { semanticCandidate: retainedCandidate }),
1653
+ });
1654
+ return {
1655
+ finalText,
1656
+ ...(retainedCandidate === undefined
1657
+ ? {}
1658
+ : { semanticCandidate: retainedCandidate }),
1659
+ unresolved: true,
1660
+ };
1661
+ }
1662
+ try {
1663
+ candidate = parseGovernedSemanticCandidate(correctiveRaw);
1664
+ retainCandidate(candidate);
1665
+ reconciliation = reconcilePlaybookSemanticEvidence({
1666
+ outcomes,
1667
+ semanticCandidate: candidate,
1668
+ finalText,
1669
+ receipt: completion.outcomeReceipt,
1670
+ });
1671
+ }
1672
+ catch (error) {
1673
+ if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
1674
+ throw error;
1675
+ }
1676
+ unresolvedGovernedEvidence(boundary.boundaryId, 'corrective semantic candidate is invalid');
1677
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
1678
+ finalText,
1679
+ ...(retainedCandidate === undefined
1680
+ ? {}
1681
+ : { semanticCandidate: retainedCandidate }),
1682
+ });
1683
+ return {
1684
+ finalText,
1685
+ ...(retainedCandidate === undefined
1686
+ ? {}
1687
+ : { semanticCandidate: retainedCandidate }),
1688
+ unresolved: true,
1689
+ };
1690
+ }
1691
+ }
1692
+ if (reconciliation === undefined) {
1693
+ throw new Error('DECIDE semantic reconciliation produced no decision');
1694
+ }
1695
+ const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, 'DECIDE governed semantic candidate');
1696
+ governedEvidenceByBoundaryId.set(boundary.boundaryId, {
1697
+ finalText,
1698
+ semanticCandidate,
1699
+ });
1700
+ if (reconciliation.status === 'unresolved') {
1701
+ unresolvedGovernedEvidence(boundary.boundaryId, reconciliation.reason);
1702
+ return { finalText, semanticCandidate, unresolved: true };
1703
+ }
1704
+ const output = reconciliation.output;
1705
+ governedOutputsByBoundaryId.set(boundary.boundaryId, output);
1706
+ if (reconciliation.status !== 'deferred') {
1707
+ return { finalText, semanticCandidate };
1708
+ }
1709
+ if (output.guard !== 'needsBossReply' ||
1710
+ typeof output.question !== 'string' ||
1711
+ output.question.trim() === '') {
1712
+ throw new TypeError('DECIDE deferred outcome must carry one exact Boss question');
1713
+ }
1714
+ const pendingQuestion = {
1715
+ questionId: input.stateId,
1716
+ asker: { kind: 'role', roleId },
1717
+ question: output.question,
1718
+ sourceItem: input.sourceItem,
1719
+ };
1720
+ return {
1721
+ finalText,
1722
+ semanticCandidate,
1723
+ deferred: {
1724
+ operationId: operationId ?? randomUUID(),
1725
+ pendingQuestion,
1726
+ playerContinuation: snapshotJsonValue(selectPlayerResume(roleId, playerId), 'DECIDE deferred player continuation'),
1727
+ },
1728
+ };
1729
+ });
1730
+ if (queued === undefined) {
1731
+ throw new Error('DECIDE semantic completion produced no evidence');
1732
+ }
1733
+ return queued;
1734
+ };
1735
+ const acknowledgeGovernedPlayerResult = (value, boundaryId) => {
1736
+ const ledger = assertPlaybookEffectLedger(value.effectLedger, 'DECIDE repository settlement effect ledger');
1737
+ const completed = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
1738
+ if (completed === undefined ||
1739
+ completed.physicalReceipt === undefined ||
1740
+ stableJson(completed.physicalReceipt, 'DECIDE completed receipt') !==
1741
+ stableJson(value.receipt, 'DECIDE acknowledged receipt')) {
1742
+ throw new TypeError('DECIDE repository settlement did not acknowledge its completed boundary');
1743
+ }
1744
+ const expectedEvidence = governedEvidenceByBoundaryId.get(boundaryId);
1745
+ const expectedReceipts = governedReceiptsByBoundaryId.get(boundaryId);
1746
+ if (expectedReceipts !== undefined &&
1747
+ stableJson(completed.physicalReceipt, 'DECIDE completed physical receipt') !==
1748
+ stableJson(expectedReceipts.physicalReceipt, 'DECIDE reconciled physical receipt')) {
1749
+ throw new TypeError('DECIDE repository settlement changed the physical receipt used during reconciliation');
1750
+ }
1751
+ if (expectedEvidence !== undefined &&
1752
+ (completed.finalText !== expectedEvidence.finalText ||
1753
+ (expectedEvidence.semanticCandidate === undefined
1754
+ ? completed.semanticCandidate !== undefined
1755
+ : completed.semanticCandidate === undefined ||
1756
+ stableJson(completed.semanticCandidate, 'DECIDE completed semantic candidate') !==
1757
+ stableJson(expectedEvidence.semanticCandidate, 'DECIDE expected semantic candidate')))) {
1758
+ throw new TypeError('DECIDE repository settlement did not acknowledge its exact governed evidence');
1759
+ }
1760
+ governedEvidenceByBoundaryId.delete(boundaryId);
1761
+ governedReceiptsByBoundaryId.delete(boundaryId);
1762
+ synchronizeDeferredProjection(ledger);
1763
+ if (value.operation.status === 'rejected') {
1764
+ throw value.operation.reason;
1765
+ }
1766
+ const result = validatePlayerResult(value.operation.value);
1767
+ const output = governedOutputsByBoundaryId.get(boundaryId);
1768
+ const failure = governedFailuresByBoundaryId.get(boundaryId);
1769
+ if (expectedReceipts !== undefined) {
1770
+ const continued = 'status' in value && value.status === 'continued'
1771
+ ? value
1772
+ : undefined;
1773
+ const acknowledgedOutcomeReceipt = continued === undefined ? value.receipt : continued.logicalReceipt;
1774
+ if (acknowledgedOutcomeReceipt !== undefined &&
1775
+ stableJson(acknowledgedOutcomeReceipt, 'DECIDE acknowledged outcome receipt') !==
1776
+ stableJson(expectedReceipts.outcomeReceipt, 'DECIDE reconciled outcome receipt')) {
1777
+ throw new TypeError('DECIDE repository settlement changed the outcome receipt used during reconciliation');
1778
+ }
1779
+ if (continued !== undefined &&
1780
+ output !== undefined &&
1781
+ output.guard !== 'needsBossReply' &&
1782
+ acknowledgedOutcomeReceipt === undefined) {
1783
+ throw new TypeError('DECIDE completed deferred continuation omitted its reconciled logical receipt');
1784
+ }
1785
+ }
1786
+ governedOutputsByBoundaryId.delete(boundaryId);
1787
+ governedFailuresByBoundaryId.delete(boundaryId);
1788
+ const linkedOperationId = completed.logicalOperationId;
1789
+ if (value.deferredStatus === 'unresolved') {
1790
+ if (linkedOperationId === undefined) {
1791
+ throw new TypeError('DECIDE unresolved deferred settlement omitted its logical operation');
1792
+ }
1793
+ deferredOperationId = linkedOperationId;
1794
+ hiddenDeferredOperationId = linkedOperationId;
1795
+ }
1796
+ else if (value.deferredStatus === 'bound') {
1797
+ if (linkedOperationId === undefined) {
1798
+ throw new TypeError('DECIDE bound deferred settlement omitted its logical operation');
1799
+ }
1800
+ deferredOperationId = linkedOperationId;
1801
+ hiddenDeferredOperationId = undefined;
1802
+ }
1803
+ else if ('status' in value &&
1804
+ value.status === 'continued' &&
1805
+ value
1806
+ .logicalReceipt === undefined &&
1807
+ linkedOperationId !== undefined) {
1808
+ deferredOperationId = linkedOperationId;
1809
+ hiddenDeferredOperationId = linkedOperationId;
1810
+ }
1811
+ else if (linkedOperationId !== undefined) {
1812
+ deferredOperationId = undefined;
1813
+ hiddenDeferredOperationId = undefined;
1814
+ }
1815
+ if (failure !== undefined)
1816
+ throw failure;
1817
+ if (value.deferredStatus === 'unresolved') {
1818
+ throw new Error('DECIDE deferred repository settlement remains unresolved');
1819
+ }
1820
+ if (output !== undefined) {
1821
+ governedPlayerOutputs.set(result, output);
1822
+ }
1823
+ else if (result.status === 'ok' &&
1824
+ !isEmptyFinalText(result.finalText)) {
1825
+ throw new Error('DECIDE governed player result has no reconciled semantic output');
1826
+ }
1827
+ return result;
1828
+ };
1829
+ const shouldRunProposalCohort = (input) => {
1830
+ if (input.stateId !== PROPOSAL_STATE_BY_ROLE[input.role] ||
1831
+ completedProposalCohortTurnId === currentTurnId) {
1832
+ return false;
1833
+ }
1834
+ const state = currentState();
1835
+ return Object.values(PROPOSAL_STATE_BY_ROLE).every((stateId) => state.activeStateIds.includes(stateId));
1836
+ };
1837
+ const settleProposalCohort = async () => {
1838
+ const members = Object.fromEntries(ROLE_IDS.map((roleId) => [roleId, pendingProposalCohort.get(roleId)]));
1839
+ const turnId = currentTurnId;
1840
+ const cohortSignal = currentSignal;
1841
+ const cohortOperations = new AbortController();
1842
+ const completionOrder = [];
1843
+ try {
1844
+ if (turnId === undefined ||
1845
+ cohortSignal === undefined ||
1846
+ members.coder === undefined ||
1847
+ members.reviewer === undefined) {
1848
+ throw new Error('DECIDE proposal cohort started without both governed members');
1849
+ }
1850
+ for (const roleId of ROLE_IDS)
1851
+ members[roleId].signal.throwIfAborted();
1852
+ const operations = Object.fromEntries(ROLE_IDS.map((roleId) => [
1853
+ roleId,
1854
+ async () => {
1855
+ const member = members[roleId];
1856
+ try {
1857
+ const operationSignal = combineSignals(member.signal, cohortOperations.signal);
1858
+ const envelope = await runPlayerCall(member.input, operationSignal, { callId: member.callId });
1859
+ member.envelope = envelope;
1860
+ if (envelope.result.status !== 'ok' &&
1861
+ !cohortOperations.signal.aborted) {
1862
+ cohortOperations.abort(new Error(`DECIDE proposal cohort ${roleId} returned status ${JSON.stringify(envelope.result.status)}`));
1863
+ }
1864
+ return envelope.result;
1865
+ }
1866
+ catch (error) {
1867
+ if (!cohortOperations.signal.aborted) {
1868
+ cohortOperations.abort(error);
1869
+ }
1870
+ throw error;
1871
+ }
1872
+ finally {
1873
+ completionOrder.push(roleId);
1874
+ }
1875
+ },
1876
+ ]));
1877
+ const invocationId = randomUUID();
1878
+ const settled = await deferredEffects.repository.runCohort({
1879
+ signal: cohortSignal,
1880
+ invocationId,
1881
+ roleIds: ROLE_IDS,
1882
+ dispositionsByRole: {
1883
+ coder: ['unchanged'],
1884
+ reviewer: ['unchanged'],
1885
+ },
1886
+ effectBoundaries: {
1887
+ coder: members.coder.effectBoundary,
1888
+ reviewer: members.reviewer.effectBoundary,
1889
+ },
1890
+ operations,
1891
+ completeEffectBoundary: (completion) => {
1892
+ const member = members[completion.roleId];
1893
+ if (member === undefined) {
1894
+ throw new TypeError(`DECIDE proposal cohort completed unknown role ${String(completion.roleId)}`);
1895
+ }
1896
+ return completionEvidenceFor(member.input, completion.roleId, resolvedPlayerId(completion.roleId), member.signal)(completion);
1897
+ },
1898
+ });
1899
+ if (settled.invocationId !== invocationId) {
1900
+ throw new TypeError('DECIDE repository cohort changed its invocation identity');
1901
+ }
1902
+ const releaseOrder = [
1903
+ ...completionOrder,
1904
+ ...ROLE_IDS.filter((roleId) => !completionOrder.includes(roleId)),
1905
+ ];
1906
+ const acknowledgedResults = new Map();
1907
+ const acknowledgementFailures = [];
1908
+ for (const roleId of releaseOrder) {
1909
+ const member = members[roleId];
1910
+ try {
1911
+ const result = acknowledgeGovernedPlayerResult({
1912
+ operation: settled.operations[roleId],
1913
+ receipt: settled.receipts[roleId],
1914
+ effectLedger: settled.effectLedger,
1915
+ }, member.effectBoundary.boundaryId);
1916
+ if (member.envelope === undefined) {
1917
+ throw new TypeError(`DECIDE repository cohort omitted its ${roleId} invocation`);
1918
+ }
1919
+ acknowledgedResults.set(roleId, result);
1920
+ }
1921
+ catch (error) {
1922
+ acknowledgementFailures.push(error);
1923
+ }
1924
+ }
1925
+ if (acknowledgementFailures.length > 0) {
1926
+ for (const result of acknowledgedResults.values()) {
1927
+ governedPlayerOutputs.delete(result);
1928
+ }
1929
+ throw acknowledgementFailures.length === 1
1930
+ ? acknowledgementFailures[0]
1931
+ : new AggregateError(acknowledgementFailures, 'DECIDE proposal cohort reconciliation failed');
1932
+ }
1933
+ for (const roleId of releaseOrder) {
1934
+ const member = members[roleId];
1935
+ const result = acknowledgedResults.get(roleId);
1936
+ if (member.envelope === undefined || result === undefined) {
1937
+ throw new Error(`DECIDE proposal cohort lost its ${roleId} acknowledgement`);
1938
+ }
1939
+ member.result.resolve({ ...member.envelope, result });
1940
+ }
1941
+ }
1942
+ catch (error) {
1943
+ for (const member of Object.values(members))
1944
+ member?.result.reject(error);
1945
+ }
1946
+ finally {
1947
+ completedProposalCohortTurnId = turnId;
1948
+ pendingProposalCohort.clear();
1949
+ activeProposalCohort = undefined;
1950
+ }
1951
+ };
1952
+ const queueProposalCohortMember = (input, signal) => {
1953
+ if (pendingProposalCohort.has(input.role)) {
1954
+ throw new Error(`DECIDE proposal cohort already registered role ${input.role}`);
1955
+ }
1956
+ const callId = `player-${++playerCallSequence}`;
1957
+ const member = {
1958
+ input,
1959
+ signal,
1960
+ callId,
1961
+ effectBoundary: governedBoundarySeed(input, callId),
1962
+ result: deferredValue(),
1963
+ };
1964
+ pendingProposalCohort.set(input.role, member);
1965
+ if (pendingProposalCohort.size === ROLE_IDS.length) {
1966
+ if (activeProposalCohort !== undefined) {
1967
+ throw new Error('DECIDE proposal cohort was started more than once');
1968
+ }
1969
+ activeProposalCohort = settleProposalCohort();
1970
+ }
1971
+ return member.result.promise;
1972
+ };
1023
1973
  const callPlayer = (input, signal) => {
1024
- return trackBoundaryCall(runPlayerCall(input, signal));
1974
+ const invocation = async () => {
1975
+ const active = activeDeferredContinuation;
1976
+ if (active === undefined && hasUnresolvedReconciliation()) {
1977
+ throw new Error('DECIDE governed semantic reconciliation remains unresolved');
1978
+ }
1979
+ if (active !== undefined) {
1980
+ if (active.effectBoundary.runtimeSessionId !==
1981
+ requireSessionIdentity().sessionId ||
1982
+ active.effectBoundary.turnId !== currentTurnId ||
1983
+ active.effectBoundary.roleId !== input.role ||
1984
+ active.effectBoundary.sourceStateId !== input.stateId ||
1985
+ active.playerContinuation === undefined) {
1986
+ throw new TypeError('DECIDE deferred continuation did not invoke its bound player boundary');
1987
+ }
1988
+ active.input = input;
1989
+ active.playerId = resolvedPlayerId(input.role);
1990
+ try {
1991
+ const envelope = await runPlayerCall(input, signal, {
1992
+ callId: active.effectBoundary.callId,
1993
+ resume: active.playerContinuation,
1994
+ });
1995
+ active.result.resolve(envelope.result);
1996
+ const acknowledgedResult = await active.acknowledged.promise;
1997
+ return { ...envelope, result: acknowledgedResult };
1998
+ }
1999
+ catch (error) {
2000
+ active.result.reject(error);
2001
+ try {
2002
+ await active.acknowledged.promise;
2003
+ }
2004
+ catch (acknowledgementError) {
2005
+ throw acknowledgementError;
2006
+ }
2007
+ throw error;
2008
+ }
2009
+ }
2010
+ if (shouldRunProposalCohort(input)) {
2011
+ return queueProposalCohortMember(input, signal);
2012
+ }
2013
+ const callId = `player-${++playerCallSequence}`;
2014
+ const effectBoundary = governedBoundarySeed(input, callId);
2015
+ let envelope;
2016
+ const settled = await deferredEffects.repository.runExclusive({
2017
+ signal,
2018
+ effectBoundary,
2019
+ operation: async () => {
2020
+ envelope = await runPlayerCall(input, signal, { callId });
2021
+ return envelope.result;
2022
+ },
2023
+ completeEffectBoundary: completionEvidenceFor(input, input.role, resolvedPlayerId(input.role), signal),
2024
+ });
2025
+ const result = acknowledgeGovernedPlayerResult(settled, effectBoundary.boundaryId);
2026
+ if (envelope === undefined) {
2027
+ throw new TypeError('DECIDE repository settlement omitted its player invocation');
2028
+ }
2029
+ return { ...envelope, result };
2030
+ };
2031
+ return trackBoundaryCall(invocation());
1025
2032
  };
1026
2033
  const player = fromPromise(async ({ input, signal }) => {
1027
2034
  const combined = combineSignals(signal, currentSignal);
@@ -1039,8 +2046,10 @@ export const createPlaybookRuntime = (options) => {
1039
2046
  throw error;
1040
2047
  }
1041
2048
  combined.throwIfAborted();
1042
- let { roleId, playerId, result } = await callPlayer(input, combined);
1043
- if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
2049
+ let { roleId, playerId, callId, result } = await callPlayer(input, combined);
2050
+ if (result.status === 'ok' &&
2051
+ isEmptyFinalText(result.finalText) &&
2052
+ automaticReplayPolicy.allowsEmptyOkCorrection(requireSessionIdentity().sessionId, callId)) {
1044
2053
  // DR-028: an `ok` result whose finalText is missing, empty, or
1045
2054
  // whitespace-only earns exactly one corrective re-ask — the same
1046
2055
  // composed call repeated, traced by runPlayerCall as its own
@@ -1050,7 +2059,7 @@ export const createPlaybookRuntime = (options) => {
1050
2059
  // are never retried), and a rejecting finish emission rejects
1051
2060
  // `callPlayer` itself, so it never reaches this branch (PBRT-47).
1052
2061
  combined.throwIfAborted();
1053
- ({ roleId, playerId, result } = await callPlayer(input, combined));
2062
+ ({ roleId, playerId, callId, result } = await callPlayer(input, combined));
1054
2063
  }
1055
2064
  if (result.status !== 'ok') {
1056
2065
  throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
@@ -1060,14 +2069,12 @@ export const createPlaybookRuntime = (options) => {
1060
2069
  throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
1061
2070
  }
1062
2071
  combined.throwIfAborted();
1063
- try {
1064
- const prompt = buildAdjudicatorPrompt(input, finalText);
1065
- return parseAdjudication(await callJudge(prompt, combined, 'player-output-adjudication', input.stateId), input, finalText);
1066
- }
1067
- catch (error) {
1068
- latchControlPlaneError(error, combined);
1069
- throw error;
2072
+ const governedOutput = governedPlayerOutputs.get(result);
2073
+ if (governedOutput !== undefined) {
2074
+ governedPlayerOutputs.delete(result);
2075
+ return governedOutput;
1070
2076
  }
2077
+ throw new Error('DECIDE governed player result was not reconciled against repository evidence');
1071
2078
  }
1072
2079
  finally {
1073
2080
  actorSettlementAborts.push(settlementAborts);
@@ -1142,19 +2149,37 @@ export const createPlaybookRuntime = (options) => {
1142
2149
  return aborts;
1143
2150
  };
1144
2151
  const inspect = (event) => {
1145
- if (event.type !== '@xstate.snapshot')
1146
- return;
1147
2152
  if (actor === undefined || event.actorRef !== actor)
1148
2153
  return;
1149
2154
  if (suppressInspectionEmissions)
1150
2155
  return;
2156
+ if (event.type === '@xstate.action') {
2157
+ try {
2158
+ acceptedOutcomeConsumer.capture(event.action);
2159
+ }
2160
+ catch (error) {
2161
+ latchInspectionError(error);
2162
+ }
2163
+ return;
2164
+ }
2165
+ if (event.type !== '@xstate.snapshot')
2166
+ return;
1151
2167
  const settlementAborts = consumeActorSettlementAborts(true);
1152
2168
  try {
1153
2169
  const snapshot = event.snapshot;
1154
2170
  const state = normalizePlaybookSnapshot(snapshot);
1155
2171
  const prior = previousState ?? state;
2172
+ let acceptedOutcomes = [];
2173
+ try {
2174
+ acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
2175
+ }
2176
+ catch (error) {
2177
+ latchInspectionError(error);
2178
+ }
1156
2179
  const context = snapshot.context;
1157
- const fsmPayload = telemetryPayload(prior, state, event.event, context);
2180
+ const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
2181
+ ? undefined
2182
+ : 'commitCoderProposal');
1158
2183
  const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
1159
2184
  void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
1160
2185
  topic: TELEMETRY_TOPIC,
@@ -1162,7 +2187,7 @@ export const createPlaybookRuntime = (options) => {
1162
2187
  }), settlementAborts).catch(() => undefined);
1163
2188
  const priorIds = new Set(previousState?.activeStateIds ?? []);
1164
2189
  previousState = state;
1165
- const pendingQuestions = pendingQuestionsFromContext(context);
2190
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
1166
2191
  const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
1167
2192
  const scheduleStatus = (message, stateId, data) => {
1168
2193
  const tracePayload = {
@@ -1174,6 +2199,9 @@ export const createPlaybookRuntime = (options) => {
1174
2199
  assertJsonSafe(tracePayload);
1175
2200
  void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data), settlementAborts).catch(() => undefined);
1176
2201
  };
2202
+ for (const acceptedOutcome of acceptedOutcomes) {
2203
+ void enqueueAcceptedOutcomeEmission(acceptedOutcome, state, settlementAborts).catch(() => undefined);
2204
+ }
1177
2205
  for (const activeStateId of state.activeStateIds) {
1178
2206
  if (priorIds.has(activeStateId) ||
1179
2207
  !STATUS_STATE_IDS.has(activeStateId)) {
@@ -1200,11 +2228,13 @@ export const createPlaybookRuntime = (options) => {
1200
2228
  }
1201
2229
  }
1202
2230
  catch (error) {
2231
+ acceptedOutcomeConsumer.reset();
1203
2232
  latchInspectionError(error, settlementAborts);
1204
2233
  }
1205
2234
  };
1206
2235
  const createRuntimeActor = (machineSnapshot) => {
1207
2236
  previousState = undefined;
2237
+ acceptedOutcomeConsumer.reset();
1208
2238
  // DR-014 §1: a restore rehydrates the persisted machine snapshot;
1209
2239
  // XState derives context/value from it and ignores `input` then.
1210
2240
  actor = createActor(providedMachine, {
@@ -1237,6 +2267,7 @@ export const createPlaybookRuntime = (options) => {
1237
2267
  if (!actor)
1238
2268
  return;
1239
2269
  suppressInspectionEmissions = true;
2270
+ acceptedOutcomeConsumer.reset();
1240
2271
  actor.stop();
1241
2272
  };
1242
2273
  const startActor = () => {
@@ -1255,16 +2286,21 @@ export const createPlaybookRuntime = (options) => {
1255
2286
  const live = actor;
1256
2287
  if (!live)
1257
2288
  throw new Error('decide runtime: actor is not initialized');
2289
+ refreshReconciliationProjection();
2290
+ if (hasUnresolvedReconciliation())
2291
+ return null;
1258
2292
  const snapshot = live.getSnapshot();
1259
2293
  const context = snapshot.context;
1260
2294
  const state = normalizePlaybookSnapshot(snapshot, {
1261
2295
  pendingCall: nestedBridge.getPendingCall(),
1262
2296
  });
1263
- const pendingQuestions = pendingQuestionsForState(state, context);
2297
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
2298
+ const failed = state.activeStateIds.includes('failed');
1264
2299
  if (pendingQuestions.length === 0 &&
1265
2300
  (snapshot.status === 'done' ||
1266
2301
  state.activeStateIds.includes('ready') ||
1267
- state.activeStateIds.includes('failed'))) {
2302
+ (failed &&
2303
+ automaticReplayPolicy.allowsFailureStateRetry()))) {
1268
2304
  return { type: 'START_DECIDE', callerTopic: text };
1269
2305
  }
1270
2306
  if (pendingQuestions.length === 0)
@@ -1276,6 +2312,458 @@ export const createPlaybookRuntime = (options) => {
1276
2312
  const raw = await callJudge(prompt, signal, 'boss-input-classification', state.stateId);
1277
2313
  return parseClassification(raw, text, pendingQuestions.map(({ questionId }) => questionId));
1278
2314
  };
2315
+ const deferredBoundarySeed = (operationId, callId) => {
2316
+ const operation = effectLedgerMirror.logicalOperations.find((candidate) => candidate.operationId === operationId);
2317
+ const latestBoundaryId = operation?.boundaryIds.at(-1);
2318
+ const priorBoundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === latestBoundaryId);
2319
+ if (operation === undefined || priorBoundary === undefined) {
2320
+ throw new TypeError('DECIDE deferred logical operation has no linked physical boundary');
2321
+ }
2322
+ if (currentTurnId === undefined ||
2323
+ !Number.isSafeInteger(currentTurnId) ||
2324
+ currentTurnId <= 0) {
2325
+ throw new Error('DECIDE deferred continuation requires an active positive turn id');
2326
+ }
2327
+ return {
2328
+ boundaryId: randomUUID(),
2329
+ runtimeSessionId: requireSessionIdentity().sessionId,
2330
+ turnId: currentTurnId,
2331
+ callId,
2332
+ roleId: priorBoundary.roleId,
2333
+ sourceStateId: priorBoundary.sourceStateId,
2334
+ sourceOutcomeSchema: snapshotJsonValue(priorBoundary.sourceOutcomeSchema, 'DECIDE deferred source outcome schema'),
2335
+ dispositions: [...priorBoundary.dispositions],
2336
+ correctionBudget: { limit: 1, spent: false },
2337
+ };
2338
+ };
2339
+ const prepareDeferredContinuation = async (signal, resumeEvent) => {
2340
+ if (deferredEffects === undefined || deferredOperationId === undefined) {
2341
+ throw new TypeError('DECIDE deferred Boss reply has no host-bound logical operation');
2342
+ }
2343
+ const operationId = deferredOperationId;
2344
+ const callId = `player-${++playerCallSequence}`;
2345
+ const effectBoundary = deferredBoundarySeed(operationId, callId);
2346
+ const active = {
2347
+ operationId,
2348
+ effectBoundary,
2349
+ result: deferredValue(),
2350
+ acknowledged: deferredValue(),
2351
+ };
2352
+ const readiness = deferredValue();
2353
+ const repositoryCall = deferredEffects.repository.runDeferred({
2354
+ mode: 'continue',
2355
+ signal,
2356
+ operationId,
2357
+ effectBoundary,
2358
+ operation: async ({ playerContinuation }) => {
2359
+ if (playerContinuation !== false &&
2360
+ (typeof playerContinuation !== 'string' ||
2361
+ playerContinuation.trim() === '')) {
2362
+ throw new TypeError('DECIDE bound deferred player continuation is invalid');
2363
+ }
2364
+ active.playerContinuation = playerContinuation;
2365
+ readiness.resolve({ status: 'ready' });
2366
+ return active.result.promise;
2367
+ },
2368
+ completeEffectBoundary: async (completion) => {
2369
+ if (active.input === undefined) {
2370
+ throw new TypeError('DECIDE deferred continuation omitted its authored player input');
2371
+ }
2372
+ return completionEvidenceFor(active.input, active.input.role, active.playerId, signal, operationId)(completion);
2373
+ },
2374
+ });
2375
+ void repositoryCall.then((value) => readiness.resolve({ status: 'settled', value }), (reason) => readiness.resolve({ status: 'rejected', reason }));
2376
+ const prepared = await readiness.promise;
2377
+ if (prepared.status === 'rejected')
2378
+ throw prepared.reason;
2379
+ if (prepared.status === 'settled') {
2380
+ synchronizeDeferredProjection(assertPlaybookEffectLedger(prepared.value.effectLedger, 'DECIDE deferred checkpoint-mismatch effect ledger'));
2381
+ hiddenDeferredOperationId = operationId;
2382
+ return { proceed: false };
2383
+ }
2384
+ activeDeferredContinuation = active;
2385
+ const acknowledgement = repositoryCall.then((value) => {
2386
+ if (value.status !== 'continued') {
2387
+ throw new TypeError('DECIDE deferred repository changed status after starting its operation');
2388
+ }
2389
+ const result = acknowledgeGovernedPlayerResult(value, effectBoundary.boundaryId);
2390
+ if (hiddenDeferredOperationId === undefined) {
2391
+ suppressInspectionEmissions = false;
2392
+ const live = actor;
2393
+ if (live === undefined) {
2394
+ throw new Error('DECIDE deferred acknowledgement lost its runtime actor');
2395
+ }
2396
+ // Entry into the governed continuation was hidden until its durable
2397
+ // effect acknowledgement. Publish that exact current root snapshot
2398
+ // before releasing the actor output, so an accepted-outcome marker
2399
+ // is confirmed against commitCoderProposal rather than the earlier
2400
+ // awaitBossReply snapshot.
2401
+ inspect({
2402
+ type: '@xstate.snapshot',
2403
+ actorRef: live,
2404
+ event: resumeEvent,
2405
+ snapshot: live.getSnapshot(),
2406
+ });
2407
+ }
2408
+ active.acknowledged.resolve(result);
2409
+ }, (error) => {
2410
+ active.acknowledged.reject(error);
2411
+ throw error;
2412
+ }).catch((error) => {
2413
+ active.acknowledged.reject(error);
2414
+ throw error;
2415
+ });
2416
+ return { proceed: true, acknowledgement };
2417
+ };
2418
+ const parkDeferredContinuation = async (signal) => {
2419
+ if (deferredEffects === undefined || deferredOperationId === undefined) {
2420
+ return;
2421
+ }
2422
+ const operationId = deferredOperationId;
2423
+ const parked = await deferredEffects.repository.runDeferred({
2424
+ mode: 'park',
2425
+ signal,
2426
+ operationId,
2427
+ });
2428
+ synchronizeDeferredProjection(assertPlaybookEffectLedger(parked.effectLedger, 'DECIDE deferred park effect ledger'));
2429
+ hiddenDeferredOperationId = operationId;
2430
+ };
2431
+ const restoreDeferredReconciliation = async (operationId, signal, publishQuestion) => {
2432
+ const restored = await deferredEffects.repository.runDeferred({
2433
+ mode: 'restore',
2434
+ signal,
2435
+ operationId,
2436
+ });
2437
+ synchronizeDeferredProjection(assertPlaybookEffectLedger(restored.effectLedger, 'DECIDE deferred restoration effect ledger'));
2438
+ if (restored.status === 'parked') {
2439
+ throw new TypeError('DECIDE deferred restoration returned an invalid parked status');
2440
+ }
2441
+ if (restored.status !== 'restored') {
2442
+ if (hiddenDeferredOperationId !== operationId) {
2443
+ throw new TypeError('DECIDE unresolved deferred restoration lost its operation identity');
2444
+ }
2445
+ return restored.status;
2446
+ }
2447
+ if (hiddenDeferredOperationId !== undefined) {
2448
+ throw new TypeError('DECIDE restored deferred operation remained unresolved');
2449
+ }
2450
+ if (openDeferredOperation()?.operationId !== operationId) {
2451
+ throw new TypeError('DECIDE restored deferred operation is not the current bound wait');
2452
+ }
2453
+ const live = actor;
2454
+ if (live === undefined) {
2455
+ throw new Error('decide runtime: init(session) must be called first');
2456
+ }
2457
+ const state = currentState();
2458
+ const context = live.getSnapshot().context;
2459
+ const pending = questionForWaitState('awaitBossReply', visiblePendingQuestionsForState(state, context));
2460
+ const operation = openDeferredOperation();
2461
+ const projectedPending = pending === undefined
2462
+ ? undefined
2463
+ : {
2464
+ questionId: pending.questionId,
2465
+ asker: pending.asker,
2466
+ question: pending.question,
2467
+ sourceItem: pending.sourceItem,
2468
+ };
2469
+ if (pending === undefined ||
2470
+ operation?.pendingQuestion === undefined ||
2471
+ stableJson(projectedPending, 'DECIDE restored FSM pending question') !==
2472
+ stableJson(operation.pendingQuestion, 'DECIDE restored deferred pending question')) {
2473
+ throw new TypeError('DECIDE restored deferred wait does not equal its FSM question');
2474
+ }
2475
+ if (publishQuestion) {
2476
+ await emitBoundaryStatus(`${pending.asker.roleId} asks: ${pending.question}`, state);
2477
+ await emitBoundaryStatus(`◆ awaiting Boss reply · ${pending.resumeStateId} · ${pending.asker.roleId} · ${pending.sourceItem}`, state);
2478
+ await flush();
2479
+ }
2480
+ return 'restored';
2481
+ };
2482
+ const unresolvedControlCandidates = () => {
2483
+ if (!hasUnresolvedReconciliation())
2484
+ return [];
2485
+ const operation = hiddenDeferredOperationId === undefined
2486
+ ? undefined
2487
+ : effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === hiddenDeferredOperationId);
2488
+ return [
2489
+ {
2490
+ action: {
2491
+ id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
2492
+ label: 'Retry unresolved effect reconciliation',
2493
+ },
2494
+ kind: 'reconcile',
2495
+ ...(operation?.checkpointRestorationEligible === true
2496
+ ? { deferredRestoreOperationId: operation.operationId }
2497
+ : {}),
2498
+ },
2499
+ {
2500
+ action: {
2501
+ id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
2502
+ label: 'Abandon unresolved workflow attempt',
2503
+ },
2504
+ kind: 'abandon',
2505
+ },
2506
+ ];
2507
+ };
2508
+ const deriveControlCandidates = () => {
2509
+ const state = currentState();
2510
+ if (state.status !== 'active' ||
2511
+ !state.quiescent ||
2512
+ nestedBridge.getPendingCall() !== undefined) {
2513
+ return [];
2514
+ }
2515
+ return unresolvedControlCandidates();
2516
+ };
2517
+ const describeControlView = () => {
2518
+ if (disposed || disposalPromise !== undefined) {
2519
+ throw new Error('decide runtime: runtime is disposing or disposed');
2520
+ }
2521
+ const live = actor;
2522
+ if (live === undefined || sessionIdentity === undefined) {
2523
+ throw new Error('decide runtime: init(session) must be called before describe');
2524
+ }
2525
+ if (currentSignal !== undefined || currentTurnId !== undefined) {
2526
+ throw new Error('decide runtime: another runtime turn is active');
2527
+ }
2528
+ refreshReconciliationProjection();
2529
+ const state = currentState();
2530
+ const context = live.getSnapshot().context;
2531
+ const unresolved = hasUnresolvedReconciliation();
2532
+ const pendingQuestions = unresolved
2533
+ ? []
2534
+ : visiblePendingQuestionsForState(state, context).map((pending) => Object.freeze({
2535
+ questionId: pending.questionId,
2536
+ asker: pending.asker,
2537
+ question: pending.question,
2538
+ sourceItem: pending.sourceItem,
2539
+ }));
2540
+ const actions = deriveControlCandidates().map(({ action }) => ({
2541
+ ...action,
2542
+ }));
2543
+ const lastError = normalizeErrorFull(context.lastError);
2544
+ const stateDescription = unresolved || state.stateId === undefined
2545
+ ? undefined
2546
+ : STATE_DESCRIPTIONS[state.stateId];
2547
+ return deepFreeze(snapshotJsonValue({
2548
+ state,
2549
+ ...(stateDescription === undefined ? {} : { stateDescription }),
2550
+ pendingQuestions,
2551
+ ...(lastError === undefined ? {} : { lastError }),
2552
+ actions,
2553
+ }, 'DECIDE control view'));
2554
+ };
2555
+ const normalizedControlError = (error) => normalizeErrorFull(error) ?? {
2556
+ name: error instanceof Error ? error.name : 'Error',
2557
+ message: error instanceof Error ? error.message : String(error),
2558
+ };
2559
+ const frozenControlReceipt = (receipt) => deepFreeze(snapshotJsonValue(receipt, 'DECIDE apply receipt'));
2560
+ const applyControlAction = async (input) => {
2561
+ if (input === null || typeof input !== 'object') {
2562
+ throw new TypeError('decide runtime: apply input must be an object');
2563
+ }
2564
+ const { actionId, key, signal } = input;
2565
+ if (typeof actionId !== 'string' || actionId.length === 0) {
2566
+ throw new TypeError('decide runtime: apply actionId must be a non-empty string');
2567
+ }
2568
+ if (typeof key !== 'string' || key.length === 0) {
2569
+ throw new TypeError('decide runtime: apply key must be a non-empty string');
2570
+ }
2571
+ if (!(signal instanceof AbortSignal)) {
2572
+ throw new TypeError('decide runtime: apply signal must be an AbortSignal');
2573
+ }
2574
+ if (disposed || disposalPromise !== undefined) {
2575
+ throw new Error('decide runtime: runtime is disposing or disposed');
2576
+ }
2577
+ if (actor === undefined || sessionIdentity === undefined) {
2578
+ throw new Error('decide runtime: init(session) must be called before apply');
2579
+ }
2580
+ if (currentSignal !== undefined || currentTurnId !== undefined) {
2581
+ throw new Error('decide runtime: another runtime turn is active');
2582
+ }
2583
+ const recorded = appliedControlReceipts.get(key);
2584
+ if (recorded !== undefined)
2585
+ return recorded;
2586
+ signal.throwIfAborted();
2587
+ refreshReconciliationProjection();
2588
+ const turnId = ++turnSequence;
2589
+ const callId = `apply-${++applyCallSequence}`;
2590
+ currentTurnId = turnId;
2591
+ currentSignal = signal;
2592
+ currentAborts = abortReasonClassifier(signal);
2593
+ controlPlaneError = undefined;
2594
+ let accepted = false;
2595
+ let receipt;
2596
+ let operationError;
2597
+ let settlementError;
2598
+ let lateDeliveryError;
2599
+ const position = { turnId, callId };
2600
+ const preAcceptanceFinish = (reason) => ({
2601
+ actionId,
2602
+ key,
2603
+ disposition: 'rejected',
2604
+ reason,
2605
+ });
2606
+ try {
2607
+ try {
2608
+ try {
2609
+ await emitTrace('apply.started', {
2610
+ actionId,
2611
+ key,
2612
+ ...stateIdentity(currentState()),
2613
+ }, position, currentAborts);
2614
+ }
2615
+ catch (error) {
2616
+ latchControlPlaneError(error, signal);
2617
+ try {
2618
+ await emitTrace('apply.finished', {
2619
+ ...preAcceptanceFinish('apply.started trace sink rejected'),
2620
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
2621
+ error: normalizedControlError(error),
2622
+ }, position, currentAborts);
2623
+ }
2624
+ catch {
2625
+ // Preserve the start failure after one best-effort finish attempt.
2626
+ }
2627
+ throw error;
2628
+ }
2629
+ await flush();
2630
+ if (signal.aborted) {
2631
+ try {
2632
+ await emitTrace('apply.finished', {
2633
+ ...preAcceptanceFinish('aborted before acceptance'),
2634
+ status: 'aborted',
2635
+ error: normalizedControlError(signal.reason),
2636
+ }, position, currentAborts);
2637
+ }
2638
+ catch (error) {
2639
+ // A rejecting abort-finish sink outranks the abort at settlement.
2640
+ settlementError ??= error;
2641
+ }
2642
+ }
2643
+ signal.throwIfAborted();
2644
+ const candidate = deriveControlCandidates().find(({ action }) => action.id === actionId);
2645
+ if (candidate === undefined) {
2646
+ receipt = frozenControlReceipt({
2647
+ disposition: 'rejected',
2648
+ reason: `action ${JSON.stringify(actionId)} is not currently advertised`,
2649
+ });
2650
+ }
2651
+ else {
2652
+ // Acceptance is the line after which this boundary always records
2653
+ // and returns a receipt: the requested effect may now exist.
2654
+ accepted = true;
2655
+ try {
2656
+ let run;
2657
+ if (candidate.kind === 'abandon') {
2658
+ signal.throwIfAborted();
2659
+ run = {
2660
+ outcome: 'unresolved-effect',
2661
+ state: currentState(),
2662
+ };
2663
+ }
2664
+ else {
2665
+ if (candidate.deferredRestoreOperationId !== undefined) {
2666
+ await restoreDeferredReconciliation(candidate.deferredRestoreOperationId, signal, true);
2667
+ }
2668
+ else {
2669
+ // The host owns receipt reconstruction. Reconciliation only
2670
+ // re-reads its authoritative ledger; it never calls a player
2671
+ // or judge to manufacture missing semantic evidence.
2672
+ refreshReconciliationProjection();
2673
+ }
2674
+ signal.throwIfAborted();
2675
+ run = {
2676
+ outcome: hasUnresolvedReconciliation()
2677
+ ? 'no-action'
2678
+ : 'quiescent',
2679
+ state: currentState(),
2680
+ };
2681
+ }
2682
+ if (controlPlaneError !== undefined)
2683
+ throw controlPlaneError;
2684
+ receipt = frozenControlReceipt({
2685
+ disposition: 'executed',
2686
+ run,
2687
+ });
2688
+ }
2689
+ catch (error) {
2690
+ receipt = frozenControlReceipt({
2691
+ disposition: 'failed',
2692
+ error: normalizedControlError(error),
2693
+ });
2694
+ }
2695
+ appliedControlReceipts.set(key, receipt);
2696
+ }
2697
+ }
2698
+ catch (error) {
2699
+ operationError = error;
2700
+ }
2701
+ try {
2702
+ await flush();
2703
+ }
2704
+ catch (error) {
2705
+ settlementError = error;
2706
+ }
2707
+ settlementError ??= controlPlaneError;
2708
+ if (accepted && settlementError !== undefined) {
2709
+ receipt = frozenControlReceipt({
2710
+ disposition: 'failed',
2711
+ error: normalizedControlError(settlementError),
2712
+ });
2713
+ appliedControlReceipts.set(key, receipt);
2714
+ settlementError = undefined;
2715
+ }
2716
+ if (receipt !== undefined) {
2717
+ const finishFailures = [];
2718
+ try {
2719
+ await emitTrace('apply.finished', { actionId, key, ...receipt }, position, currentAborts);
2720
+ }
2721
+ catch (error) {
2722
+ if (!accepted || !isAbortFailure(error, signal)) {
2723
+ collectFailure(finishFailures, error);
2724
+ }
2725
+ }
2726
+ try {
2727
+ await flush();
2728
+ }
2729
+ catch (error) {
2730
+ if (!accepted || !isAbortFailure(error, signal)) {
2731
+ collectFailure(finishFailures, error);
2732
+ }
2733
+ }
2734
+ if (finishFailures.length > 0) {
2735
+ const failure = finishFailures.length === 1
2736
+ ? finishFailures[0]
2737
+ : new AggregateError(finishFailures, 'DECIDE apply settlement emissions failed');
2738
+ if (accepted)
2739
+ lateDeliveryError = failure;
2740
+ else
2741
+ settlementError = failure;
2742
+ }
2743
+ }
2744
+ }
2745
+ finally {
2746
+ currentSignal = undefined;
2747
+ currentAborts = undefined;
2748
+ currentTurnId = undefined;
2749
+ controlPlaneError = undefined;
2750
+ if (accepted && receipt !== undefined) {
2751
+ appliedControlReceipts.set(key, receipt);
2752
+ }
2753
+ }
2754
+ if (lateDeliveryError !== undefined) {
2755
+ collectFailure(emissionFailures, lateDeliveryError);
2756
+ }
2757
+ if (accepted && receipt !== undefined)
2758
+ return receipt;
2759
+ const failure = settlementError ?? operationError;
2760
+ if (failure !== undefined)
2761
+ throw failure;
2762
+ if (receipt === undefined) {
2763
+ throw new Error('decide runtime: apply produced no receipt');
2764
+ }
2765
+ return receipt;
2766
+ };
1279
2767
  const resultForSnapshot = (signal) => {
1280
2768
  const live = actor;
1281
2769
  if (!live)
@@ -1399,6 +2887,7 @@ export const createPlaybookRuntime = (options) => {
1399
2887
  activeEmissionCalls.clear();
1400
2888
  emissionQueue.clear();
1401
2889
  judgeQueue.clear();
2890
+ semanticCompletionQueue.clear();
1402
2891
  actor = undefined;
1403
2892
  currentSignal = undefined;
1404
2893
  currentAborts = undefined;
@@ -1416,7 +2905,20 @@ export const createPlaybookRuntime = (options) => {
1416
2905
  judgeCallSequence = 0;
1417
2906
  playerCallSequence = 0;
1418
2907
  playbookCallSequence = 0;
2908
+ applyCallSequence = 0;
1419
2909
  playbookCallTurnIds.clear();
2910
+ governedOutputsByBoundaryId.clear();
2911
+ governedFailuresByBoundaryId.clear();
2912
+ governedEvidenceByBoundaryId.clear();
2913
+ governedReceiptsByBoundaryId.clear();
2914
+ pendingProposalCohort.clear();
2915
+ activeProposalCohort = undefined;
2916
+ completedProposalCohortTurnId = undefined;
2917
+ deferredOperationId = undefined;
2918
+ hiddenDeferredOperationId = undefined;
2919
+ unresolvedSemanticBoundaryIds.clear();
2920
+ appliedControlReceipts.clear();
2921
+ activeDeferredContinuation = undefined;
1420
2922
  lifecycleStarted = false;
1421
2923
  };
1422
2924
  return {
@@ -1437,6 +2939,10 @@ export const createPlaybookRuntime = (options) => {
1437
2939
  ports = identity.ports;
1438
2940
  sessionIdentity = identity;
1439
2941
  try {
2942
+ if (deferredEffects !== undefined) {
2943
+ effectLedgerMirror = readEffectLedger();
2944
+ synchronizeDeferredProjection(effectLedgerMirror);
2945
+ }
1440
2946
  suppressInspectionEmissions = false;
1441
2947
  createRuntimeActor();
1442
2948
  const state = currentState();
@@ -1457,6 +2963,9 @@ export const createPlaybookRuntime = (options) => {
1457
2963
  initInFlight = undefined;
1458
2964
  }
1459
2965
  },
2966
+ describe: describeControlView,
2967
+ unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
2968
+ apply: applyControlAction,
1460
2969
  // DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
1461
2970
  // session, including one already-started suspended REVIEW call.
1462
2971
  // Defined only at a safe capture point — initialized, not disposing
@@ -1504,8 +3013,12 @@ export const createPlaybookRuntime = (options) => {
1504
3013
  return undefined;
1505
3014
  const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
1506
3015
  const context = actor.getSnapshot().context;
3016
+ if (deferredEffects !== undefined) {
3017
+ synchronizeDeferredProjection(readEffectLedger());
3018
+ }
3019
+ const unresolved = hasUnresolvedReconciliation();
1507
3020
  return {
1508
- schemaVersion: 3,
3021
+ schemaVersion: 4,
1509
3022
  playbookId: sessionIdentity.playbookId,
1510
3023
  machine,
1511
3024
  roleResumeTokens: snapshotRoleResumeTokens(),
@@ -1517,12 +3030,15 @@ export const createPlaybookRuntime = (options) => {
1517
3030
  playbookCall: playbookCallSequence,
1518
3031
  },
1519
3032
  state,
1520
- pendingBossQuestions: pendingQuestionsForState(state, context).map((pending) => ({
1521
- questionId: pending.questionId,
1522
- asker: pending.asker,
1523
- question: pending.question,
1524
- sourceItem: pending.sourceItem,
1525
- })),
3033
+ pendingBossQuestions: unresolved
3034
+ ? []
3035
+ : visiblePendingQuestionsForState(state, context).map((pending) => ({
3036
+ questionId: pending.questionId,
3037
+ asker: pending.asker,
3038
+ question: pending.question,
3039
+ sourceItem: pending.sourceItem,
3040
+ })),
3041
+ effectLedger: effectLedgerMirror,
1526
3042
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
1527
3043
  };
1528
3044
  },
@@ -1540,6 +3056,21 @@ export const createPlaybookRuntime = (options) => {
1540
3056
  }
1541
3057
  const identity = bindSession(session);
1542
3058
  const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, identity.playbookId, { allowSuspendedCall: true });
3059
+ if (deferredEffects === undefined) {
3060
+ if (boundSnapshot.effectLedger.revision !== 0 ||
3061
+ boundSnapshot.effectLedger.boundaries.length !== 0 ||
3062
+ boundSnapshot.effectLedger.logicalOperations.length !== 0) {
3063
+ throw new TypeError('decide runtime snapshot effectLedger must be the canonical empty ledger');
3064
+ }
3065
+ }
3066
+ else {
3067
+ const current = readEffectLedger();
3068
+ if (stableJson(current, 'DECIDE current effect ledger') !==
3069
+ stableJson(boundSnapshot.effectLedger, 'DECIDE snapshot effect ledger')) {
3070
+ throw new TypeError('decide runtime snapshot effectLedger must equal the current host mirror');
3071
+ }
3072
+ effectLedgerMirror = current;
3073
+ }
1543
3074
  const suspendedCall = boundSnapshot.suspendedCall;
1544
3075
  let finishInitialization;
1545
3076
  const initialization = new Promise((resolve) => {
@@ -1556,6 +3087,7 @@ export const createPlaybookRuntime = (options) => {
1556
3087
  judgeCallSequence = boundSnapshot.sequences.judgeCall;
1557
3088
  playerCallSequence = boundSnapshot.sequences.playerCall;
1558
3089
  playbookCallSequence = boundSnapshot.sequences.playbookCall;
3090
+ applyCallSequence = boundSnapshot.sequences.trace;
1559
3091
  if (identity.playerSessions) {
1560
3092
  priorExternalRoleTokens = snapshotRoleResumeTokens();
1561
3093
  }
@@ -1564,6 +3096,7 @@ export const createPlaybookRuntime = (options) => {
1564
3096
  if (suspendedCall !== undefined) {
1565
3097
  playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
1566
3098
  }
3099
+ synchronizeDeferredProjection(effectLedgerMirror);
1567
3100
  suppressInspectionEmissions = true;
1568
3101
  createRuntimeActor(boundSnapshot.machine);
1569
3102
  actor?.start();
@@ -1635,24 +3168,90 @@ export const createPlaybookRuntime = (options) => {
1635
3168
  const event = await classify(turn.text, turn.signal);
1636
3169
  if (!event) {
1637
3170
  const state = currentState();
1638
- await emitBoundaryStatus('No playbook action classified.', state);
3171
+ if (!hasUnresolvedReconciliation()) {
3172
+ await emitBoundaryStatus('No playbook action classified.', state);
3173
+ }
1639
3174
  result = { outcome: 'no-action', state };
1640
3175
  }
1641
3176
  else if (event.type === 'NO_ACTION') {
1642
3177
  result = { outcome: 'no-action', state: currentState() };
1643
3178
  }
1644
3179
  else {
1645
- await emitBoundaryStatus(event.type, currentState());
1646
- if (actor.getSnapshot().status === 'done') {
1647
- stopActor();
1648
- startActor();
3180
+ const before = currentState();
3181
+ const boundCommitWait = deferredEffects !== undefined &&
3182
+ deferredOperationId !== undefined &&
3183
+ before.activeStateIds.includes('awaitBossReply');
3184
+ if (boundCommitWait && event.type === 'BOSS_INTERRUPT') {
3185
+ await parkDeferredContinuation(turn.signal);
3186
+ result = { outcome: 'no-action', state: currentState() };
3187
+ }
3188
+ else {
3189
+ const prepared = boundCommitWait &&
3190
+ event.type === 'BOSS_REPLY' &&
3191
+ event.questionId === 'commitCoderProposal'
3192
+ ? await prepareDeferredContinuation(turn.signal, event)
3193
+ : undefined;
3194
+ if (prepared?.proceed === false) {
3195
+ result = { outcome: 'no-action', state: currentState() };
3196
+ }
3197
+ else {
3198
+ await emitBoundaryStatus(event.type, before);
3199
+ if (actor.getSnapshot().status === 'done') {
3200
+ stopActor();
3201
+ startActor();
3202
+ }
3203
+ const deferredMachineCheckpoint = prepared?.proceed === true
3204
+ ? detachPersistedMachineSnapshot(actor.getPersistedSnapshot())
3205
+ : undefined;
3206
+ if (deferredMachineCheckpoint !== undefined) {
3207
+ suppressInspectionEmissions = true;
3208
+ }
3209
+ try {
3210
+ actor.send(event);
3211
+ await driveToQuiescence();
3212
+ await drainBoundaryCallsAndEmissions();
3213
+ await prepared?.acknowledgement;
3214
+ if (deferredMachineCheckpoint !== undefined &&
3215
+ hiddenDeferredOperationId !== undefined) {
3216
+ stopActor();
3217
+ createRuntimeActor(deferredMachineCheckpoint);
3218
+ actor.start();
3219
+ previousState = before;
3220
+ suppressInspectionEmissions = false;
3221
+ }
3222
+ }
3223
+ catch (error) {
3224
+ activeDeferredContinuation?.result.reject(error);
3225
+ if (deferredEffects !== undefined) {
3226
+ try {
3227
+ synchronizeDeferredProjection(readEffectLedger());
3228
+ hiddenDeferredOperationId ??= deferredOperationId;
3229
+ }
3230
+ catch {
3231
+ hiddenDeferredOperationId ??= deferredOperationId;
3232
+ }
3233
+ }
3234
+ if (deferredMachineCheckpoint !== undefined) {
3235
+ try {
3236
+ stopActor();
3237
+ createRuntimeActor(deferredMachineCheckpoint);
3238
+ actor.start();
3239
+ previousState = before;
3240
+ }
3241
+ finally {
3242
+ suppressInspectionEmissions = false;
3243
+ }
3244
+ }
3245
+ throw error;
3246
+ }
3247
+ finally {
3248
+ activeDeferredContinuation = undefined;
3249
+ }
3250
+ if (controlPlaneError !== undefined)
3251
+ throw controlPlaneError;
3252
+ result = resultForSnapshot(turn.signal);
3253
+ }
1649
3254
  }
1650
- actor.send(event);
1651
- await driveToQuiescence();
1652
- await drainBoundaryCallsAndEmissions();
1653
- if (controlPlaneError !== undefined)
1654
- throw controlPlaneError;
1655
- result = resultForSnapshot(turn.signal);
1656
3255
  }
1657
3256
  }
1658
3257
  settlement = {
@@ -1759,6 +3358,10 @@ export const createPlaybookRuntime = (options) => {
1759
3358
  if (currentSignal !== undefined) {
1760
3359
  throw new Error('decide runtime: another runtime turn is active');
1761
3360
  }
3361
+ refreshReconciliationProjection();
3362
+ if (hasUnresolvedReconciliation()) {
3363
+ return { outcome: 'no-action', state: currentState() };
3364
+ }
1762
3365
  currentTurnId = playbookCallTurnIds.get(callId);
1763
3366
  currentSignal = signal;
1764
3367
  currentAborts = abortReasonClassifier(signal);
@@ -1890,6 +3493,7 @@ export const createPlaybookRuntime = (options) => {
1890
3493
  activeEmissionCalls.clear();
1891
3494
  emissionQueue.clear();
1892
3495
  judgeQueue.clear();
3496
+ semanticCompletionQueue.clear();
1893
3497
  actor = undefined;
1894
3498
  currentSignal = undefined;
1895
3499
  currentAborts = undefined;
@@ -1900,6 +3504,19 @@ export const createPlaybookRuntime = (options) => {
1900
3504
  sessionIdentity = undefined;
1901
3505
  previousState = undefined;
1902
3506
  controlPlaneError = undefined;
3507
+ governedOutputsByBoundaryId.clear();
3508
+ governedFailuresByBoundaryId.clear();
3509
+ governedEvidenceByBoundaryId.clear();
3510
+ governedReceiptsByBoundaryId.clear();
3511
+ pendingProposalCohort.clear();
3512
+ activeProposalCohort = undefined;
3513
+ completedProposalCohortTurnId = undefined;
3514
+ deferredOperationId = undefined;
3515
+ hiddenDeferredOperationId = undefined;
3516
+ unresolvedSemanticBoundaryIds.clear();
3517
+ appliedControlReceipts.clear();
3518
+ applyCallSequence = 0;
3519
+ activeDeferredContinuation = undefined;
1903
3520
  disposed = true;
1904
3521
  }
1905
3522
  if (failures.length === 1)
@@ -1916,6 +3533,10 @@ export const createPlaybookRuntime = (options) => {
1916
3533
  return nestedBridge;
1917
3534
  },
1918
3535
  };
3536
+ }
3537
+ export const createPlaybookRuntime = (factoryInput) => {
3538
+ const construction = schema3Construction(factoryInput);
3539
+ return createDecidePlaybookRuntime(construction.configuredOptions, construction.hostCapabilities);
1919
3540
  };
1920
3541
  export const _internal = {
1921
3542
  composePlayerPrompt,
@@ -1924,7 +3545,6 @@ export const _internal = {
1924
3545
  buildClassifierPrompt,
1925
3546
  parseClassification,
1926
3547
  buildAdjudicatorPrompt,
1927
- parseAdjudication,
1928
3548
  combineSignals,
1929
3549
  pendingQuestionsFromContext,
1930
3550
  pendingQuestionsForState,
@@ -1935,6 +3555,7 @@ export const _internal = {
1935
3555
  ROLE_STATE_IDS,
1936
3556
  VERBATIM_PAYLOAD_FIELDS,
1937
3557
  BOSS_INTERRUPT_TARGETS,
3558
+ UNFINISHED_FINAL_STATE_IDS,
1938
3559
  CONTINUATION_PREAMBLE,
1939
3560
  TELEMETRY_TOPIC,
1940
3561
  };