@sublang/playbook 8.0.0 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +3 -3
  2. package/docs/cli.md +66 -23
  3. package/docs/configuration.md +13 -8
  4. package/docs/embedding.md +45 -14
  5. package/package.json +7 -3
  6. package/reference/sdlc/captain.md +14 -10
  7. package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
  8. package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
  9. package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
  10. package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
  11. package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
  12. package/reference/sdlc/captain.playbook/captain.playbook.js +151 -10
  13. package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
  14. package/reference/sdlc/code.md +0 -1
  15. package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
  16. package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
  17. package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
  18. package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
  19. package/reference/sdlc/code.playbook/bin/run.js +365 -63
  20. package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
  21. package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
  22. package/reference/sdlc/code.playbook/code.fsm.js +85 -29
  23. package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
  24. package/reference/sdlc/code.playbook/code.gears.md +0 -2
  25. package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
  26. package/reference/sdlc/code.playbook/code.playbook.js +67 -4
  27. package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
  28. package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
  29. package/reference/sdlc/code.playbook/code.registry.js +10 -3
  30. package/reference/sdlc/code.playbook/code.registry.ts +23 -5
  31. package/reference/sdlc/code.playbook/playbook-captain.d.ts +99 -7
  32. package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
  33. package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
  34. package/reference/sdlc/decide.md +0 -1
  35. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
  36. package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
  37. package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
  38. package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
  39. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +15 -5
  40. package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
  41. package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
  42. package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
  43. package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
  44. package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
  45. package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
  46. package/reference/sdlc/review.playbook/review.fsm.js +133 -12
  47. package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
  48. package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
  49. package/reference/sdlc/review.playbook/review.playbook.js +78 -4
  50. package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
  51. package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
  52. package/reference/sdlc/review.playbook/review.registry.js +10 -3
  53. package/reference/sdlc/review.playbook/review.registry.ts +23 -5
  54. package/slc/gears2fsm.md +25 -7
  55. package/slc/link.md +727 -82
  56. package/src/accepted-outcome.d.ts +18 -0
  57. package/src/accepted-outcome.js +94 -0
  58. package/src/accepted-outcome.ts +140 -0
  59. package/src/runtime.d.ts +165 -3
  60. package/src/runtime.ts +214 -2
  61. package/src/xstate-playbook-runtime.d.ts +162 -13
  62. package/src/xstate-playbook-runtime.js +3344 -564
  63. package/src/xstate-playbook-runtime.ts +4873 -637
  64. package/src/xstate-runtime.d.ts +76 -8
  65. package/src/xstate-runtime.js +1001 -64
  66. package/src/xstate-runtime.ts +1640 -91
@@ -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');
@@ -30,19 +32,28 @@ function snapshotDecideRuntimeOptions(value) {
30
32
  }
31
33
  return Object.freeze({});
32
34
  }
33
- const STATE_DESCRIPTIONS = {
34
- ready: 'Waiting for a topic to decide.',
35
- askCoderProposal: 'Coder independently proposes a spec design.',
36
- askReviewerProposal: 'Reviewer independently proposes a spec design.',
37
- waitCoderProposalReply: 'Coder waits for Boss to answer a question.',
38
- waitReviewerProposalReply: 'Reviewer waits for Boss to answer a question.',
39
- commitCoderProposal: 'Coder writes and commits Coder’s independent proposal.',
40
- awaitBossReply: 'Waiting for Boss to answer Coder’s question.',
41
- reviewCommit: 'REVIEW examines the committed proposal.',
42
- failed: 'DECIDE failed and is waiting for a new topic.',
43
- reportedReviewFailure: 'DECIDE reports REVIEW’s failure and its last commit.',
44
- done: 'DECIDE completed with an approved commit.',
45
- };
35
+ function authoredStateDescriptions(states) {
36
+ const descriptions = {};
37
+ const visit = (children) => {
38
+ for (const state of Object.values(children ?? {})) {
39
+ const stateId = state.meta?.playbook?.stateId;
40
+ const description = state.meta?.playbook?.description;
41
+ if (typeof stateId === 'string' &&
42
+ typeof description === 'string' &&
43
+ description.trim().length > 0) {
44
+ const existing = descriptions[stateId];
45
+ if (existing !== undefined && existing !== description) {
46
+ throw new Error(`DECIDE state ${stateId} declares conflicting descriptions`);
47
+ }
48
+ descriptions[stateId] = description;
49
+ }
50
+ visit(state.states);
51
+ }
52
+ };
53
+ visit(states);
54
+ return Object.freeze(descriptions);
55
+ }
56
+ const STATE_DESCRIPTIONS = authoredStateDescriptions(decideMachine.config.states);
46
57
  const ROLE_STATES = [
47
58
  { stateId: 'askCoderProposal', role: 'coder', sourceItem: 'DECIDE-1' },
48
59
  {
@@ -53,13 +64,64 @@ const ROLE_STATES = [
53
64
  { stateId: 'commitCoderProposal', role: 'coder', sourceItem: 'DECIDE-3' },
54
65
  ];
55
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
+ });
56
113
  const ROLE_IDS = ['coder', 'reviewer'];
57
114
  const ROLE_ID_SET = new Set(ROLE_IDS);
58
115
  const roleLabel = (roleId) => roleId === 'coder' ? 'Coder' : 'Reviewer';
59
116
  const BOSS_INTERRUPT_TARGETS = ['independentProposals'];
60
117
  const BOSS_INTERRUPT_TARGET_IDS = new Set(BOSS_INTERRUPT_TARGETS);
118
+ const UNFINISHED_FINAL_STATE_IDS = new Set([
119
+ 'reportedReviewFailure',
120
+ ]);
61
121
  const TELEMETRY_TOPIC = 'playbook.fsm.state';
62
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';
63
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.';
64
126
  const PLACEHOLDER_FIELDS = [['<caller-topic>', 'callerTopic']];
65
127
  const VERBATIM_PAYLOAD_FIELDS = new Set([
@@ -114,6 +176,27 @@ function requiredFieldsFor(description) {
114
176
  }
115
177
  return fields;
116
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
+ }
117
200
  // LLM judges routinely wrap JSON in prose/fences or damage its tail. Match
118
201
  // CODE's recovery contract: scan candidate starts in document order, prefer a
119
202
  // strict balanced value at each position, then repair trailing commas and
@@ -185,6 +268,14 @@ function sortJson(value) {
185
268
  function stableJson(value, path) {
186
269
  return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
187
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
+ }
188
279
  function stripCodeFence(text) {
189
280
  const fence = text.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
190
281
  return fence ? fence[1].trim() : text;
@@ -314,7 +405,8 @@ function parseClassification(raw, text, pendingQuestionIds = []) {
314
405
  }
315
406
  return null;
316
407
  }
317
- function buildAdjudicatorPrompt(input, playerOutput) {
408
+ function buildAdjudicatorPrompt(input, playerOutput, correction) {
409
+ const outcomes = governedOutcomesFor(input);
318
410
  const lines = [];
319
411
  lines.push('You are the guard adjudicator for a playbook state machine.');
320
412
  lines.push('This is hidden control work. Do not call tools, inspect files, or ' +
@@ -330,54 +422,32 @@ function buildAdjudicatorPrompt(input, playerOutput) {
330
422
  lines.push('');
331
423
  lines.push('Guards (choose exactly one; the descriptions are authoritative and must be applied as written):');
332
424
  for (const [guard, description] of Object.entries(input.result)) {
333
- 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}`);
334
429
  }
335
- const runtimeOwnedFields = new Set();
336
- for (const description of Object.values(input.result)) {
337
- for (const field of requiredFieldsFor(description)) {
338
- if (VERBATIM_PAYLOAD_FIELDS.has(field))
339
- runtimeOwnedFields.add(field);
340
- }
341
- }
342
- 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) {
343
434
  lines.push('');
344
- 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.');
345
441
  }
346
- lines.push('');
347
- lines.push('Reply with a single JSON object: { "guard": "<one of the guard names above>", ...any payload fields the chosen guard description requires }.');
348
442
  return lines.join('\n');
349
443
  }
350
- function parseAdjudication(raw, input, finalText) {
351
- const obj = extractJson(raw);
352
- if (!obj || typeof obj.guard !== 'string' || obj.guard.trim() === '') {
353
- throw new Error('adjudicator returned empty or malformed JSON');
354
- }
355
- const guard = obj.guard;
356
- if (!Object.prototype.hasOwnProperty.call(input.result, guard)) {
357
- throw new Error(`adjudicator returned undeclared guard "${guard}" for ${input.sourceItem}`);
358
- }
359
- const requiredFields = requiredFieldsFor(input.result[guard]);
360
- const allowedFields = new Set(['guard', ...requiredFields]);
361
- for (const key of Reflect.ownKeys(obj)) {
362
- if (typeof key !== 'string' || !allowedFields.has(key)) {
363
- throw new Error(`adjudicator response for guard "${guard}" included undeclared field "${String(key)}"`);
364
- }
444
+ function parseGovernedSemanticCandidate(raw) {
445
+ try {
446
+ return parseJudgeJson(raw);
365
447
  }
366
- const output = { guard };
367
- for (const field of requiredFields) {
368
- if (VERBATIM_PAYLOAD_FIELDS.has(field)) {
369
- output[field] = finalText;
370
- continue;
371
- }
372
- const value = obj[field];
373
- if (value === undefined ||
374
- value === null ||
375
- (typeof value === 'string' && value.trim() === '')) {
376
- throw new Error(`adjudicator response for guard "${guard}" missing required field "${field}"`);
377
- }
378
- output[field] = value;
448
+ catch (error) {
449
+ throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
379
450
  }
380
- return output;
381
451
  }
382
452
  function combineSignals(a, b) {
383
453
  return combineAbortSignals(a, b);
@@ -398,9 +468,136 @@ function normalizeErrorFull(err) {
398
468
  function isEmptyFinalText(finalText) {
399
469
  return finalText === undefined || finalText.trim().length === 0;
400
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
+ }
401
590
  function isAbortFailure(error, signal) {
402
591
  return signal.aborted && Object.is(error, signal.reason);
403
592
  }
593
+ function abortReasonClassifier(...sources) {
594
+ const captured = sources.filter((source) => source !== undefined);
595
+ return Object.freeze({
596
+ isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
597
+ ? isAbortFailure(error, source)
598
+ : source.isAbortReason(error)),
599
+ });
600
+ }
404
601
  function pendingQuestionsFromContext(context) {
405
602
  const pending = context.pendingBossQuestions;
406
603
  if (pending === undefined ||
@@ -440,6 +637,22 @@ const STATUS_STATE_IDS = new Set([
440
637
  ...WAIT_STATE_IDS,
441
638
  'failed',
442
639
  ]);
640
+ // PBRT-45: a question is pending only while its authored reply-wait state
641
+ // is active. The context retains an answered question through the resumed
642
+ // player call so the Q+A continuation prompt can quote it, and each branch
643
+ // keeps its own entry through the parallel region — so an unfiltered
644
+ // projection would report the answered question as still awaiting during
645
+ // the resume, and both branch questions after only one remains pending.
646
+ const RESUME_WAIT_STATE_IDS = {
647
+ ...Object.fromEntries(Object.entries(WAIT_STATE_RESUME_IDS).map(([waitStateId, resumeStateId]) => [
648
+ resumeStateId,
649
+ waitStateId,
650
+ ])),
651
+ commitCoderProposal: 'awaitBossReply',
652
+ };
653
+ function pendingQuestionsForState(state, context) {
654
+ return pendingQuestionsFromContext(context).filter((pending) => state.activeStateIds.includes(RESUME_WAIT_STATE_IDS[pending.resumeStateId] ?? ''));
655
+ }
443
656
  function questionForWaitState(stateId, pendingQuestions) {
444
657
  const resumeStateId = WAIT_STATE_RESUME_IDS[stateId];
445
658
  if (resumeStateId !== undefined) {
@@ -477,8 +690,8 @@ function normalizedTransitionEvent(event) {
477
690
  }
478
691
  return snapshotJsonValue(descriptor, 'FSM event');
479
692
  }
480
- function telemetryPayload(previousState, state, event, context) {
481
- const pendingBossQuestions = pendingQuestionsFromContext(context);
693
+ function telemetryPayload(previousState, state, event, context, hiddenQuestionId) {
694
+ const pendingBossQuestions = pendingQuestionsForState(state, context).filter(({ questionId }) => questionId !== hiddenQuestionId);
482
695
  const prior = previousState ?? state;
483
696
  const payload = {
484
697
  from: prior.value,
@@ -494,12 +707,20 @@ function telemetryPayload(previousState, state, event, context) {
494
707
  assertJsonSafe(payload);
495
708
  return payload;
496
709
  }
497
- export const createPlaybookRuntime = (options) => {
710
+ function createDecidePlaybookRuntime(options, deferredEffects) {
711
+ const automaticReplayPolicy = createAutomaticReplayPolicy(deferredEffects);
498
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);
499
717
  let ports;
500
718
  let sessionIdentity;
501
719
  let actor;
502
720
  let currentSignal;
721
+ let currentAborts;
722
+ const actorSettlementAborts = [];
723
+ let actorSettlementErrorAborts;
503
724
  let currentTurnId;
504
725
  let previousState;
505
726
  let suppressInspectionEmissions = false;
@@ -509,6 +730,7 @@ export const createPlaybookRuntime = (options) => {
509
730
  let judgeCallSequence = 0;
510
731
  let playerCallSequence = 0;
511
732
  let playbookCallSequence = 0;
733
+ let applyCallSequence = 0;
512
734
  let lifecycleStarted = false;
513
735
  let initInFlight;
514
736
  let disposed = false;
@@ -522,6 +744,23 @@ export const createPlaybookRuntime = (options) => {
522
744
  const activeEmissionCalls = new Set();
523
745
  const emissionQueue = new PQueue({ concurrency: 1 });
524
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;
525
764
  const collectFailure = (failures, error) => {
526
765
  if (error instanceof AggregateError) {
527
766
  for (const nested of error.errors)
@@ -536,24 +775,27 @@ export const createPlaybookRuntime = (options) => {
536
775
  if (!isAbortFailure(error, signal))
537
776
  controlPlaneError ??= error;
538
777
  };
539
- const latchInspectionError = (error) => {
540
- if (currentSignal !== undefined) {
541
- latchControlPlaneError(error, currentSignal);
542
- }
543
- else {
778
+ const latchInspectionError = (error, aborts = currentAborts) => {
779
+ if (aborts?.isAbortReason(error))
780
+ return;
781
+ if (currentSignal !== undefined)
782
+ controlPlaneError ??= error;
783
+ else
544
784
  collectFailure(emissionFailures, error);
545
- }
546
785
  };
547
- const enqueue = (fn) => {
786
+ const enqueue = (fn, aborts = currentAborts) => {
787
+ const enqueueAborts = aborts;
548
788
  const queued = emissionQueue.add(fn);
549
789
  activeEmissionCalls.add(queued);
550
790
  void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
551
791
  activeEmissionCalls.delete(queued);
552
- collectFailure(emissionFailures, error);
792
+ if (!enqueueAborts?.isAbortReason(error)) {
793
+ collectFailure(emissionFailures, error);
794
+ }
553
795
  });
554
796
  return queued;
555
797
  };
556
- const flush = async () => {
798
+ const flush = async (_aborts = currentAborts) => {
557
799
  while (true) {
558
800
  const active = [...activeEmissionCalls];
559
801
  if (active.length > 0)
@@ -569,9 +811,15 @@ export const createPlaybookRuntime = (options) => {
569
811
  return;
570
812
  const failures = emissionFailures;
571
813
  emissionFailures = [];
572
- if (failures.length === 1)
573
- throw failures[0];
574
- throw new AggregateError(failures, 'decide runtime emissions failed');
814
+ const failure = failures.length === 1
815
+ ? failures[0]
816
+ : new AggregateError(failures, 'decide runtime emissions failed');
817
+ // Enqueue ownership already classified every stored failure as distinct.
818
+ // Preserve that classification if an unrelated public boundary drains
819
+ // it with a signal whose reason happens to be the same object.
820
+ if (currentSignal !== undefined)
821
+ controlPlaneError ??= failure;
822
+ throw failure;
575
823
  };
576
824
  const drainBoundaryCallsAndEmissions = async () => {
577
825
  while (true) {
@@ -602,6 +850,9 @@ export const createPlaybookRuntime = (options) => {
602
850
  };
603
851
  const bindSession = (nextSession) => {
604
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
+ }
605
856
  if (bound.roleBindings === undefined)
606
857
  return bound;
607
858
  const actual = Object.keys(bound.roleBindings).sort();
@@ -734,15 +985,216 @@ export const createPlaybookRuntime = (options) => {
734
985
  pendingCall,
735
986
  });
736
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
+ };
737
1182
  const stateIdentity = (state) => {
738
1183
  return state.stateId === undefined ? {} : { stateId: state.stateId };
739
1184
  };
740
- const enqueueTracedEmission = (type, payload, meta = {}, describedEmission) => {
741
- const runtimePorts = requirePorts();
1185
+ const enqueueTracedEmission = (type, payload, meta = {}, describedEmission, aborts) => {
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 = {}) => {
742
1194
  const identity = requireSessionIdentity();
743
1195
  const jsonPayload = snapshotJsonValue(payload, `trace ${type} payload`);
744
- const trace = Object.freeze({
745
- schemaVersion: 3,
1196
+ return Object.freeze({
1197
+ schemaVersion: 4,
746
1198
  sessionId: identity.sessionId,
747
1199
  playbookId: identity.playbookId,
748
1200
  rootSessionId: identity.rootSessionId,
@@ -760,12 +1212,25 @@ export const createPlaybookRuntime = (options) => {
760
1212
  ...(meta.callId !== undefined ? { callId: meta.callId } : {}),
761
1213
  payload: jsonPayload,
762
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 });
763
1221
  return enqueue(async () => {
764
- await runtimePorts.emitTelemetry({ topic: TRACE_TOPIC, payload: trace });
765
- await describedEmission?.(runtimePorts);
766
- });
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);
1232
+ }, aborts);
767
1233
  };
768
- const emitTrace = (type, payload, meta = {}) => enqueueTracedEmission(type, payload, meta);
769
1234
  const emitBoundaryStatus = async (message, state) => {
770
1235
  const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
771
1236
  await enqueueTracedEmission('status.emitted', {
@@ -777,20 +1242,24 @@ export const createPlaybookRuntime = (options) => {
777
1242
  }, { turnId: currentTurnId }, (runtimePorts) => runtimePorts.emitStatus(message));
778
1243
  };
779
1244
  const emitCallStarted = async (startedType, finishedType, identity, meta, signal) => {
1245
+ const aborts = abortReasonClassifier(signal);
780
1246
  try {
781
- await emitTrace(startedType, identity, meta);
1247
+ await emitTrace(startedType, identity, meta, aborts);
782
1248
  }
783
1249
  catch (error) {
784
1250
  latchControlPlaneError(error, signal);
785
1251
  try {
786
1252
  await emitTrace(finishedType, {
787
1253
  ...identity,
788
- status: 'error',
1254
+ // A started-trace sink rejection causally identical to the
1255
+ // boundary reason is the abort's own evidence: the pair
1256
+ // finishes 'aborted', not 'error' (DR-036 §4).
1257
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
789
1258
  error: normalizeErrorFull(error) ?? {
790
1259
  name: 'Error',
791
1260
  message: String(error),
792
1261
  },
793
- }, meta);
1262
+ }, meta, aborts);
794
1263
  }
795
1264
  catch {
796
1265
  // Preserve the start failure after one best-effort finish attempt.
@@ -799,6 +1268,7 @@ export const createPlaybookRuntime = (options) => {
799
1268
  }
800
1269
  };
801
1270
  const runJudgeCall = async (prompt, signal, purpose, callStateId) => {
1271
+ const aborts = abortReasonClassifier(signal);
802
1272
  const identity = {
803
1273
  purpose,
804
1274
  ...(callStateId !== undefined ? { stateId: callStateId } : {}),
@@ -826,15 +1296,18 @@ export const createPlaybookRuntime = (options) => {
826
1296
  latchControlPlaneError(error, signal);
827
1297
  await emitTrace('judge.call.finished', {
828
1298
  ...identity,
829
- status: signal.aborted ? 'aborted' : 'error',
1299
+ // Only the exact abort reason is cancellation; a distinct
1300
+ // failure under an aborted signal stays an error
1301
+ // (slc/link.md §Abort).
1302
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
830
1303
  error: normalizeErrorFull(error) ?? {
831
1304
  name: 'Error',
832
1305
  message: String(error),
833
1306
  },
834
- }, { turnId: currentTurnId, callId });
1307
+ }, { turnId: currentTurnId, callId }, aborts);
835
1308
  throw error;
836
1309
  }
837
- await emitTrace('judge.call.finished', { ...identity, status: 'ok', reply: finalText }, { turnId: currentTurnId, callId });
1310
+ await emitTrace('judge.call.finished', { ...identity, status: 'ok', reply: finalText }, { turnId: currentTurnId, callId }, aborts);
838
1311
  return finalText;
839
1312
  });
840
1313
  if (queued === undefined) {
@@ -843,7 +1316,8 @@ export const createPlaybookRuntime = (options) => {
843
1316
  return queued;
844
1317
  };
845
1318
  const callJudge = (prompt, signal, purpose, callStateId) => trackBoundaryCall(runJudgeCall(prompt, signal, purpose, callStateId));
846
- const runPlayerCall = async (input, signal) => {
1319
+ const runPlayerCall = async (input, signal, continuation) => {
1320
+ const aborts = abortReasonClassifier(signal);
847
1321
  if (!ROLE_ID_SET.has(input.role)) {
848
1322
  throw new TypeError(`DECIDE player input role must name a declared local role`);
849
1323
  }
@@ -854,13 +1328,17 @@ export const createPlaybookRuntime = (options) => {
854
1328
  let resume;
855
1329
  try {
856
1330
  signal.throwIfAborted();
857
- resume = selectPlayerResume(roleId, playerId);
1331
+ resume =
1332
+ continuation !== undefined &&
1333
+ Object.prototype.hasOwnProperty.call(continuation, 'resume')
1334
+ ? continuation.resume
1335
+ : selectPlayerResume(roleId, playerId);
858
1336
  }
859
1337
  catch (error) {
860
1338
  latchControlPlaneError(error, signal);
861
1339
  throw error;
862
1340
  }
863
- const callId = `player-${++playerCallSequence}`;
1341
+ const callId = continuation?.callId ?? `player-${++playerCallSequence}`;
864
1342
  const identity = {
865
1343
  stateId: input.stateId,
866
1344
  sourceItem: input.sourceItem,
@@ -870,12 +1348,15 @@ export const createPlaybookRuntime = (options) => {
870
1348
  };
871
1349
  const emitFailure = (error) => emitTrace('player.call.finished', {
872
1350
  ...identity,
873
- status: signal.aborted ? 'aborted' : 'error',
1351
+ // Only the exact abort reason is cancellation; a distinct
1352
+ // failure under an aborted signal stays an error
1353
+ // (slc/link.md §Abort).
1354
+ status: isAbortFailure(error, signal) ? 'aborted' : 'error',
874
1355
  error: normalizeErrorFull(error) ?? {
875
1356
  name: 'Error',
876
1357
  message: String(error),
877
1358
  },
878
- }, { turnId: currentTurnId, callId });
1359
+ }, { turnId: currentTurnId, callId }, aborts);
879
1360
  if (inFlightPlayerKeys.has(playerKey)) {
880
1361
  const error = new Error(`resolved player key "${playerKey}" already has an in-flight call`);
881
1362
  await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, { turnId: currentTurnId, callId }, signal);
@@ -952,10 +1433,11 @@ export const createPlaybookRuntime = (options) => {
952
1433
  ...(result.error !== undefined
953
1434
  ? { error: normalizeErrorFull(result.error) }
954
1435
  : {}),
955
- }, { turnId: currentTurnId, callId });
1436
+ }, { turnId: currentTurnId, callId }, aborts);
956
1437
  return {
957
1438
  roleId,
958
1439
  ...(playerId === undefined ? {} : { playerId }),
1440
+ callId,
959
1441
  result,
960
1442
  };
961
1443
  }
@@ -963,57 +1445,646 @@ export const createPlaybookRuntime = (options) => {
963
1445
  inFlightPlayerKeys.delete(playerKey);
964
1446
  }
965
1447
  };
966
- const callPlayer = (input, signal) => {
967
- return trackBoundaryCall(runPlayerCall(input, signal));
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
+ };
968
1468
  };
969
- const player = fromPromise(async ({ input, signal }) => {
970
- const combined = combineSignals(signal, currentSignal);
971
- // XState starts invoked actors while publishing the entering snapshot.
972
- // Yield through the runtime emission queue before crossing the player
973
- // boundary so state trace/status always precede its call-start trace.
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 = [];
974
1843
  try {
975
- await flush();
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
+ }
976
1941
  }
977
1942
  catch (error) {
978
- latchControlPlaneError(error, combined);
979
- throw error;
1943
+ for (const member of Object.values(members))
1944
+ member?.result.reject(error);
980
1945
  }
981
- combined.throwIfAborted();
982
- let { roleId, playerId, result } = await callPlayer(input, combined);
983
- if (result.status === 'ok' && isEmptyFinalText(result.finalText)) {
984
- // DR-028: an `ok` result whose finalText is missing, empty, or
985
- // whitespace-only earns exactly one corrective re-ask — the same
986
- // composed call repeated, traced by runPlayerCall as its own
987
- // player-call pair, with the resume selection re-read from the
988
- // token map the first result left (PBRT-38). An abort that lands
989
- // between the two calls ends the turn without the re-ask (aborts
990
- // are never retried), and a rejecting finish emission rejects
991
- // `callPlayer` itself, so it never reaches this branch (PBRT-47).
992
- combined.throwIfAborted();
993
- ({ roleId, playerId, result } = await callPlayer(input, combined));
1946
+ finally {
1947
+ completedProposalCohortTurnId = turnId;
1948
+ pendingProposalCohort.clear();
1949
+ activeProposalCohort = undefined;
994
1950
  }
995
- if (result.status !== 'ok') {
996
- throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
1951
+ };
1952
+ const queueProposalCohortMember = (input, signal) => {
1953
+ if (pendingProposalCohort.has(input.role)) {
1954
+ throw new Error(`DECIDE proposal cohort already registered role ${input.role}`);
997
1955
  }
998
- const finalText = result.finalText ?? '';
999
- if (isEmptyFinalText(finalText)) {
1000
- throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
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();
1001
1970
  }
1002
- combined.throwIfAborted();
1971
+ return member.result.promise;
1972
+ };
1973
+ const callPlayer = (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());
2032
+ };
2033
+ const player = fromPromise(async ({ input, signal }) => {
2034
+ const combined = combineSignals(signal, currentSignal);
2035
+ const settlementAborts = abortReasonClassifier(combined);
1003
2036
  try {
1004
- const prompt = buildAdjudicatorPrompt(input, finalText);
1005
- return parseAdjudication(await callJudge(prompt, combined, 'player-output-adjudication', input.stateId), input, finalText);
2037
+ // XState starts invoked actors while publishing the entering snapshot.
2038
+ // Yield through the runtime emission queue before crossing the player
2039
+ // boundary so state trace/status always precede its call-start trace.
2040
+ combined.throwIfAborted();
2041
+ try {
2042
+ await flush(settlementAborts);
2043
+ }
2044
+ catch (error) {
2045
+ latchControlPlaneError(error, combined);
2046
+ throw error;
2047
+ }
2048
+ combined.throwIfAborted();
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)) {
2053
+ // DR-028: an `ok` result whose finalText is missing, empty, or
2054
+ // whitespace-only earns exactly one corrective re-ask — the same
2055
+ // composed call repeated, traced by runPlayerCall as its own
2056
+ // player-call pair, with the resume selection re-read from the
2057
+ // token map the first result left (PBRT-38). An abort that lands
2058
+ // between the two calls ends the turn without the re-ask (aborts
2059
+ // are never retried), and a rejecting finish emission rejects
2060
+ // `callPlayer` itself, so it never reaches this branch (PBRT-47).
2061
+ combined.throwIfAborted();
2062
+ ({ roleId, playerId, callId, result } = await callPlayer(input, combined));
2063
+ }
2064
+ if (result.status !== 'ok') {
2065
+ throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "${result.status}"${result.error ? `: ${result.error}` : ''}`);
2066
+ }
2067
+ const finalText = result.finalText ?? '';
2068
+ if (isEmptyFinalText(finalText)) {
2069
+ throw new Error(`${roleLabel(roleId)}${playerId === undefined ? '' : ` (${playerId})`} returned status "ok" with no finalText`);
2070
+ }
2071
+ combined.throwIfAborted();
2072
+ const governedOutput = governedPlayerOutputs.get(result);
2073
+ if (governedOutput !== undefined) {
2074
+ governedPlayerOutputs.delete(result);
2075
+ return governedOutput;
2076
+ }
2077
+ throw new Error('DECIDE governed player result was not reconciled against repository evidence');
1006
2078
  }
1007
- catch (error) {
1008
- latchControlPlaneError(error, combined);
1009
- throw error;
2079
+ finally {
2080
+ actorSettlementAborts.push(settlementAborts);
1010
2081
  }
1011
2082
  });
1012
2083
  nestedBridge = createNestedPlaybookBridge({
1013
2084
  nextCallId: () => `playbook-${++playbookCallSequence}`,
1014
2085
  getBoundarySignal: () => currentSignal,
1015
2086
  callPlaybook: (request, signal) => trackBoundaryCall(Promise.resolve(requirePorts().callPlaybook(request, signal))),
1016
- emitStarted: async (event) => {
2087
+ emitStarted: async (event, aborts) => {
1017
2088
  playbookCallTurnIds.set(event.callId, currentTurnId);
1018
2089
  await emitTrace('playbook.call.started', {
1019
2090
  stateId: event.stateId,
@@ -1022,9 +2093,9 @@ export const createPlaybookRuntime = (options) => {
1022
2093
  }, {
1023
2094
  ...(currentTurnId === undefined ? {} : { turnId: currentTurnId }),
1024
2095
  callId: event.callId,
1025
- });
2096
+ }, aborts);
1026
2097
  },
1027
- emitFinished: async (event) => {
2098
+ emitFinished: async (event, aborts) => {
1028
2099
  const turnId = playbookCallTurnIds.get(event.callId);
1029
2100
  try {
1030
2101
  await emitTrace('playbook.call.finished', {
@@ -1035,50 +2106,88 @@ export const createPlaybookRuntime = (options) => {
1035
2106
  }, {
1036
2107
  ...(turnId === undefined ? {} : { turnId }),
1037
2108
  callId: event.callId,
1038
- });
2109
+ }, aborts);
1039
2110
  }
1040
2111
  finally {
1041
2112
  playbookCallTurnIds.delete(event.callId);
1042
2113
  }
1043
2114
  },
1044
2115
  drain: flush,
1045
- bindResumeSignal: (signal) => {
2116
+ bindResumeSignal: (signal, aborts) => {
1046
2117
  currentSignal = signal;
2118
+ currentAborts = aborts ?? abortReasonClassifier(signal);
1047
2119
  },
1048
- onControlPlaneError: (error) => {
1049
- const signal = currentSignal;
1050
- if (!signal || !isAbortFailure(error, signal)) {
2120
+ bindActorSettlement: (aborts) => {
2121
+ actorSettlementAborts.push(aborts);
2122
+ },
2123
+ onControlPlaneError: (error, aborts) => {
2124
+ if (!aborts?.isAbortReason(error) &&
2125
+ !currentAborts?.isAbortReason(error)) {
1051
2126
  controlPlaneError ??= error;
1052
2127
  }
1053
2128
  },
1054
- onBackgroundError: (error) => {
1055
- collectFailure(emissionFailures, error);
2129
+ onBackgroundError: (error, aborts) => {
2130
+ if (!aborts?.isAbortReason(error)) {
2131
+ collectFailure(emissionFailures, error);
2132
+ }
1056
2133
  },
1057
2134
  });
1058
2135
  const providedMachine = decideMachine.provide({
1059
2136
  actors: { player, playbook: nestedBridge.actorLogic },
1060
2137
  });
2138
+ const consumeActorSettlementAborts = (forSnapshot = false) => {
2139
+ const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
2140
+ actorSettlementErrorAborts = undefined;
2141
+ if (forSnapshot && aborts !== undefined) {
2142
+ actorSettlementErrorAborts = aborts;
2143
+ queueMicrotask(() => {
2144
+ if (actorSettlementErrorAborts === aborts) {
2145
+ actorSettlementErrorAborts = undefined;
2146
+ }
2147
+ });
2148
+ }
2149
+ return aborts;
2150
+ };
1061
2151
  const inspect = (event) => {
1062
- if (event.type !== '@xstate.snapshot')
1063
- return;
1064
2152
  if (actor === undefined || event.actorRef !== actor)
1065
2153
  return;
1066
2154
  if (suppressInspectionEmissions)
1067
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;
2167
+ const settlementAborts = consumeActorSettlementAborts(true);
1068
2168
  try {
1069
2169
  const snapshot = event.snapshot;
1070
2170
  const state = normalizePlaybookSnapshot(snapshot);
1071
2171
  const prior = previousState ?? state;
2172
+ let acceptedOutcomes = [];
2173
+ try {
2174
+ acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
2175
+ }
2176
+ catch (error) {
2177
+ latchInspectionError(error);
2178
+ }
1072
2179
  const context = snapshot.context;
1073
- const fsmPayload = telemetryPayload(prior, state, event.event, context);
2180
+ const fsmPayload = telemetryPayload(prior, state, event.event, context, hiddenDeferredOperationId === undefined
2181
+ ? undefined
2182
+ : 'commitCoderProposal');
1074
2183
  const describedFsmPayload = snapshotJsonValue(fsmPayload, 'described FSM telemetry');
1075
2184
  void enqueueTracedEmission('fsm.transition', fsmPayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitTelemetry({
1076
2185
  topic: TELEMETRY_TOPIC,
1077
2186
  payload: describedFsmPayload,
1078
- })).catch(() => undefined);
2187
+ }), settlementAborts).catch(() => undefined);
1079
2188
  const priorIds = new Set(previousState?.activeStateIds ?? []);
1080
2189
  previousState = state;
1081
- const pendingQuestions = pendingQuestionsFromContext(context);
2190
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
1082
2191
  const bossRelevantStateIds = state.activeStateIds.filter((stateId) => STATUS_STATE_IDS.has(stateId));
1083
2192
  const scheduleStatus = (message, stateId, data) => {
1084
2193
  const tracePayload = {
@@ -1088,8 +2197,11 @@ export const createPlaybookRuntime = (options) => {
1088
2197
  ...(data !== undefined ? { data } : {}),
1089
2198
  };
1090
2199
  assertJsonSafe(tracePayload);
1091
- void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data)).catch(() => undefined);
2200
+ void enqueueTracedEmission('status.emitted', tracePayload, { turnId: currentTurnId }, (emissionPorts) => emissionPorts.emitStatus(message, data), settlementAborts).catch(() => undefined);
1092
2201
  };
2202
+ for (const acceptedOutcome of acceptedOutcomes) {
2203
+ void enqueueAcceptedOutcomeEmission(acceptedOutcome, state, settlementAborts).catch(() => undefined);
2204
+ }
1093
2205
  for (const activeStateId of state.activeStateIds) {
1094
2206
  if (priorIds.has(activeStateId) ||
1095
2207
  !STATUS_STATE_IDS.has(activeStateId)) {
@@ -1116,11 +2228,13 @@ export const createPlaybookRuntime = (options) => {
1116
2228
  }
1117
2229
  }
1118
2230
  catch (error) {
1119
- latchInspectionError(error);
2231
+ acceptedOutcomeConsumer.reset();
2232
+ latchInspectionError(error, settlementAborts);
1120
2233
  }
1121
2234
  };
1122
2235
  const createRuntimeActor = (machineSnapshot) => {
1123
2236
  previousState = undefined;
2237
+ acceptedOutcomeConsumer.reset();
1124
2238
  // DR-014 §1: a restore rehydrates the persisted machine snapshot;
1125
2239
  // XState derives context/value from it and ignores `input` then.
1126
2240
  actor = createActor(providedMachine, {
@@ -1132,6 +2246,15 @@ export const createPlaybookRuntime = (options) => {
1132
2246
  }),
1133
2247
  inspect,
1134
2248
  });
2249
+ // A synchronous FSM action throw errors the actor without any pending
2250
+ // boundary await to observe it; unobserved, XState would surface it via
2251
+ // reportUnhandledError as an uncaughtException. Observe it here: latch
2252
+ // it as a control error while a turn signal is active (unless it is
2253
+ // the abort reason itself), otherwise collect it with the emission
2254
+ // failures (slc/link.md §Abort).
2255
+ actor.subscribe({
2256
+ error: (error) => latchInspectionError(error, consumeActorSettlementAborts()),
2257
+ });
1135
2258
  };
1136
2259
  // PBRT-6: the single seam that stops this runtime's actor. Stopping a
1137
2260
  // still-running actor fires one more `@xstate.snapshot` for the *unchanged*
@@ -1144,6 +2267,7 @@ export const createPlaybookRuntime = (options) => {
1144
2267
  if (!actor)
1145
2268
  return;
1146
2269
  suppressInspectionEmissions = true;
2270
+ acceptedOutcomeConsumer.reset();
1147
2271
  actor.stop();
1148
2272
  };
1149
2273
  const startActor = () => {
@@ -1162,16 +2286,21 @@ export const createPlaybookRuntime = (options) => {
1162
2286
  const live = actor;
1163
2287
  if (!live)
1164
2288
  throw new Error('decide runtime: actor is not initialized');
2289
+ refreshReconciliationProjection();
2290
+ if (hasUnresolvedReconciliation())
2291
+ return null;
1165
2292
  const snapshot = live.getSnapshot();
1166
2293
  const context = snapshot.context;
1167
2294
  const state = normalizePlaybookSnapshot(snapshot, {
1168
2295
  pendingCall: nestedBridge.getPendingCall(),
1169
2296
  });
1170
- const pendingQuestions = pendingQuestionsFromContext(context);
2297
+ const pendingQuestions = visiblePendingQuestionsForState(state, context);
2298
+ const failed = state.activeStateIds.includes('failed');
1171
2299
  if (pendingQuestions.length === 0 &&
1172
2300
  (snapshot.status === 'done' ||
1173
2301
  state.activeStateIds.includes('ready') ||
1174
- state.activeStateIds.includes('failed'))) {
2302
+ (failed &&
2303
+ automaticReplayPolicy.allowsFailureStateRetry()))) {
1175
2304
  return { type: 'START_DECIDE', callerTopic: text };
1176
2305
  }
1177
2306
  if (pendingQuestions.length === 0)
@@ -1183,6 +2312,458 @@ export const createPlaybookRuntime = (options) => {
1183
2312
  const raw = await callJudge(prompt, signal, 'boss-input-classification', state.stateId);
1184
2313
  return parseClassification(raw, text, pendingQuestions.map(({ questionId }) => questionId));
1185
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
+ };
1186
2767
  const resultForSnapshot = (signal) => {
1187
2768
  const live = actor;
1188
2769
  if (!live)
@@ -1191,33 +2772,53 @@ export const createPlaybookRuntime = (options) => {
1191
2772
  const pendingCall = nestedBridge.getPendingCall();
1192
2773
  const state = normalizePlaybookSnapshot(snapshot, { pendingCall });
1193
2774
  const context = snapshot.context;
1194
- if (signal?.aborted) {
1195
- return {
1196
- outcome: 'aborted',
1197
- state,
1198
- ...(signal.reason === undefined
1199
- ? {}
1200
- : {
1201
- error: normalizeErrorFull(signal.reason) ?? {
1202
- name: 'AbortError',
1203
- message: String(signal.reason),
1204
- },
1205
- }),
1206
- };
1207
- }
2775
+ const abortedResult = (abortSignal) => ({
2776
+ outcome: 'aborted',
2777
+ state,
2778
+ ...(abortSignal.reason === undefined
2779
+ ? {}
2780
+ : {
2781
+ error: normalizeErrorFull(abortSignal.reason) ?? {
2782
+ name: 'AbortError',
2783
+ message: String(abortSignal.reason),
2784
+ },
2785
+ }),
2786
+ });
2787
+ if (snapshot.status === 'error') {
2788
+ // An errored actor outranks a coincident abort unless the actor's
2789
+ // error is the abort reason itself (slc/link.md §Abort).
2790
+ const actorError = snapshot.error;
2791
+ if (actorError !== undefined &&
2792
+ signal !== undefined &&
2793
+ isAbortFailure(actorError, signal)) {
2794
+ return abortedResult(signal);
2795
+ }
2796
+ throw (actorError ?? new Error('decide runtime actor entered error status'));
2797
+ }
2798
+ // Terminal completion outranks a coincident abort (DR-036 §3): reporting
2799
+ // 'aborted' over a completed machine would hide a terminal state that the
2800
+ // next turn silently restarts, duplicating the workflow's side effects.
1208
2801
  if (snapshot.status === 'done') {
1209
2802
  const output = snapshot.output;
1210
2803
  if (output !== undefined)
1211
2804
  assertJsonSafe(output, 'terminal output');
2805
+ const stateDescription = state.activeStateIds.includes('done')
2806
+ ? STATE_DESCRIPTIONS.done
2807
+ : state.activeStateIds.includes('reportedReviewFailure')
2808
+ ? STATE_DESCRIPTIONS.reportedReviewFailure
2809
+ : undefined;
2810
+ if (stateDescription === undefined) {
2811
+ throw new Error('decide runtime: completed actor has no authored final-state description');
2812
+ }
1212
2813
  return {
1213
2814
  outcome: 'terminal',
1214
2815
  state,
2816
+ stateDescription,
1215
2817
  ...(output === undefined ? {} : { output }),
1216
2818
  };
1217
2819
  }
1218
- if (snapshot.status === 'error') {
1219
- throw (snapshot.error ??
1220
- new Error('decide runtime actor entered error status'));
2820
+ if (signal?.aborted) {
2821
+ return abortedResult(signal);
1221
2822
  }
1222
2823
  if (state.activeStateIds.includes('failed')) {
1223
2824
  const error = normalizeErrorFull(context.lastError);
@@ -1286,8 +2887,12 @@ export const createPlaybookRuntime = (options) => {
1286
2887
  activeEmissionCalls.clear();
1287
2888
  emissionQueue.clear();
1288
2889
  judgeQueue.clear();
2890
+ semanticCompletionQueue.clear();
1289
2891
  actor = undefined;
1290
2892
  currentSignal = undefined;
2893
+ currentAborts = undefined;
2894
+ actorSettlementAborts.length = 0;
2895
+ actorSettlementErrorAborts = undefined;
1291
2896
  currentTurnId = undefined;
1292
2897
  ports = undefined;
1293
2898
  sessionIdentity = undefined;
@@ -1300,7 +2905,20 @@ export const createPlaybookRuntime = (options) => {
1300
2905
  judgeCallSequence = 0;
1301
2906
  playerCallSequence = 0;
1302
2907
  playbookCallSequence = 0;
2908
+ applyCallSequence = 0;
1303
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;
1304
2922
  lifecycleStarted = false;
1305
2923
  };
1306
2924
  return {
@@ -1321,6 +2939,10 @@ export const createPlaybookRuntime = (options) => {
1321
2939
  ports = identity.ports;
1322
2940
  sessionIdentity = identity;
1323
2941
  try {
2942
+ if (deferredEffects !== undefined) {
2943
+ effectLedgerMirror = readEffectLedger();
2944
+ synchronizeDeferredProjection(effectLedgerMirror);
2945
+ }
1324
2946
  suppressInspectionEmissions = false;
1325
2947
  createRuntimeActor();
1326
2948
  const state = currentState();
@@ -1341,6 +2963,9 @@ export const createPlaybookRuntime = (options) => {
1341
2963
  initInFlight = undefined;
1342
2964
  }
1343
2965
  },
2966
+ describe: describeControlView,
2967
+ unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
2968
+ apply: applyControlAction,
1344
2969
  // DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
1345
2970
  // session, including one already-started suspended REVIEW call.
1346
2971
  // Defined only at a safe capture point — initialized, not disposing
@@ -1388,8 +3013,12 @@ export const createPlaybookRuntime = (options) => {
1388
3013
  return undefined;
1389
3014
  const machine = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
1390
3015
  const context = actor.getSnapshot().context;
3016
+ if (deferredEffects !== undefined) {
3017
+ synchronizeDeferredProjection(readEffectLedger());
3018
+ }
3019
+ const unresolved = hasUnresolvedReconciliation();
1391
3020
  return {
1392
- schemaVersion: 3,
3021
+ schemaVersion: 4,
1393
3022
  playbookId: sessionIdentity.playbookId,
1394
3023
  machine,
1395
3024
  roleResumeTokens: snapshotRoleResumeTokens(),
@@ -1401,12 +3030,15 @@ export const createPlaybookRuntime = (options) => {
1401
3030
  playbookCall: playbookCallSequence,
1402
3031
  },
1403
3032
  state,
1404
- pendingBossQuestions: pendingQuestionsFromContext(context).map((pending) => ({
1405
- questionId: pending.questionId,
1406
- asker: pending.asker,
1407
- question: pending.question,
1408
- sourceItem: pending.sourceItem,
1409
- })),
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,
1410
3042
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
1411
3043
  };
1412
3044
  },
@@ -1424,6 +3056,21 @@ export const createPlaybookRuntime = (options) => {
1424
3056
  }
1425
3057
  const identity = bindSession(session);
1426
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
+ }
1427
3074
  const suspendedCall = boundSnapshot.suspendedCall;
1428
3075
  let finishInitialization;
1429
3076
  const initialization = new Promise((resolve) => {
@@ -1440,6 +3087,7 @@ export const createPlaybookRuntime = (options) => {
1440
3087
  judgeCallSequence = boundSnapshot.sequences.judgeCall;
1441
3088
  playerCallSequence = boundSnapshot.sequences.playerCall;
1442
3089
  playbookCallSequence = boundSnapshot.sequences.playbookCall;
3090
+ applyCallSequence = boundSnapshot.sequences.trace;
1443
3091
  if (identity.playerSessions) {
1444
3092
  priorExternalRoleTokens = snapshotRoleResumeTokens();
1445
3093
  }
@@ -1448,6 +3096,7 @@ export const createPlaybookRuntime = (options) => {
1448
3096
  if (suspendedCall !== undefined) {
1449
3097
  playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
1450
3098
  }
3099
+ synchronizeDeferredProjection(effectLedgerMirror);
1451
3100
  suppressInspectionEmissions = true;
1452
3101
  createRuntimeActor(boundSnapshot.machine);
1453
3102
  actor?.start();
@@ -1500,12 +3149,17 @@ export const createPlaybookRuntime = (options) => {
1500
3149
  const turnId = ++turnSequence;
1501
3150
  currentTurnId = turnId;
1502
3151
  currentSignal = turn.signal;
3152
+ currentAborts = abortReasonClassifier(turn.signal);
1503
3153
  controlPlaneError = undefined;
1504
3154
  let result = resultForSnapshot(turn.signal);
1505
3155
  let settlement = result;
1506
3156
  const failures = [];
1507
3157
  try {
1508
3158
  await emitTrace('boss.input.received', { text: turn.text }, { turnId });
3159
+ // A boundary entered aborted records the attempted input, then refuses
3160
+ // delivery before deterministic mapping or the classifier can perform
3161
+ // any host-visible work (DR-036 §5).
3162
+ turn.signal.throwIfAborted();
1509
3163
  if (turn.text.trim().length === 0) {
1510
3164
  const state = currentState();
1511
3165
  result = { outcome: 'no-action', state };
@@ -1514,24 +3168,90 @@ export const createPlaybookRuntime = (options) => {
1514
3168
  const event = await classify(turn.text, turn.signal);
1515
3169
  if (!event) {
1516
3170
  const state = currentState();
1517
- await emitBoundaryStatus('No playbook action classified.', state);
3171
+ if (!hasUnresolvedReconciliation()) {
3172
+ await emitBoundaryStatus('No playbook action classified.', state);
3173
+ }
1518
3174
  result = { outcome: 'no-action', state };
1519
3175
  }
1520
3176
  else if (event.type === 'NO_ACTION') {
1521
3177
  result = { outcome: 'no-action', state: currentState() };
1522
3178
  }
1523
3179
  else {
1524
- await emitBoundaryStatus(event.type, currentState());
1525
- if (actor.getSnapshot().status === 'done') {
1526
- stopActor();
1527
- 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
+ }
1528
3254
  }
1529
- actor.send(event);
1530
- await driveToQuiescence();
1531
- await drainBoundaryCallsAndEmissions();
1532
- if (controlPlaneError !== undefined)
1533
- throw controlPlaneError;
1534
- result = resultForSnapshot(turn.signal);
1535
3255
  }
1536
3256
  }
1537
3257
  settlement = {
@@ -1541,16 +3261,19 @@ export const createPlaybookRuntime = (options) => {
1541
3261
  }
1542
3262
  catch (error) {
1543
3263
  const primaryError = controlPlaneError;
3264
+ // Only a rejection that is the exact abort reason settles as the
3265
+ // cancellation; a distinct failure observed while the signal is
3266
+ // aborted remains a control error (slc/link.md §Abort).
1544
3267
  if (primaryError !== undefined) {
1545
3268
  collectFailure(failures, primaryError);
1546
3269
  }
1547
- else if (!turn.signal.aborted) {
3270
+ else if (!isAbortFailure(error, turn.signal)) {
1548
3271
  collectFailure(failures, error);
1549
3272
  }
1550
3273
  const state = currentState();
1551
3274
  const effectiveError = primaryError ?? error;
1552
3275
  result =
1553
- turn.signal.aborted && primaryError === undefined
3276
+ isAbortFailure(error, turn.signal) && primaryError === undefined
1554
3277
  ? resultForSnapshot(turn.signal)
1555
3278
  : {
1556
3279
  outcome: 'failed',
@@ -1571,12 +3294,14 @@ export const createPlaybookRuntime = (options) => {
1571
3294
  catch (error) {
1572
3295
  const primaryError = controlPlaneError;
1573
3296
  const effectiveError = primaryError ?? error;
1574
- collectFailure(failures, effectiveError);
3297
+ // A drain rejection that is the exact abort reason evidences the
3298
+ // cancellation, not a control-plane failure (slc/link.md §Abort).
3299
+ const drainAborted = isAbortFailure(effectiveError, turn.signal);
3300
+ if (!drainAborted)
3301
+ collectFailure(failures, effectiveError);
1575
3302
  const state = currentState();
1576
3303
  result = {
1577
- outcome: turn.signal.aborted && primaryError === undefined
1578
- ? 'aborted'
1579
- : 'failed',
3304
+ outcome: drainAborted ? 'aborted' : 'failed',
1580
3305
  state,
1581
3306
  error: normalizeErrorFull(effectiveError) ?? {
1582
3307
  name: 'Error',
@@ -1585,21 +3310,31 @@ export const createPlaybookRuntime = (options) => {
1585
3310
  };
1586
3311
  settlement = { ...result, ...stateIdentity(state) };
1587
3312
  }
1588
- currentSignal = undefined;
1589
3313
  try {
1590
3314
  await emitTrace('boss.input.settled', settlement, { turnId });
1591
3315
  }
1592
3316
  catch (error) {
1593
- collectFailure(failures, error);
3317
+ // A settlement-trace rejection that is the exact abort reason also
3318
+ // evidences the cancellation (slc/link.md §Abort).
3319
+ if (!isAbortFailure(error, turn.signal)) {
3320
+ collectFailure(failures, error);
3321
+ }
1594
3322
  }
1595
3323
  try {
1596
3324
  await flush();
1597
3325
  }
1598
3326
  catch (error) {
1599
- collectFailure(failures, error);
3327
+ // A late flush rejection that is the exact abort reason likewise
3328
+ // evidences the cancellation; the settled result already labels
3329
+ // the turn aborted then (slc/link.md §Abort).
3330
+ if (!isAbortFailure(error, turn.signal)) {
3331
+ collectFailure(failures, error);
3332
+ }
1600
3333
  }
1601
3334
  finally {
1602
3335
  const primaryError = controlPlaneError;
3336
+ currentSignal = undefined;
3337
+ currentAborts = undefined;
1603
3338
  currentTurnId = undefined;
1604
3339
  controlPlaneError = undefined;
1605
3340
  if (primaryError !== undefined)
@@ -1623,8 +3358,13 @@ export const createPlaybookRuntime = (options) => {
1623
3358
  if (currentSignal !== undefined) {
1624
3359
  throw new Error('decide runtime: another runtime turn is active');
1625
3360
  }
3361
+ refreshReconciliationProjection();
3362
+ if (hasUnresolvedReconciliation()) {
3363
+ return { outcome: 'no-action', state: currentState() };
3364
+ }
1626
3365
  currentTurnId = playbookCallTurnIds.get(callId);
1627
3366
  currentSignal = signal;
3367
+ currentAborts = abortReasonClassifier(signal);
1628
3368
  controlPlaneError = undefined;
1629
3369
  let runResult;
1630
3370
  let operationError;
@@ -1654,14 +3394,50 @@ export const createPlaybookRuntime = (options) => {
1654
3394
  catch (error) {
1655
3395
  drainError = error;
1656
3396
  }
1657
- const failure = controlPlaneError ?? drainError ?? operationError;
3397
+ const aborts = currentAborts ?? abortReasonClassifier(signal);
3398
+ // The control latch has already classified its failure as distinct
3399
+ // under the operation that owned it. Only still-unclassified drain and
3400
+ // operation candidates may be cancellation evidence for this resume.
3401
+ const controlFailure = controlPlaneError;
3402
+ const drainAbort = controlFailure === undefined &&
3403
+ drainError !== undefined &&
3404
+ aborts.isAbortReason(drainError);
3405
+ const operationAbort = controlFailure === undefined &&
3406
+ operationError !== undefined &&
3407
+ aborts.isAbortReason(operationError);
3408
+ const abortEvidence = (drainAbort ? drainError : undefined) ??
3409
+ (operationAbort ? operationError : undefined);
3410
+ const failure = controlFailure ??
3411
+ (drainAbort ? undefined : drainError) ??
3412
+ (operationAbort ? undefined : operationError);
1658
3413
  currentSignal = undefined;
3414
+ currentAborts = undefined;
1659
3415
  currentTurnId = undefined;
1660
3416
  controlPlaneError = undefined;
1661
3417
  if (failure !== undefined)
1662
3418
  throw failure;
3419
+ if (abortEvidence !== undefined &&
3420
+ runResult?.outcome !== 'terminal' &&
3421
+ runResult?.outcome !== 'suspended') {
3422
+ const state = currentState();
3423
+ runResult = {
3424
+ outcome: 'aborted',
3425
+ state,
3426
+ error: normalizeErrorFull(abortEvidence) ?? {
3427
+ name: 'AbortError',
3428
+ message: String(abortEvidence),
3429
+ },
3430
+ };
3431
+ }
1663
3432
  if (runResult === undefined) {
1664
- throw new Error('decide runtime: playbook resume produced no result');
3433
+ if (signal.aborted) {
3434
+ // Every candidate was the abort's own evidence: settle on the
3435
+ // machine's state under the aborted boundary signal (DR-036 §4).
3436
+ runResult = resultForSnapshot(signal);
3437
+ }
3438
+ else {
3439
+ throw new Error('decide runtime: playbook resume produced no result');
3440
+ }
1665
3441
  }
1666
3442
  return runResult;
1667
3443
  },
@@ -1717,13 +3493,30 @@ export const createPlaybookRuntime = (options) => {
1717
3493
  activeEmissionCalls.clear();
1718
3494
  emissionQueue.clear();
1719
3495
  judgeQueue.clear();
3496
+ semanticCompletionQueue.clear();
1720
3497
  actor = undefined;
1721
3498
  currentSignal = undefined;
3499
+ currentAborts = undefined;
3500
+ actorSettlementAborts.length = 0;
3501
+ actorSettlementErrorAborts = undefined;
1722
3502
  currentTurnId = undefined;
1723
3503
  ports = undefined;
1724
3504
  sessionIdentity = undefined;
1725
3505
  previousState = undefined;
1726
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;
1727
3520
  disposed = true;
1728
3521
  }
1729
3522
  if (failures.length === 1)
@@ -1734,7 +3527,16 @@ export const createPlaybookRuntime = (options) => {
1734
3527
  })();
1735
3528
  return disposalPromise;
1736
3529
  },
3530
+ // @internal — test-only parity with the shared factory's bridge escape
3531
+ // hatch. This is hidden by the PlaybookRuntime return type.
3532
+ _getNestedBridge() {
3533
+ return nestedBridge;
3534
+ },
1737
3535
  };
3536
+ }
3537
+ export const createPlaybookRuntime = (factoryInput) => {
3538
+ const construction = schema3Construction(factoryInput);
3539
+ return createDecidePlaybookRuntime(construction.configuredOptions, construction.hostCapabilities);
1738
3540
  };
1739
3541
  export const _internal = {
1740
3542
  composePlayerPrompt,
@@ -1743,9 +3545,9 @@ export const _internal = {
1743
3545
  buildClassifierPrompt,
1744
3546
  parseClassification,
1745
3547
  buildAdjudicatorPrompt,
1746
- parseAdjudication,
1747
3548
  combineSignals,
1748
3549
  pendingQuestionsFromContext,
3550
+ pendingQuestionsForState,
1749
3551
  normalizeErrorCompact,
1750
3552
  normalizeErrorFull,
1751
3553
  STATE_DESCRIPTIONS,
@@ -1753,6 +3555,7 @@ export const _internal = {
1753
3555
  ROLE_STATE_IDS,
1754
3556
  VERBATIM_PAYLOAD_FIELDS,
1755
3557
  BOSS_INTERRUPT_TARGETS,
3558
+ UNFINISHED_FINAL_STATE_IDS,
1756
3559
  CONTINUATION_PREAMBLE,
1757
3560
  TELEMETRY_TOPIC,
1758
3561
  };