@sublang/playbook 9.0.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/cli.md +109 -21
- package/docs/configuration.md +89 -40
- package/docs/embedding.md +126 -12
- package/package.json +14 -3
- package/reference/sdlc/captain.md +14 -10
- package/reference/sdlc/captain.playbook/captain.fsm.d.ts +33 -13
- package/reference/sdlc/captain.playbook/captain.fsm.js +80 -9
- package/reference/sdlc/captain.playbook/captain.fsm.ts +137 -18
- package/reference/sdlc/captain.playbook/captain.gears.md +10 -6
- package/reference/sdlc/captain.playbook/captain.playbook.d.ts +5 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +140 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +188 -16
- package/reference/sdlc/code.md +35 -16
- package/reference/sdlc/code.playbook/bin/interactive-session.js +228 -23
- package/reference/sdlc/code.playbook/bin/launch-config.js +611 -221
- package/reference/sdlc/code.playbook/bin/playbook.js +304 -178
- package/reference/sdlc/code.playbook/bin/replay-observer.js +221 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +669 -215
- package/reference/sdlc/code.playbook/bin/session-store.js +4546 -502
- package/reference/sdlc/code.playbook/code.fsm.d.ts +7 -0
- package/reference/sdlc/code.playbook/code.fsm.js +74 -25
- package/reference/sdlc/code.playbook/code.fsm.ts +83 -29
- package/reference/sdlc/code.playbook/code.gears.md +0 -2
- package/reference/sdlc/code.playbook/code.playbook.d.ts +5 -2
- package/reference/sdlc/code.playbook/code.playbook.js +54 -2
- package/reference/sdlc/code.playbook/code.playbook.ts +75 -6
- package/reference/sdlc/code.playbook/code.registry.d.ts +10 -3
- package/reference/sdlc/code.playbook/code.registry.js +10 -3
- package/reference/sdlc/code.playbook/code.registry.ts +23 -5
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +103 -8
- package/reference/sdlc/code.playbook/playbook-captain.js +1871 -75
- package/reference/sdlc/code.playbook/playbook-captain.ts +2801 -102
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +14 -10
- package/reference/sdlc/code.playbook/session-store.d.ts +82 -0
- package/reference/sdlc/code.playbook/session-store.js +113 -0
- package/reference/sdlc/decide.md +24 -16
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +7 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +80 -29
- package/reference/sdlc/decide.playbook/decide.fsm.ts +89 -31
- package/reference/sdlc/decide.playbook/decide.gears.md +0 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +13 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1712 -91
- package/reference/sdlc/decide.playbook/decide.playbook.ts +2677 -136
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +7 -3
- package/reference/sdlc/decide.playbook/decide.registry.js +10 -3
- package/reference/sdlc/decide.playbook/decide.registry.ts +20 -5
- package/reference/sdlc/review.md +36 -18
- package/reference/sdlc/review.playbook/review.fsm.d.ts +7 -0
- package/reference/sdlc/review.playbook/review.fsm.js +133 -12
- package/reference/sdlc/review.playbook/review.fsm.ts +140 -12
- package/reference/sdlc/review.playbook/review.playbook.d.ts +5 -2
- package/reference/sdlc/review.playbook/review.playbook.js +65 -2
- package/reference/sdlc/review.playbook/review.playbook.ts +83 -6
- package/reference/sdlc/review.playbook/review.registry.d.ts +10 -3
- package/reference/sdlc/review.playbook/review.registry.js +10 -3
- package/reference/sdlc/review.playbook/review.registry.ts +23 -5
- package/slc/gears2fsm.md +6 -5
- package/slc/link.md +544 -41
- package/src/accepted-outcome.d.ts +18 -0
- package/src/accepted-outcome.js +94 -0
- package/src/accepted-outcome.ts +140 -0
- package/src/runtime.d.ts +164 -3
- package/src/runtime.ts +213 -2
- package/src/xstate-playbook-runtime.d.ts +149 -10
- package/src/xstate-playbook-runtime.js +2569 -270
- package/src/xstate-playbook-runtime.ts +4133 -490
- package/src/xstate-runtime.d.ts +59 -1
- package/src/xstate-runtime.js +866 -7
- package/src/xstate-runtime.ts +1397 -7
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// machinery that slc/link.md previously regenerated inside every linked
|
|
6
6
|
// `<name>.playbook.ts` artifact — actor wiring, boundary tracing, judge
|
|
7
7
|
// classification/adjudication, script execution, nested-playbook bridging,
|
|
8
|
-
// Boss-reply suspension, snapshot/
|
|
8
|
+
// Boss-reply suspension, snapshot restore/adoption, and disposal — lives here
|
|
9
|
+
// once.
|
|
9
10
|
// A linked artifact supplies only its per-workflow `spec` (options
|
|
10
11
|
// validation and any strategy overrides) and its own FSM; the factory
|
|
11
12
|
// interprets the FSM data the artifact already carries.
|
|
@@ -15,9 +16,12 @@
|
|
|
15
16
|
// its behavior tests are the equivalence proof. Do not change observable
|
|
16
17
|
// behavior here without consulting those suites.
|
|
17
18
|
import { spawn } from 'node:child_process';
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
18
21
|
import PQueue from 'p-queue';
|
|
19
22
|
import { createActor, fromPromise } from 'xstate';
|
|
20
|
-
import {
|
|
23
|
+
import { createAcceptedOutcomeConsumer, } from './accepted-outcome.js';
|
|
24
|
+
import { assertPlaybookRuntimeSnapshot, assertPlaybookEffectLedger, combineAbortSignals, createNestedPlaybookBridge, detachPersistedMachineSnapshot, normalizeError, normalizePlaybookSnapshot, snapshotJsonValue, snapshotPlaybookSession, isPlaybookEffectLedgerMonotonicExtension, PlaybookSemanticCandidateStructureError, reconcilePlaybookSemanticEvidence, validateCaptainResult, validatePlayerResult, waitForPlaybookQuiescence, } from './xstate-runtime.js';
|
|
21
25
|
export const BOSS_REPLY_ERRORS = {
|
|
22
26
|
missingQuestion: "needsBossReply outcome missing 'question' field",
|
|
23
27
|
unregisteredState: (stateId) => `state ${stateId} declared needsBossReply but is not registered as resumable`,
|
|
@@ -56,6 +60,95 @@ function isEmptyFinalText(finalText) {
|
|
|
56
60
|
return finalText === undefined || finalText.trim().length === 0;
|
|
57
61
|
}
|
|
58
62
|
const emptyOkRetryFailures = new WeakSet();
|
|
63
|
+
const HOST_CAPABILITIES_OPTION_KEY = 'hostCapabilities';
|
|
64
|
+
const UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID = 'reconcile:unresolved-effect';
|
|
65
|
+
const UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID = 'abandon:unresolved-effect';
|
|
66
|
+
function deferredValue() {
|
|
67
|
+
let resolve;
|
|
68
|
+
let reject;
|
|
69
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
70
|
+
resolve = resolvePromise;
|
|
71
|
+
reject = rejectPromise;
|
|
72
|
+
});
|
|
73
|
+
return { promise, resolve, reject };
|
|
74
|
+
}
|
|
75
|
+
function assertNoConfiguredHostCapabilities(value, label) {
|
|
76
|
+
if (value !== null &&
|
|
77
|
+
typeof value === 'object' &&
|
|
78
|
+
Object.prototype.hasOwnProperty.call(value, HOST_CAPABILITIES_OPTION_KEY)) {
|
|
79
|
+
throw new TypeError(`${label} configured options must not contain hostCapabilities`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function configuredOptionsFromFactoryInput(value, label) {
|
|
83
|
+
if (value === null ||
|
|
84
|
+
typeof value !== 'object' ||
|
|
85
|
+
Array.isArray(value) ||
|
|
86
|
+
(Object.getPrototypeOf(value) !== Object.prototype &&
|
|
87
|
+
Object.getPrototypeOf(value) !== null)) {
|
|
88
|
+
throw new TypeError(`${label} schema-3 factory input must be a plain object`);
|
|
89
|
+
}
|
|
90
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
91
|
+
const keys = Reflect.ownKeys(value);
|
|
92
|
+
if (keys.length !== 2 ||
|
|
93
|
+
!keys.includes('configuredOptions') ||
|
|
94
|
+
!keys.includes(HOST_CAPABILITIES_OPTION_KEY) ||
|
|
95
|
+
keys.some((key) => {
|
|
96
|
+
const descriptor = descriptors[key];
|
|
97
|
+
return (descriptor?.get !== undefined ||
|
|
98
|
+
descriptor?.set !== undefined ||
|
|
99
|
+
descriptor?.enumerable !== true ||
|
|
100
|
+
!Object.prototype.hasOwnProperty.call(descriptor, 'value'));
|
|
101
|
+
})) {
|
|
102
|
+
throw new TypeError(`${label} schema-3 factory input must contain exactly configuredOptions and hostCapabilities data properties`);
|
|
103
|
+
}
|
|
104
|
+
const hostCapabilities = descriptors.hostCapabilities.value;
|
|
105
|
+
if (hostCapabilities === null ||
|
|
106
|
+
typeof hostCapabilities !== 'object' ||
|
|
107
|
+
Array.isArray(hostCapabilities)) {
|
|
108
|
+
throw new TypeError(`${label} schema-3 factory input hostCapabilities must be a live object`);
|
|
109
|
+
}
|
|
110
|
+
const configuredOptions = descriptors.configuredOptions.value;
|
|
111
|
+
assertNoConfiguredHostCapabilities(configuredOptions, label);
|
|
112
|
+
const ledgerDescriptor = Object.getOwnPropertyDescriptor(hostCapabilities, 'effectLedger');
|
|
113
|
+
if (ledgerDescriptor === undefined ||
|
|
114
|
+
!Object.prototype.hasOwnProperty.call(ledgerDescriptor, 'value') ||
|
|
115
|
+
ledgerDescriptor.get !== undefined ||
|
|
116
|
+
ledgerDescriptor.set !== undefined) {
|
|
117
|
+
throw new TypeError(`${label} schema-3 factory input hostCapabilities.effectLedger must be an own data property`);
|
|
118
|
+
}
|
|
119
|
+
const effectLedger = ledgerDescriptor.value;
|
|
120
|
+
if (effectLedger === null ||
|
|
121
|
+
typeof effectLedger !== 'object' ||
|
|
122
|
+
Array.isArray(effectLedger) ||
|
|
123
|
+
typeof effectLedger.snapshot !== 'function' ||
|
|
124
|
+
typeof effectLedger.writeAhead !== 'function') {
|
|
125
|
+
throw new TypeError(`${label} schema-3 factory input hostCapabilities.effectLedger must expose snapshot and writeAhead functions`);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
configuredOptions,
|
|
129
|
+
hostCapabilities,
|
|
130
|
+
effectLedger: effectLedger,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
|
|
134
|
+
const descriptor = hostCapabilities === undefined
|
|
135
|
+
? undefined
|
|
136
|
+
: Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
|
|
137
|
+
const repository = descriptor?.value;
|
|
138
|
+
if (descriptor === undefined ||
|
|
139
|
+
!Object.prototype.hasOwnProperty.call(descriptor, 'value') ||
|
|
140
|
+
descriptor.get !== undefined ||
|
|
141
|
+
descriptor.set !== undefined ||
|
|
142
|
+
repository === null ||
|
|
143
|
+
typeof repository !== 'object' ||
|
|
144
|
+
Array.isArray(repository) ||
|
|
145
|
+
typeof repository.runExclusive !==
|
|
146
|
+
'function' ||
|
|
147
|
+
typeof repository.runDeferred !== 'function') {
|
|
148
|
+
throw new TypeError(`${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`);
|
|
149
|
+
}
|
|
150
|
+
return repository;
|
|
151
|
+
}
|
|
59
152
|
function markEmptyOkRetryFailure(error) {
|
|
60
153
|
emptyOkRetryFailures.add(error);
|
|
61
154
|
return error;
|
|
@@ -78,7 +171,7 @@ function isEmptyOkRetryFailure(error) {
|
|
|
78
171
|
export const RUNTIME_ABI = 1;
|
|
79
172
|
/** The linked-artifact schema versions this engine accepts (DR-022). */
|
|
80
173
|
export const SUPPORTED_ARTIFACT_SCHEMAS = Object.freeze([
|
|
81
|
-
|
|
174
|
+
3,
|
|
82
175
|
]);
|
|
83
176
|
// PBRT-50: validate a declaration against the loaded engine, schema first,
|
|
84
177
|
// so one clear diagnostic covers a fully skewed artifact. Declaration-free
|
|
@@ -106,6 +199,7 @@ function assertRuntimeCompat(compat, label) {
|
|
|
106
199
|
throw new TypeError(`${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
|
|
107
200
|
`@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`);
|
|
108
201
|
}
|
|
202
|
+
return artifactSchema;
|
|
109
203
|
}
|
|
110
204
|
// ---------------------------------------------------------------------------
|
|
111
205
|
// Tolerant judge-JSON recovery (slc/link.md §Boss-event mapping).
|
|
@@ -361,6 +455,97 @@ function sortJson(value) {
|
|
|
361
455
|
function stableJson(value, path) {
|
|
362
456
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
363
457
|
}
|
|
458
|
+
// DR-040 task 8: a retained checkpoint authorizes adoption without a replay
|
|
459
|
+
// fence only when the authoritative ledger preserves the checkpoint exactly,
|
|
460
|
+
// has made no deferred-operation progress, and every later physical boundary
|
|
461
|
+
// is complete and proves `unchanged`. This is intentionally the same
|
|
462
|
+
// fail-closed shape as uncertain whole-turn replay.
|
|
463
|
+
function retainedAdoptionCheckpointIsSafe(checkpoint, current) {
|
|
464
|
+
if (checkpoint.boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (!isPlaybookEffectLedgerMonotonicExtension(checkpoint, current)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
if (!isDeepStrictEqual(current.boundaries.slice(0, checkpoint.boundaries.length), checkpoint.boundaries) ||
|
|
471
|
+
!isDeepStrictEqual(current.logicalOperations, checkpoint.logicalOperations)) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return current.boundaries
|
|
475
|
+
.slice(checkpoint.boundaries.length)
|
|
476
|
+
.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged');
|
|
477
|
+
}
|
|
478
|
+
const RETAINED_EFFECT_SESSION_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
479
|
+
function requireAdoptionIdentity(value, path) {
|
|
480
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
481
|
+
throw new TypeError(`${path} must be a non-empty string`);
|
|
482
|
+
}
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
// DR-038 §5: capture the host-owned source lineage before adoption binds
|
|
486
|
+
// anything. The retained stack already carries both identities: a frame's
|
|
487
|
+
// sessionId names its source runtime, while the common rootSessionId names the
|
|
488
|
+
// retained generation. A suspended parent also needs the host's freshly
|
|
489
|
+
// allocated target child id so its bridge can be re-keyed without leaking a
|
|
490
|
+
// source-session id into the new engagement.
|
|
491
|
+
function snapshotAdoptionContext(value, targetSession, sourceSnapshot) {
|
|
492
|
+
const captured = snapshotJsonValue(value, 'playbook adoption context');
|
|
493
|
+
if (captured === null ||
|
|
494
|
+
Array.isArray(captured) ||
|
|
495
|
+
typeof captured !== 'object') {
|
|
496
|
+
throw new TypeError('playbook adoption context must be an object');
|
|
497
|
+
}
|
|
498
|
+
const fields = captured;
|
|
499
|
+
const hasSuspendedCall = sourceSnapshot.suspendedCall !== undefined;
|
|
500
|
+
const expectedKeys = [
|
|
501
|
+
'sourceGenerationId',
|
|
502
|
+
'sourceSessionId',
|
|
503
|
+
...(hasSuspendedCall ? ['targetChildSessionId'] : []),
|
|
504
|
+
].sort();
|
|
505
|
+
const actualKeys = Object.keys(fields).sort();
|
|
506
|
+
if (actualKeys.length !== expectedKeys.length ||
|
|
507
|
+
actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
508
|
+
throw new TypeError(`playbook adoption context must contain exactly ${expectedKeys.join(', ')}`);
|
|
509
|
+
}
|
|
510
|
+
const sourceSessionId = requireAdoptionIdentity(fields.sourceSessionId, 'playbook adoption context sourceSessionId');
|
|
511
|
+
const sourceGenerationId = requireAdoptionIdentity(fields.sourceGenerationId, 'playbook adoption context sourceGenerationId');
|
|
512
|
+
const sourceIsRoot = sourceSessionId === sourceGenerationId;
|
|
513
|
+
if ((targetSession.depth === 0) !== sourceIsRoot) {
|
|
514
|
+
throw new TypeError('playbook adoption context source identities do not match the target frame depth');
|
|
515
|
+
}
|
|
516
|
+
const targetChildSessionId = hasSuspendedCall
|
|
517
|
+
? requireAdoptionIdentity(fields.targetChildSessionId, 'playbook adoption context targetChildSessionId')
|
|
518
|
+
: undefined;
|
|
519
|
+
const sourceIds = new Set([
|
|
520
|
+
sourceSessionId,
|
|
521
|
+
sourceGenerationId,
|
|
522
|
+
...(sourceSnapshot.suspendedCall === undefined
|
|
523
|
+
? []
|
|
524
|
+
: [sourceSnapshot.suspendedCall.childSessionId]),
|
|
525
|
+
]);
|
|
526
|
+
const targetIds = [
|
|
527
|
+
targetSession.sessionId,
|
|
528
|
+
targetSession.rootSessionId,
|
|
529
|
+
...(targetSession.parentSessionId === undefined
|
|
530
|
+
? []
|
|
531
|
+
: [targetSession.parentSessionId]),
|
|
532
|
+
...(targetChildSessionId === undefined ? [] : [targetChildSessionId]),
|
|
533
|
+
];
|
|
534
|
+
if (targetIds.some((identity) => sourceIds.has(identity))) {
|
|
535
|
+
throw new TypeError('playbook adoption target identities must be fresh from the source generation');
|
|
536
|
+
}
|
|
537
|
+
if (targetChildSessionId !== undefined &&
|
|
538
|
+
(targetChildSessionId === targetSession.sessionId ||
|
|
539
|
+
targetChildSessionId === targetSession.rootSessionId ||
|
|
540
|
+
targetChildSessionId === targetSession.parentSessionId)) {
|
|
541
|
+
throw new TypeError('playbook adoption targetChildSessionId must name a fresh child frame');
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
sourceSessionId,
|
|
545
|
+
sourceGenerationId,
|
|
546
|
+
...(targetChildSessionId === undefined ? {} : { targetChildSessionId }),
|
|
547
|
+
});
|
|
548
|
+
}
|
|
364
549
|
/**
|
|
365
550
|
* Default direct-Captain prompt composer (slc/link.md §Captain prompt
|
|
366
551
|
* composition). Placeholder substitution is presence-based: string fields
|
|
@@ -430,6 +615,43 @@ export function defaultBuildJudgePrompt(input, finalText) {
|
|
|
430
615
|
}
|
|
431
616
|
return lines.join('\n');
|
|
432
617
|
}
|
|
618
|
+
function buildGovernedJudgePrompt(input, finalText, outcomes, correction) {
|
|
619
|
+
const lines = [
|
|
620
|
+
'This is hidden control work. Do not call tools, inspect files, or seek external evidence.',
|
|
621
|
+
'Decide only from the supplied player output and declared outcomes.',
|
|
622
|
+
'Reply with exactly one JSON object and no prose.',
|
|
623
|
+
'',
|
|
624
|
+
`The ${input.role} role just produced this output:`,
|
|
625
|
+
'',
|
|
626
|
+
'```',
|
|
627
|
+
finalText,
|
|
628
|
+
'```',
|
|
629
|
+
'',
|
|
630
|
+
'Pick exactly one declared `guard`. Include every semantic-owned field for that guard and no other field.',
|
|
631
|
+
'Do not include presentation-, effect-, or runtime-owned fields; the runtime supplies those from their authoritative evidence.',
|
|
632
|
+
'',
|
|
633
|
+
];
|
|
634
|
+
for (const [guard, description] of Object.entries(input.result)) {
|
|
635
|
+
const semanticFields = Object.entries(outcomes[guard]?.fields ?? {})
|
|
636
|
+
.filter(([, authority]) => authority === 'semantic')
|
|
637
|
+
.map(([field]) => field);
|
|
638
|
+
lines.push(`- \`${guard}\` — semantic fields: ${semanticFields.length === 0
|
|
639
|
+
? '(none)'
|
|
640
|
+
: semanticFields.map((field) => `\`${field}\``).join(', ')}; ${description}`);
|
|
641
|
+
}
|
|
642
|
+
if (correction !== undefined) {
|
|
643
|
+
lines.push('', 'Your first reply was structurally invalid:', '', '```', correction.reply, '```', '', `Validation error: ${correction.error}`, 'Correct only that structure using the same player output and outcome schema.');
|
|
644
|
+
}
|
|
645
|
+
return lines.join('\n');
|
|
646
|
+
}
|
|
647
|
+
function parseGovernedSemanticCandidate(raw) {
|
|
648
|
+
try {
|
|
649
|
+
return parseJudgeJson(raw);
|
|
650
|
+
}
|
|
651
|
+
catch (error) {
|
|
652
|
+
throw new PlaybookSemanticCandidateStructureError(error instanceof Error ? error.message : 'reply is not valid JSON');
|
|
653
|
+
}
|
|
654
|
+
}
|
|
433
655
|
const NO_VERBATIM_FIELDS = new Set();
|
|
434
656
|
/**
|
|
435
657
|
* LLM-judge adjudicator for delegated players. Coerces the player's
|
|
@@ -462,7 +684,12 @@ export async function adjudicatePlayerOutput(spec, input, finalText, ports, sign
|
|
|
462
684
|
const verbatim = finalText.trim();
|
|
463
685
|
for (const field of extractFields(input.result[guard])) {
|
|
464
686
|
if (verbatimFields.has(field)) {
|
|
465
|
-
obj
|
|
687
|
+
Object.defineProperty(obj, field, {
|
|
688
|
+
value: verbatim,
|
|
689
|
+
enumerable: true,
|
|
690
|
+
configurable: true,
|
|
691
|
+
writable: true,
|
|
692
|
+
});
|
|
466
693
|
continue;
|
|
467
694
|
}
|
|
468
695
|
if (typeof obj[field] !== 'string') {
|
|
@@ -490,6 +717,7 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
490
717
|
let roleId;
|
|
491
718
|
let prompt;
|
|
492
719
|
try {
|
|
720
|
+
spec.validateInput?.(input);
|
|
493
721
|
roleId = spec.resolveRoleId(input);
|
|
494
722
|
prompt = spec.composePlayerPrompt(input);
|
|
495
723
|
}
|
|
@@ -503,7 +731,9 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
503
731
|
? boundary.callPlayer(input, roleId, prompt, activeSignal)
|
|
504
732
|
: ports.callPlayer(roleId, prompt, activeSignal, { resume });
|
|
505
733
|
let result = await callPlayer(false);
|
|
506
|
-
if (result.status === 'ok' &&
|
|
734
|
+
if (result.status === 'ok' &&
|
|
735
|
+
isEmptyFinalText(result.finalText) &&
|
|
736
|
+
(spec.allowsCorrectiveReplay?.(result) ?? true)) {
|
|
507
737
|
// An abort that lands between the empty first result and the
|
|
508
738
|
// corrective call ends the turn as ordinary abort settlement with
|
|
509
739
|
// no second host call — aborts are never retried (DR-028 via
|
|
@@ -531,12 +761,20 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
531
761
|
throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
|
|
532
762
|
}
|
|
533
763
|
try {
|
|
534
|
-
const
|
|
764
|
+
const governed = boundary?.takeGovernedPlayerOutput?.(result);
|
|
765
|
+
if (governed?.status === 'unresolved') {
|
|
766
|
+
throw governed.error;
|
|
767
|
+
}
|
|
768
|
+
const output = governed?.status === 'resolved'
|
|
769
|
+
? governed.output
|
|
770
|
+
: await adjudicatePlayerOutput(spec.adjudication, input, finalText, ports, activeSignal, boundary);
|
|
771
|
+
boundary?.recordGovernedPlayerOutput?.(result, output);
|
|
535
772
|
validateBossReplyOutput(input, output, spec.resumableStateIds);
|
|
536
773
|
return output;
|
|
537
774
|
}
|
|
538
775
|
catch (error) {
|
|
539
|
-
if (!isAbortFailure(error, activeSignal)
|
|
776
|
+
if (!isAbortFailure(error, activeSignal) &&
|
|
777
|
+
!isFsmResultFailure(error)) {
|
|
540
778
|
onControlPlaneError?.(error);
|
|
541
779
|
}
|
|
542
780
|
throw error;
|
|
@@ -850,7 +1088,7 @@ function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
|
|
|
850
1088
|
}
|
|
851
1089
|
function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
|
|
852
1090
|
if (value === undefined) {
|
|
853
|
-
throw new TypeError(`${label} roleStates must be supplied for schema
|
|
1091
|
+
throw new TypeError(`${label} roleStates must be supplied for schema 3`);
|
|
854
1092
|
}
|
|
855
1093
|
const captured = snapshotJsonValue(value, `${label} roleStates`);
|
|
856
1094
|
if (!isPlainObject(captured)) {
|
|
@@ -894,23 +1132,189 @@ function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
|
|
|
894
1132
|
}
|
|
895
1133
|
return statuses;
|
|
896
1134
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1135
|
+
const OUTCOME_FIELD_AUTHORITIES = new Set([
|
|
1136
|
+
'presentation',
|
|
1137
|
+
'semantic',
|
|
1138
|
+
'effect',
|
|
1139
|
+
'runtime',
|
|
1140
|
+
]);
|
|
1141
|
+
const REPOSITORY_DISPOSITIONS = new Set([
|
|
1142
|
+
'unchanged',
|
|
1143
|
+
'one-descendant-commit',
|
|
1144
|
+
'deferred',
|
|
1145
|
+
]);
|
|
1146
|
+
const OUTCOME_FIELD_KEY_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
1147
|
+
const SEMANTIC_PAYLOAD_FIELDS = new Set([
|
|
1148
|
+
'irNumber',
|
|
1149
|
+
'irTask',
|
|
1150
|
+
]);
|
|
1151
|
+
function requireExactObjectKeys(value, expected, path) {
|
|
1152
|
+
const actual = Object.keys(value);
|
|
1153
|
+
const missing = expected.filter((key) => !actual.includes(key));
|
|
1154
|
+
const extra = actual.filter((key) => !expected.includes(key));
|
|
1155
|
+
if (missing.length === 0 && extra.length === 0)
|
|
1156
|
+
return;
|
|
1157
|
+
throw new TypeError(`${path} must contain exactly ${expected.join(', ')}` +
|
|
1158
|
+
(missing.length === 0 ? '' : `; missing ${missing.join(', ')}`) +
|
|
1159
|
+
(extra.length === 0 ? '' : `; unknown ${extra.join(', ')}`));
|
|
1160
|
+
}
|
|
1161
|
+
function requireAuthorityIdentifier(value, path) {
|
|
1162
|
+
if (!OUTCOME_FIELD_KEY_PATTERN.test(value)) {
|
|
1163
|
+
throw new TypeError(`${path} must be an identifier`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function snapshotOutcomeAuthority(descriptor, label, playerStates, verbatimPayloadFields) {
|
|
1167
|
+
const path = `${label} outcomeAuthority`;
|
|
1168
|
+
if (descriptor === undefined ||
|
|
1169
|
+
!Object.prototype.hasOwnProperty.call(descriptor, 'value') ||
|
|
1170
|
+
descriptor.enumerable !== true) {
|
|
1171
|
+
throw new TypeError(`${path} must be an own enumerable data property for schema 3`);
|
|
1172
|
+
}
|
|
1173
|
+
const captured = snapshotJsonValue(descriptor.value, path);
|
|
1174
|
+
if (!isPlainObject(captured)) {
|
|
1175
|
+
throw new TypeError(`${path} must be an object`);
|
|
1176
|
+
}
|
|
1177
|
+
requireExactObjectKeys(captured, ['governedPlayerStates'], path);
|
|
1178
|
+
const governed = captured.governedPlayerStates;
|
|
1179
|
+
if (!isPlainObject(governed)) {
|
|
1180
|
+
throw new TypeError(`${path}.governedPlayerStates must be an object`);
|
|
1181
|
+
}
|
|
1182
|
+
for (const stateId of playerStates.keys()) {
|
|
1183
|
+
if (!Object.prototype.hasOwnProperty.call(governed, stateId)) {
|
|
1184
|
+
throw new TypeError(`${path}.governedPlayerStates must declare player state ${stateId}`);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
for (const stateId of Object.keys(governed)) {
|
|
1188
|
+
if (!playerStates.has(stateId)) {
|
|
1189
|
+
throw new TypeError(`${path}.governedPlayerStates.${stateId} does not name a player state`);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const usedVerbatimFields = new Set();
|
|
1193
|
+
const normalizedStates = Object.create(null);
|
|
1194
|
+
for (const [stateId, rawOutcomes] of Object.entries(governed)) {
|
|
1195
|
+
const statePath = `${path}.governedPlayerStates.${stateId}`;
|
|
1196
|
+
if (!isPlainObject(rawOutcomes) || Object.keys(rawOutcomes).length === 0) {
|
|
1197
|
+
throw new TypeError(`${statePath} must declare at least one outcome`);
|
|
1198
|
+
}
|
|
1199
|
+
const outcomes = Object.create(null);
|
|
1200
|
+
for (const [outcome, rawSpec] of Object.entries(rawOutcomes)) {
|
|
1201
|
+
requireAuthorityIdentifier(outcome, `${statePath} outcome key`);
|
|
1202
|
+
const outcomePath = `${statePath}.${outcome}`;
|
|
1203
|
+
if (!isPlainObject(rawSpec)) {
|
|
1204
|
+
throw new TypeError(`${outcomePath} must be an object`);
|
|
1205
|
+
}
|
|
1206
|
+
requireExactObjectKeys(rawSpec, ['fields', 'repositoryDisposition'], outcomePath);
|
|
1207
|
+
if (!isPlainObject(rawSpec.fields)) {
|
|
1208
|
+
throw new TypeError(`${outcomePath}.fields must be an object`);
|
|
1209
|
+
}
|
|
1210
|
+
const fields = Object.create(null);
|
|
1211
|
+
for (const [field, authority] of Object.entries(rawSpec.fields)) {
|
|
1212
|
+
requireAuthorityIdentifier(field, `${outcomePath}.fields key`);
|
|
1213
|
+
if (field === 'guard') {
|
|
1214
|
+
throw new TypeError(`${outcomePath}.fields.guard is not allowed; the outcome key owns the semantic discriminator`);
|
|
1215
|
+
}
|
|
1216
|
+
if (typeof authority !== 'string' ||
|
|
1217
|
+
!OUTCOME_FIELD_AUTHORITIES.has(authority)) {
|
|
1218
|
+
throw new TypeError(`${outcomePath}.fields.${field} must name presentation, semantic, effect, or runtime authority`);
|
|
1219
|
+
}
|
|
1220
|
+
const requiredAuthorities = new Set();
|
|
1221
|
+
if (field === 'latestCommit')
|
|
1222
|
+
requiredAuthorities.add('effect');
|
|
1223
|
+
if (SEMANTIC_PAYLOAD_FIELDS.has(field)) {
|
|
1224
|
+
requiredAuthorities.add('semantic');
|
|
1225
|
+
}
|
|
1226
|
+
if (field === 'question' || verbatimPayloadFields.has(field)) {
|
|
1227
|
+
requiredAuthorities.add('presentation');
|
|
1228
|
+
}
|
|
1229
|
+
if (requiredAuthorities.size > 1) {
|
|
1230
|
+
throw new TypeError(`${outcomePath}.fields.${field} has conflicting linker authority requirements`);
|
|
1231
|
+
}
|
|
1232
|
+
const requiredAuthority = [...requiredAuthorities][0];
|
|
1233
|
+
if (requiredAuthority !== undefined && authority !== requiredAuthority) {
|
|
1234
|
+
throw new TypeError(`${outcomePath}.fields.${field} must use ${requiredAuthority} authority`);
|
|
1235
|
+
}
|
|
1236
|
+
if (verbatimPayloadFields.has(field))
|
|
1237
|
+
usedVerbatimFields.add(field);
|
|
1238
|
+
fields[field] = authority;
|
|
1239
|
+
}
|
|
1240
|
+
const disposition = rawSpec.repositoryDisposition;
|
|
1241
|
+
if (typeof disposition !== 'string' ||
|
|
1242
|
+
!REPOSITORY_DISPOSITIONS.has(disposition)) {
|
|
1243
|
+
throw new TypeError(`${outcomePath}.repositoryDisposition must be unchanged, one-descendant-commit, or deferred`);
|
|
1244
|
+
}
|
|
1245
|
+
if (disposition !== 'one-descendant-commit' &&
|
|
1246
|
+
Object.values(fields).includes('effect')) {
|
|
1247
|
+
throw new TypeError(`${outcomePath} may declare effect-owned fields only for one-descendant-commit`);
|
|
1248
|
+
}
|
|
1249
|
+
outcomes[outcome] = Object.freeze({
|
|
1250
|
+
fields: Object.freeze(fields),
|
|
1251
|
+
repositoryDisposition: disposition,
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
for (const [outcome, outcomeSpec] of Object.entries(outcomes)) {
|
|
1255
|
+
if (outcomeSpec.repositoryDisposition !== 'deferred')
|
|
1256
|
+
continue;
|
|
1257
|
+
if (outcome !== 'needsBossReply') {
|
|
1258
|
+
throw new TypeError(`${statePath}.${outcome} may use deferred only for needsBossReply`);
|
|
1259
|
+
}
|
|
1260
|
+
if (outcomeSpec.fields.question !== 'presentation') {
|
|
1261
|
+
throw new TypeError(`${statePath}.needsBossReply deferred outcome must declare presentation-owned question`);
|
|
1262
|
+
}
|
|
1263
|
+
if (!Object.entries(outcomes).some(([other, candidate]) => other !== outcome &&
|
|
1264
|
+
candidate.repositoryDisposition === 'one-descendant-commit')) {
|
|
1265
|
+
throw new TypeError(`${statePath}.needsBossReply deferred outcome requires another one-descendant-commit outcome`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
normalizedStates[stateId] = Object.freeze(outcomes);
|
|
1269
|
+
}
|
|
1270
|
+
for (const field of verbatimPayloadFields) {
|
|
1271
|
+
requireAuthorityIdentifier(field, `${path} verbatimPayloadFields entry`);
|
|
1272
|
+
if (!usedVerbatimFields.has(field)) {
|
|
1273
|
+
throw new TypeError(`${path} verbatimPayloadFields entry ${field} is absent from governed payload fields`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return Object.freeze({
|
|
1277
|
+
governedPlayerStates: Object.freeze(normalizedStates),
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
function sameStringSet(left, right) {
|
|
1281
|
+
if (left.length !== right.length)
|
|
1282
|
+
return false;
|
|
1283
|
+
const expected = new Set(right);
|
|
1284
|
+
return left.every((value) => expected.has(value));
|
|
1285
|
+
}
|
|
1286
|
+
function assertGovernedPlayerInput(authority, input, extractFields, label) {
|
|
1287
|
+
if (authority === undefined)
|
|
1288
|
+
return;
|
|
1289
|
+
const state = authority.governedPlayerStates[input.stateId];
|
|
1290
|
+
if (state === undefined) {
|
|
1291
|
+
throw new TypeError(`${label} outcomeAuthority has no governed player state ${input.stateId}`);
|
|
1292
|
+
}
|
|
1293
|
+
const actualOutcomes = Object.keys(input.result);
|
|
1294
|
+
const governedOutcomes = Object.keys(state);
|
|
1295
|
+
if (!sameStringSet(actualOutcomes, governedOutcomes)) {
|
|
1296
|
+
throw new TypeError(`${label} outcomeAuthority for ${input.stateId} must exactly match outcomes ` +
|
|
1297
|
+
governedOutcomes.join(', '));
|
|
1298
|
+
}
|
|
1299
|
+
for (const outcome of governedOutcomes) {
|
|
1300
|
+
const description = input.result[outcome];
|
|
1301
|
+
if (typeof description !== 'string') {
|
|
1302
|
+
throw new TypeError(`${label} player outcome ${input.stateId}.${outcome} must have a string description`);
|
|
1303
|
+
}
|
|
1304
|
+
const describedFields = [...new Set(extractFields(description))];
|
|
1305
|
+
const candidateFields = Object.keys(state[outcome].fields);
|
|
1306
|
+
if (!sameStringSet(describedFields, candidateFields)) {
|
|
1307
|
+
throw new TypeError(`${label} outcomeAuthority fields for ${input.stateId}.${outcome} ` +
|
|
1308
|
+
'must exactly match its described output fields');
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
904
1311
|
}
|
|
905
1312
|
function askerLabel(asker) {
|
|
906
1313
|
return asker.kind === 'captain' ? 'Captain' : asker.roleId;
|
|
907
1314
|
}
|
|
908
1315
|
function makeDefaultStatusesForState(roleStates) {
|
|
909
|
-
return (state, context
|
|
1316
|
+
return (state, context) => {
|
|
910
1317
|
const statuses = [];
|
|
911
|
-
const guard = settlingGuard(event);
|
|
912
|
-
if (guard !== undefined)
|
|
913
|
-
statuses.push({ message: `→ ${guard}` });
|
|
914
1318
|
const stateId = state.stateId;
|
|
915
1319
|
if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) {
|
|
916
1320
|
return statuses;
|
|
@@ -1294,29 +1698,44 @@ function assertFlatStateIdentity(machine, label) {
|
|
|
1294
1698
|
}
|
|
1295
1699
|
}
|
|
1296
1700
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1701
|
+
function rootFinalStateIdsFromMachine(machine) {
|
|
1702
|
+
const config = machine.config;
|
|
1703
|
+
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
1704
|
+
return new Set();
|
|
1705
|
+
}
|
|
1706
|
+
const stateIds = new Set();
|
|
1707
|
+
for (const [stateId, stateDef] of Object.entries(config.states)) {
|
|
1708
|
+
if (isPlainObject(stateDef) && stateDef.type === 'final') {
|
|
1709
|
+
stateIds.add(stateId);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return stateIds;
|
|
1713
|
+
}
|
|
1714
|
+
// PBRT-52: whether a final outcome leaves the procedure unfinished remains
|
|
1715
|
+
// authored link metadata. The machine can still prove the mechanical half:
|
|
1716
|
+
// every declared stable id must resolve to one of its root final states.
|
|
1717
|
+
function assertUnfinishedFinalStateIds(value, machine, label) {
|
|
1718
|
+
if (value === undefined)
|
|
1719
|
+
return;
|
|
1720
|
+
const rootFinalStateIds = rootFinalStateIdsFromMachine(machine);
|
|
1721
|
+
for (const stateId of value) {
|
|
1722
|
+
if (typeof stateId !== 'string' || !rootFinalStateIds.has(stateId)) {
|
|
1723
|
+
throw new TypeError(`${label} unfinishedFinalStateIds entry ${JSON.stringify(stateId)} ` +
|
|
1724
|
+
'does not name a root final state');
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1309
1728
|
export function createXStatePlaybookRuntime(machine, spec) {
|
|
1310
1729
|
const label = spec.label ?? 'playbook';
|
|
1311
1730
|
// DR-022 / PBRT-50: reject an incompatible artifact declaration before any
|
|
1312
1731
|
// machine interpretation, against this loaded engine's own self-report.
|
|
1313
|
-
assertRuntimeCompat(spec.compat, label);
|
|
1732
|
+
const artifactSchema = assertRuntimeCompat(spec.compat, label);
|
|
1314
1733
|
const specDescriptors = Object.getOwnPropertyDescriptors(spec);
|
|
1315
1734
|
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
|
|
1316
|
-
throw new TypeError(`${label}
|
|
1735
|
+
throw new TypeError(`${label} artifacts must supply roleStates, not playerStates`);
|
|
1317
1736
|
}
|
|
1318
1737
|
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
|
|
1319
|
-
throw new TypeError(`${label}
|
|
1738
|
+
throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
|
|
1320
1739
|
}
|
|
1321
1740
|
if (machineDeclaresParallelState(machine)) {
|
|
1322
1741
|
throw new Error(`${label} uses a parallel state; the shared runtime supports only single-region FSMs`);
|
|
@@ -1325,6 +1744,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1325
1744
|
throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
|
|
1326
1745
|
}
|
|
1327
1746
|
assertFlatStateIdentity(machine, label);
|
|
1747
|
+
assertUnfinishedFinalStateIds(spec.unfinishedFinalStateIds, machine, label);
|
|
1748
|
+
const retainedGenerationMetadata = spec.unfinishedFinalStateIds === undefined
|
|
1749
|
+
? undefined
|
|
1750
|
+
: Object.freeze({
|
|
1751
|
+
unfinishedFinalStateIds: Object.freeze([
|
|
1752
|
+
...spec.unfinishedFinalStateIds,
|
|
1753
|
+
]),
|
|
1754
|
+
});
|
|
1328
1755
|
const declaredActors = collectInvokeSources(machine);
|
|
1329
1756
|
const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
|
|
1330
1757
|
// DR-029: source state descriptions label the control actions the
|
|
@@ -1357,18 +1784,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1357
1784
|
((input) => defaultComposePlayerPrompt(input, spec.placeholderFields));
|
|
1358
1785
|
const composeCaptainPrompt = spec.composeCaptainPrompt ??
|
|
1359
1786
|
((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
1787
|
+
const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
1788
|
+
const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
|
|
1360
1789
|
const adjudication = {
|
|
1361
1790
|
...(spec.buildJudgePrompt !== undefined
|
|
1362
1791
|
? { buildJudgePrompt: spec.buildJudgePrompt }
|
|
1363
1792
|
: {}),
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
: {}),
|
|
1367
|
-
...(spec.verbatimPayloadFields !== undefined
|
|
1368
|
-
? { verbatimPayloadFields: spec.verbatimPayloadFields }
|
|
1369
|
-
: {}),
|
|
1793
|
+
extractRequiredFields: extractFields,
|
|
1794
|
+
verbatimPayloadFields,
|
|
1370
1795
|
};
|
|
1371
|
-
const
|
|
1796
|
+
const outcomeAuthority = snapshotOutcomeAuthority(specDescriptors.outcomeAuthority, label, roleStates, verbatimPayloadFields);
|
|
1797
|
+
for (const [stateId, outcomes] of Object.entries(outcomeAuthority.governedPlayerStates)) {
|
|
1798
|
+
if (Object.values(outcomes).some(({ repositoryDisposition }) => repositoryDisposition === 'deferred') &&
|
|
1799
|
+
!resumableStateIds.has(stateId)) {
|
|
1800
|
+
throw new TypeError(`${label} outcomeAuthority deferred state ${stateId} must be registered in resumableStateIds`);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1372
1803
|
// Build the derived classifier unconditionally: it is the sole validator of
|
|
1373
1804
|
// supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
|
|
1374
1805
|
// fail factory construction whether or not this spec overrides the
|
|
@@ -1379,6 +1810,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1379
1810
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
1380
1811
|
const statusesForState = spec.statusesForState ??
|
|
1381
1812
|
makeDefaultStatusesForState(roleStates);
|
|
1813
|
+
const usesDefaultStatuses = spec.statusesForState === undefined;
|
|
1382
1814
|
const classificationStatus = spec.classificationStatus ??
|
|
1383
1815
|
((event) => event.type);
|
|
1384
1816
|
const machineInput = spec.machineInput ?? ((options) => options);
|
|
@@ -1387,8 +1819,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1387
1819
|
const cwd = options?.cwd;
|
|
1388
1820
|
return typeof cwd === 'string' ? cwd : undefined;
|
|
1389
1821
|
});
|
|
1390
|
-
|
|
1391
|
-
const
|
|
1822
|
+
const createPlaybookRuntime = function createPlaybookRuntime(factoryOptions) {
|
|
1823
|
+
const construction = configuredOptionsFromFactoryInput(factoryOptions, label);
|
|
1824
|
+
const configuredOptions = construction.configuredOptions;
|
|
1825
|
+
const effectLedgerCapability = construction.effectLedger;
|
|
1826
|
+
const hasGovernedPlayerStates = Object.keys(outcomeAuthority.governedPlayerStates).length > 0;
|
|
1827
|
+
const acceptedOutcomeConsumer = createAcceptedOutcomeConsumer((source, acceptedOutcome) => {
|
|
1828
|
+
const governedPlayerStates = outcomeAuthority.governedPlayerStates;
|
|
1829
|
+
if (!Object.prototype.hasOwnProperty.call(governedPlayerStates, source)) {
|
|
1830
|
+
return false;
|
|
1831
|
+
}
|
|
1832
|
+
const declarations = governedPlayerStates[source];
|
|
1833
|
+
return (declarations !== undefined &&
|
|
1834
|
+
Object.prototype.hasOwnProperty.call(declarations, acceptedOutcome));
|
|
1835
|
+
});
|
|
1836
|
+
const repositoryCapability = hasGovernedPlayerStates
|
|
1837
|
+
? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label)
|
|
1838
|
+
: undefined;
|
|
1839
|
+
const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
|
|
1840
|
+
let effectLedgerMirror = currentEffectLedger();
|
|
1841
|
+
let retainedEffectSourceSessionId;
|
|
1842
|
+
let retainedEffectReconciliation;
|
|
1843
|
+
let retainedEffectReconciliationRequired = false;
|
|
1844
|
+
const playerBoundaryReceipts = new WeakMap();
|
|
1845
|
+
const governedPlayerSettlements = new WeakMap();
|
|
1846
|
+
const governedSettlementsByBoundaryId = new Map();
|
|
1847
|
+
const governedCompletionEvidenceByBoundaryId = new Map();
|
|
1848
|
+
const unresolvedSemanticBoundaryIds = new Set();
|
|
1849
|
+
let reconstructedGovernedDelivery;
|
|
1850
|
+
let reconstructedGovernedPrefixSequence;
|
|
1851
|
+
const reconstructedGovernedResults = new WeakMap();
|
|
1852
|
+
let reconstructedAcceptancePending;
|
|
1853
|
+
const boundOptions = spec.snapshotOptions(configuredOptions);
|
|
1854
|
+
assertNoConfiguredHostCapabilities(boundOptions, label);
|
|
1392
1855
|
const boundScriptCwd = scriptCwd(boundOptions);
|
|
1393
1856
|
let actor;
|
|
1394
1857
|
let session;
|
|
@@ -1417,6 +1880,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1417
1880
|
// its accepted receipt.
|
|
1418
1881
|
let activeAbortEmission;
|
|
1419
1882
|
let activeTurnId;
|
|
1883
|
+
// The durable host attempt observed by governed calls in the active
|
|
1884
|
+
// public boundary. The failed-state latch survives later no-action turns;
|
|
1885
|
+
// clearing it at every boundary start must not make unsafe replay appear
|
|
1886
|
+
// newly eligible.
|
|
1887
|
+
let activeGovernedBoundarySeen = false;
|
|
1888
|
+
let activeGovernedAttemptId;
|
|
1889
|
+
let activeEffectLedgerPrefixSequence;
|
|
1890
|
+
let failedGovernedAttemptUnknown = false;
|
|
1891
|
+
let failedEffectBoundaryPrefix;
|
|
1892
|
+
let failedGovernedAttemptId;
|
|
1893
|
+
let deferredReconciliationOperationId;
|
|
1894
|
+
let deferredSettlementClosure;
|
|
1895
|
+
let expectedBoundPendingQuestion;
|
|
1896
|
+
let activeDeferredContinuation;
|
|
1897
|
+
let deferInspectionEmissions = false;
|
|
1898
|
+
let deferredInspectionEmissions = [];
|
|
1420
1899
|
let controlPlaneError;
|
|
1421
1900
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
1422
1901
|
// status emitter. undefined before the first inspect firing.
|
|
@@ -1444,6 +1923,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1444
1923
|
const privateResumeTokens = new Map();
|
|
1445
1924
|
const activePlayerKeys = new Set();
|
|
1446
1925
|
const playbookCallTurnIds = new Map();
|
|
1926
|
+
const playbookCallEffectPrefixes = new Map();
|
|
1447
1927
|
// Captain and judge work share one serialized lane (slc/link.md
|
|
1448
1928
|
// §Session lifecycle).
|
|
1449
1929
|
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
@@ -1453,6 +1933,427 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1453
1933
|
// Inspection callbacks enqueue a complete ordered batch synchronously;
|
|
1454
1934
|
// imperative boundaries await their queued work directly.
|
|
1455
1935
|
let emissionFailure;
|
|
1936
|
+
function runtimeLogicalOperations(ledger = effectLedgerMirror) {
|
|
1937
|
+
if (session === undefined)
|
|
1938
|
+
return [];
|
|
1939
|
+
const runtimeSessionIds = new Set([
|
|
1940
|
+
session.sessionId,
|
|
1941
|
+
...(retainedEffectSourceSessionId === undefined
|
|
1942
|
+
? []
|
|
1943
|
+
: [retainedEffectSourceSessionId]),
|
|
1944
|
+
]);
|
|
1945
|
+
return ledger.logicalOperations.filter((operation) => operation.playbookId === session.playbookId &&
|
|
1946
|
+
runtimeSessionIds.has(operation.runtimeSessionId));
|
|
1947
|
+
}
|
|
1948
|
+
function refreshRetainedEffectReconciliation(current = effectLedgerMirror) {
|
|
1949
|
+
const retained = retainedEffectReconciliation;
|
|
1950
|
+
if (retained === undefined) {
|
|
1951
|
+
retainedEffectReconciliationRequired = false;
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
const safe = retainedAdoptionCheckpointIsSafe(retained.checkpoint, current);
|
|
1955
|
+
retainedEffectReconciliationRequired = !safe;
|
|
1956
|
+
if (safe) {
|
|
1957
|
+
retainedEffectReconciliation = undefined;
|
|
1958
|
+
reconstructedGovernedPrefixSequence = undefined;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
function bindRetainedEffectReconciliation(retained, current) {
|
|
1962
|
+
retainedEffectReconciliation = retained;
|
|
1963
|
+
refreshRetainedEffectReconciliation(current);
|
|
1964
|
+
reconstructedGovernedPrefixSequence =
|
|
1965
|
+
retainedEffectReconciliation === undefined
|
|
1966
|
+
? undefined
|
|
1967
|
+
: (retainedEffectReconciliation.checkpoint.boundaries.at(-1)
|
|
1968
|
+
?.sequence ?? 0);
|
|
1969
|
+
}
|
|
1970
|
+
function refreshRetainedEffectFenceFromHost() {
|
|
1971
|
+
if (retainedEffectReconciliation === undefined)
|
|
1972
|
+
return;
|
|
1973
|
+
const retainedBeforeRefresh = retainedEffectReconciliation;
|
|
1974
|
+
try {
|
|
1975
|
+
effectLedgerMirror = currentEffectLedger();
|
|
1976
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
1977
|
+
syncDeferredReconciliationOverlay();
|
|
1978
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
1979
|
+
}
|
|
1980
|
+
catch {
|
|
1981
|
+
// A fence can open only from validated authoritative evidence. If the
|
|
1982
|
+
// live mirror or its source-owned deferred-operation view cannot be
|
|
1983
|
+
// read exactly, keep every ordinary entry point closed.
|
|
1984
|
+
retainedEffectReconciliation ??= retainedBeforeRefresh;
|
|
1985
|
+
deferredReconciliationOperationId = undefined;
|
|
1986
|
+
retainedEffectReconciliationRequired = true;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
function syncDeferredReconciliationOverlay() {
|
|
1990
|
+
const unresolved = runtimeLogicalOperations().filter((operation) => operation.logicalReceipt === undefined &&
|
|
1991
|
+
(operation.checkpointRestorationEligible ||
|
|
1992
|
+
operation.pendingQuestion === undefined));
|
|
1993
|
+
if (unresolved.length > 1) {
|
|
1994
|
+
throw new Error(`${label} effect ledger contains multiple unresolved deferred operations`);
|
|
1995
|
+
}
|
|
1996
|
+
deferredReconciliationOperationId = unresolved[0]?.operationId;
|
|
1997
|
+
}
|
|
1998
|
+
function runtimeBoundaryIsOwned(boundary) {
|
|
1999
|
+
if (session === undefined || boundary.playbookId !== session.playbookId) {
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
return (boundary.runtimeSessionId === session.sessionId ||
|
|
2003
|
+
boundary.runtimeSessionId === retainedEffectSourceSessionId);
|
|
2004
|
+
}
|
|
2005
|
+
function governedOutcomesForBoundary(candidate) {
|
|
2006
|
+
const outcomes = outcomeAuthority?.governedPlayerStates[candidate.sourceStateId];
|
|
2007
|
+
if (outcomes === undefined)
|
|
2008
|
+
return undefined;
|
|
2009
|
+
if (!isPlainObject(candidate.sourceOutcomeSchema) ||
|
|
2010
|
+
!sameStringSet(Object.keys(candidate.sourceOutcomeSchema), Object.keys(outcomes))) {
|
|
2011
|
+
return undefined;
|
|
2012
|
+
}
|
|
2013
|
+
for (const [guard, description] of Object.entries(candidate.sourceOutcomeSchema)) {
|
|
2014
|
+
if (typeof description !== 'string')
|
|
2015
|
+
return undefined;
|
|
2016
|
+
const describedFields = [...new Set(extractFields(description))];
|
|
2017
|
+
if (!sameStringSet(describedFields, Object.keys(outcomes[guard].fields))) {
|
|
2018
|
+
return undefined;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const expectedDispositions = [
|
|
2022
|
+
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
2023
|
+
];
|
|
2024
|
+
if (!sameStringSet(candidate.dispositions, expectedDispositions)) {
|
|
2025
|
+
return undefined;
|
|
2026
|
+
}
|
|
2027
|
+
return outcomes;
|
|
2028
|
+
}
|
|
2029
|
+
function persistedBoundaryReconciliation(candidate, ledger) {
|
|
2030
|
+
const outcomes = governedOutcomesForBoundary(candidate);
|
|
2031
|
+
if (outcomes === undefined || candidate.semanticCandidate === undefined) {
|
|
2032
|
+
return undefined;
|
|
2033
|
+
}
|
|
2034
|
+
let receipt = candidate.physicalReceipt;
|
|
2035
|
+
let historicalDeferred = false;
|
|
2036
|
+
let awaitingLogicalReceipt = false;
|
|
2037
|
+
if (candidate.logicalOperationId !== undefined) {
|
|
2038
|
+
const operation = ledger.logicalOperations.find(({ operationId }) => operationId === candidate.logicalOperationId);
|
|
2039
|
+
if (operation === undefined)
|
|
2040
|
+
return undefined;
|
|
2041
|
+
const latestBoundaryId = operation.boundaryIds.at(-1);
|
|
2042
|
+
if (latestBoundaryId !== candidate.boundaryId) {
|
|
2043
|
+
// Earlier questions remain independently validated historical
|
|
2044
|
+
// evidence. Their physical same-HEAD receipt, candidate, and
|
|
2045
|
+
// reciprocal operation link must still prove a deferred arm.
|
|
2046
|
+
historicalDeferred = true;
|
|
2047
|
+
}
|
|
2048
|
+
else if (operation.logicalReceipt !== undefined) {
|
|
2049
|
+
receipt = operation.logicalReceipt;
|
|
2050
|
+
}
|
|
2051
|
+
else if (operation.pendingQuestion === undefined ||
|
|
2052
|
+
operation.checkpoint === undefined ||
|
|
2053
|
+
!Object.prototype.hasOwnProperty.call(operation, 'playerContinuation')) {
|
|
2054
|
+
return undefined;
|
|
2055
|
+
}
|
|
2056
|
+
else {
|
|
2057
|
+
awaitingLogicalReceipt = true;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const reconciliation = reconcilePlaybookSemanticEvidence({
|
|
2062
|
+
outcomes,
|
|
2063
|
+
semanticCandidate: candidate.semanticCandidate,
|
|
2064
|
+
finalText: candidate.finalText,
|
|
2065
|
+
receipt,
|
|
2066
|
+
});
|
|
2067
|
+
if (awaitingLogicalReceipt &&
|
|
2068
|
+
reconciliation.status !== 'deferred') {
|
|
2069
|
+
return undefined;
|
|
2070
|
+
}
|
|
2071
|
+
return {
|
|
2072
|
+
reconciliation,
|
|
2073
|
+
historicalDeferred,
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
catch {
|
|
2077
|
+
return undefined;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
function boundaryNeedsSemanticReconciliation(candidate, ledger) {
|
|
2081
|
+
if (!runtimeBoundaryIsOwned(candidate))
|
|
2082
|
+
return false;
|
|
2083
|
+
if (governedOutcomesForBoundary(candidate) === undefined)
|
|
2084
|
+
return true;
|
|
2085
|
+
const persisted = persistedBoundaryReconciliation(candidate, ledger);
|
|
2086
|
+
if (persisted !== undefined) {
|
|
2087
|
+
if (persisted.reconciliation.status === 'unresolved')
|
|
2088
|
+
return true;
|
|
2089
|
+
if (persisted.historicalDeferred) {
|
|
2090
|
+
return persisted.reconciliation.status !== 'deferred';
|
|
2091
|
+
}
|
|
2092
|
+
if (persisted.reconciliation.status === 'deferred' &&
|
|
2093
|
+
candidate.logicalOperationId === undefined) {
|
|
2094
|
+
return true;
|
|
2095
|
+
}
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
2098
|
+
if (candidate.physicalReceipt === undefined) {
|
|
2099
|
+
// An unsafe retained suffix is already owned by the task-8 adoption
|
|
2100
|
+
// fence, which may still expose its exact deferred-restoration
|
|
2101
|
+
// action. A same-generation incomplete boundary has no such fence
|
|
2102
|
+
// and remains semantic/effect unresolved until host reconstruction.
|
|
2103
|
+
return retainedEffectReconciliation === undefined;
|
|
2104
|
+
}
|
|
2105
|
+
if (typeof candidate.finalText === 'string' &&
|
|
2106
|
+
candidate.finalText.trim().length > 0) {
|
|
2107
|
+
return true;
|
|
2108
|
+
}
|
|
2109
|
+
return candidate.physicalReceipt.classification !== 'unchanged';
|
|
2110
|
+
}
|
|
2111
|
+
function refreshUnresolvedSemanticReconciliation(current = effectLedgerMirror) {
|
|
2112
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
2113
|
+
if (outcomeAuthority === undefined || session === undefined)
|
|
2114
|
+
return;
|
|
2115
|
+
for (const candidate of current.boundaries) {
|
|
2116
|
+
if (boundaryNeedsSemanticReconciliation(candidate, current)) {
|
|
2117
|
+
unresolvedSemanticBoundaryIds.add(candidate.boundaryId);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
function prepareReconstructedGovernedDelivery(state, ledger = effectLedgerMirror) {
|
|
2122
|
+
reconstructedGovernedDelivery = undefined;
|
|
2123
|
+
if (state.stateId === undefined ||
|
|
2124
|
+
state.activeStateIds.length !== 1) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
const owned = ledger.boundaries.filter(runtimeBoundaryIsOwned);
|
|
2128
|
+
const candidate = reconstructedGovernedPrefixSequence === undefined
|
|
2129
|
+
? owned.at(-1)
|
|
2130
|
+
: owned.find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
|
|
2131
|
+
if (candidate === undefined || candidate.sourceStateId !== state.stateId) {
|
|
2132
|
+
return;
|
|
2133
|
+
}
|
|
2134
|
+
const persisted = persistedBoundaryReconciliation(candidate, ledger);
|
|
2135
|
+
if (persisted === undefined ||
|
|
2136
|
+
persisted.historicalDeferred ||
|
|
2137
|
+
persisted.reconciliation.status !== 'resolved' ||
|
|
2138
|
+
typeof candidate.finalText !== 'string') {
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
reconstructedGovernedDelivery = {
|
|
2142
|
+
boundary: candidate,
|
|
2143
|
+
finalText: candidate.finalText,
|
|
2144
|
+
settlement: {
|
|
2145
|
+
status: 'resolved',
|
|
2146
|
+
output: persisted.reconciliation.output,
|
|
2147
|
+
},
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function takeReconstructedGovernedPlayerResult(input, roleId) {
|
|
2151
|
+
const reconstructed = reconstructedGovernedDelivery;
|
|
2152
|
+
if (reconstructed === undefined)
|
|
2153
|
+
return undefined;
|
|
2154
|
+
// A reconstructed envelope is consumable once even when a hostile host
|
|
2155
|
+
// changes its mirror between restore validation and actor startup.
|
|
2156
|
+
reconstructedGovernedDelivery = undefined;
|
|
2157
|
+
const current = currentEffectLedger();
|
|
2158
|
+
effectLedgerMirror = current;
|
|
2159
|
+
syncDeferredReconciliationOverlay();
|
|
2160
|
+
refreshUnresolvedSemanticReconciliation(current);
|
|
2161
|
+
const completed = current.boundaries.find(({ boundaryId }) => boundaryId === reconstructed.boundary.boundaryId);
|
|
2162
|
+
const expected = reconstructedGovernedPrefixSequence === undefined
|
|
2163
|
+
? current.boundaries.filter(runtimeBoundaryIsOwned).at(-1)
|
|
2164
|
+
: current.boundaries
|
|
2165
|
+
.filter(runtimeBoundaryIsOwned)
|
|
2166
|
+
.find(({ sequence }) => sequence > reconstructedGovernedPrefixSequence);
|
|
2167
|
+
const persisted = completed === undefined
|
|
2168
|
+
? undefined
|
|
2169
|
+
: persistedBoundaryReconciliation(completed, current);
|
|
2170
|
+
if (completed === undefined ||
|
|
2171
|
+
expected?.boundaryId !== completed.boundaryId ||
|
|
2172
|
+
!isDeepStrictEqual(completed, reconstructed.boundary) ||
|
|
2173
|
+
completed.sourceStateId !== input.stateId ||
|
|
2174
|
+
completed.roleId !== roleId ||
|
|
2175
|
+
!isDeepStrictEqual(completed.sourceOutcomeSchema, input.result) ||
|
|
2176
|
+
persisted === undefined ||
|
|
2177
|
+
persisted.historicalDeferred ||
|
|
2178
|
+
persisted.reconciliation.status !== 'resolved' ||
|
|
2179
|
+
completed.finalText !== reconstructed.finalText ||
|
|
2180
|
+
!isDeepStrictEqual(persisted.reconciliation.output, reconstructed.settlement.output)) {
|
|
2181
|
+
unresolvedSemanticBoundaryIds.add(reconstructed.boundary.boundaryId);
|
|
2182
|
+
throw markFsmResultFailure(new Error(`${label} retained governed semantic envelope is no longer exact`));
|
|
2183
|
+
}
|
|
2184
|
+
validateBossReplyOutput(input, reconstructed.settlement.output, resumableStateIds);
|
|
2185
|
+
const result = validatePlayerResult({
|
|
2186
|
+
status: 'ok',
|
|
2187
|
+
finalText: reconstructed.finalText,
|
|
2188
|
+
});
|
|
2189
|
+
playerBoundaryReceipts.set(result, {
|
|
2190
|
+
boundaryId: completed.boundaryId,
|
|
2191
|
+
attemptId: completed.attemptId,
|
|
2192
|
+
});
|
|
2193
|
+
governedPlayerSettlements.set(result, reconstructed.settlement);
|
|
2194
|
+
reconstructedGovernedResults.set(result, completed);
|
|
2195
|
+
return result;
|
|
2196
|
+
}
|
|
2197
|
+
function acceptReconstructedGovernedDelivery(state) {
|
|
2198
|
+
const accepted = reconstructedAcceptancePending;
|
|
2199
|
+
if (accepted === undefined ||
|
|
2200
|
+
state.stateId === accepted.sourceStateId) {
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
reconstructedAcceptancePending = undefined;
|
|
2204
|
+
let current;
|
|
2205
|
+
try {
|
|
2206
|
+
current = currentEffectLedger();
|
|
2207
|
+
effectLedgerMirror = current;
|
|
2208
|
+
syncDeferredReconciliationOverlay();
|
|
2209
|
+
refreshUnresolvedSemanticReconciliation(current);
|
|
2210
|
+
}
|
|
2211
|
+
catch {
|
|
2212
|
+
unresolvedSemanticBoundaryIds.add(accepted.boundaryId);
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
const acknowledged = current.boundaries.find(({ boundaryId }) => boundaryId === accepted.boundaryId);
|
|
2216
|
+
if (acknowledged === undefined ||
|
|
2217
|
+
!isDeepStrictEqual(acknowledged, accepted)) {
|
|
2218
|
+
unresolvedSemanticBoundaryIds.add(accepted.boundaryId);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (reconstructedGovernedPrefixSequence !== undefined) {
|
|
2222
|
+
reconstructedGovernedPrefixSequence = accepted.sequence;
|
|
2223
|
+
prepareReconstructedGovernedDelivery(state, current);
|
|
2224
|
+
if (current.boundaries
|
|
2225
|
+
.filter(runtimeBoundaryIsOwned)
|
|
2226
|
+
.some(({ sequence }) => sequence > reconstructedGovernedPrefixSequence)) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
if (unresolvedSemanticBoundaryIds.size > 0 ||
|
|
2231
|
+
deferredReconciliationOperationId !== undefined) {
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
// Task 9 has now projected the retained, host-acknowledged envelope
|
|
2235
|
+
// into the FSM. Only after that acceptance may the task-8 adoption
|
|
2236
|
+
// marker retire; an unresolved sibling boundary leaves it intact.
|
|
2237
|
+
retainedEffectReconciliation = undefined;
|
|
2238
|
+
retainedEffectReconciliationRequired = false;
|
|
2239
|
+
reconstructedGovernedPrefixSequence = undefined;
|
|
2240
|
+
}
|
|
2241
|
+
function hasUnresolvedReconciliation() {
|
|
2242
|
+
return (deferredReconciliationOperationId !== undefined ||
|
|
2243
|
+
retainedEffectReconciliationRequired ||
|
|
2244
|
+
unresolvedSemanticBoundaryIds.size > 0);
|
|
2245
|
+
}
|
|
2246
|
+
function unresolvedEffectEnvelopeIdentities() {
|
|
2247
|
+
if (session === undefined)
|
|
2248
|
+
return [];
|
|
2249
|
+
const current = currentEffectLedger();
|
|
2250
|
+
effectLedgerMirror = current;
|
|
2251
|
+
refreshRetainedEffectReconciliation(current);
|
|
2252
|
+
syncDeferredReconciliationOverlay();
|
|
2253
|
+
refreshUnresolvedSemanticReconciliation(current);
|
|
2254
|
+
if (!hasUnresolvedReconciliation())
|
|
2255
|
+
return [];
|
|
2256
|
+
const boundaryIds = new Set(unresolvedSemanticBoundaryIds);
|
|
2257
|
+
const operationIds = new Set();
|
|
2258
|
+
if (deferredReconciliationOperationId !== undefined) {
|
|
2259
|
+
operationIds.add(deferredReconciliationOperationId);
|
|
2260
|
+
}
|
|
2261
|
+
if (retainedEffectReconciliationRequired) {
|
|
2262
|
+
const checkpointLength = retainedEffectReconciliation?.checkpoint
|
|
2263
|
+
.boundaries.length ?? 0;
|
|
2264
|
+
for (const boundary of current.boundaries.slice(checkpointLength)) {
|
|
2265
|
+
if (boundary.physicalReceipt?.classification === 'unchanged') {
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
boundaryIds.add(boundary.boundaryId);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
for (const boundaryId of [...boundaryIds]) {
|
|
2272
|
+
const boundary = current.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
2273
|
+
if (boundary?.logicalOperationId !== undefined &&
|
|
2274
|
+
current.logicalOperations.some(({ operationId }) => operationId === boundary.logicalOperationId)) {
|
|
2275
|
+
operationIds.add(boundary.logicalOperationId);
|
|
2276
|
+
for (const memberId of current.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId).boundaryIds) {
|
|
2277
|
+
boundaryIds.delete(memberId);
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const ordered = [
|
|
2282
|
+
...[...boundaryIds].map((boundaryId) => ({
|
|
2283
|
+
order: current.boundaries.find((candidate) => candidate.boundaryId === boundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
2284
|
+
value: { kind: 'boundary', boundaryId },
|
|
2285
|
+
})),
|
|
2286
|
+
...[...operationIds].map((operationId) => {
|
|
2287
|
+
const operation = current.logicalOperations.find((candidate) => candidate.operationId === operationId);
|
|
2288
|
+
const firstBoundaryId = operation?.boundaryIds[0];
|
|
2289
|
+
return {
|
|
2290
|
+
order: current.boundaries.find(({ boundaryId }) => boundaryId === firstBoundaryId)?.sequence ?? Number.MAX_SAFE_INTEGER,
|
|
2291
|
+
value: { kind: 'logical-operation', operationId },
|
|
2292
|
+
};
|
|
2293
|
+
}),
|
|
2294
|
+
].sort((left, right) => left.order - right.order);
|
|
2295
|
+
return deepFreeze(snapshotJsonValue(ordered.map(({ value }) => value), `${label} unresolved effect envelope identities`));
|
|
2296
|
+
}
|
|
2297
|
+
function closeAfterIndeterminateDeferredSettlement(operationId, cause) {
|
|
2298
|
+
try {
|
|
2299
|
+
effectLedgerMirror = currentEffectLedger();
|
|
2300
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
2301
|
+
syncDeferredReconciliationOverlay();
|
|
2302
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
2303
|
+
}
|
|
2304
|
+
catch {
|
|
2305
|
+
// The current host mirror is itself unavailable. The closure below
|
|
2306
|
+
// keeps every public state surface shut until a fresh host recovers
|
|
2307
|
+
// the write-ahead record and constructs a replacement runtime.
|
|
2308
|
+
}
|
|
2309
|
+
expectedBoundPendingQuestion = undefined;
|
|
2310
|
+
deferredSettlementClosure ??= new Error(`${label} deferred settlement is indeterminate; recover the host effect ledger before continuing`, { cause });
|
|
2311
|
+
if (operationId !== undefined &&
|
|
2312
|
+
deferredReconciliationOperationId === undefined) {
|
|
2313
|
+
deferredReconciliationOperationId = operationId;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
function assertDeferredSettlementOpen(method) {
|
|
2317
|
+
if (deferredSettlementClosure !== undefined) {
|
|
2318
|
+
throw new Error(`createPlaybookRuntime.${method}: deferred settlement recovery is required`, { cause: deferredSettlementClosure });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
function currentBoundDeferredOperation(pending) {
|
|
2322
|
+
const projected = {
|
|
2323
|
+
questionId: pending.questionId,
|
|
2324
|
+
asker: pending.asker,
|
|
2325
|
+
question: pending.question,
|
|
2326
|
+
sourceItem: pending.sourceItem,
|
|
2327
|
+
};
|
|
2328
|
+
const matches = runtimeLogicalOperations().filter((operation) => operation.logicalReceipt === undefined &&
|
|
2329
|
+
operation.checkpoint !== undefined &&
|
|
2330
|
+
operation.pendingQuestion !== undefined &&
|
|
2331
|
+
operation.playerContinuation !== undefined &&
|
|
2332
|
+
!operation.checkpointRestorationEligible &&
|
|
2333
|
+
isDeepStrictEqual(operation.pendingQuestion, projected));
|
|
2334
|
+
if (matches.length > 1) {
|
|
2335
|
+
throw new Error(`${label} effect ledger contains multiple operations for one pending question`);
|
|
2336
|
+
}
|
|
2337
|
+
return matches[0];
|
|
2338
|
+
}
|
|
2339
|
+
function continuationBoundarySeed(operation, turnId) {
|
|
2340
|
+
const latestBoundaryId = operation.boundaryIds.at(-1);
|
|
2341
|
+
const latestBoundary = effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === latestBoundaryId);
|
|
2342
|
+
if (latestBoundary === undefined) {
|
|
2343
|
+
throw new Error(`${label} deferred logical operation has no latest physical boundary`);
|
|
2344
|
+
}
|
|
2345
|
+
return {
|
|
2346
|
+
boundaryId: randomUUID(),
|
|
2347
|
+
runtimeSessionId: latestBoundary.runtimeSessionId,
|
|
2348
|
+
turnId,
|
|
2349
|
+
callId: `player-${++playerCallSequence}`,
|
|
2350
|
+
roleId: latestBoundary.roleId,
|
|
2351
|
+
sourceStateId: latestBoundary.sourceStateId,
|
|
2352
|
+
sourceOutcomeSchema: latestBoundary.sourceOutcomeSchema,
|
|
2353
|
+
dispositions: latestBoundary.dispositions,
|
|
2354
|
+
correctionBudget: { limit: 1, spent: false },
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
1456
2357
|
function bindSession(nextSession) {
|
|
1457
2358
|
const bound = snapshotPlaybookSession(nextSession);
|
|
1458
2359
|
if (bound.roleBindings === undefined)
|
|
@@ -1655,7 +2556,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1655
2556
|
const currentSession = requireSession();
|
|
1656
2557
|
const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1657
2558
|
return {
|
|
1658
|
-
schemaVersion:
|
|
2559
|
+
schemaVersion: 4,
|
|
1659
2560
|
sessionId: currentSession.sessionId,
|
|
1660
2561
|
playbookId: currentSession.playbookId,
|
|
1661
2562
|
rootSessionId: currentSession.rootSessionId,
|
|
@@ -1769,31 +2670,605 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1769
2670
|
throw error;
|
|
1770
2671
|
}
|
|
1771
2672
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2673
|
+
function governedBoundarySeed(input, roleId, callId, turnId) {
|
|
2674
|
+
const governed = outcomeAuthority?.governedPlayerStates[input.stateId];
|
|
2675
|
+
if (governed === undefined)
|
|
2676
|
+
return undefined;
|
|
2677
|
+
if (!Number.isSafeInteger(turnId) || turnId === undefined || turnId <= 0) {
|
|
2678
|
+
throw new Error(`${label} governed player call requires an active positive turn id`);
|
|
2679
|
+
}
|
|
2680
|
+
const dispositions = [
|
|
2681
|
+
...new Set(Object.values(governed).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
2682
|
+
];
|
|
2683
|
+
if (dispositions.length === 0) {
|
|
2684
|
+
throw new Error(`${label} governed player call has no repository disposition`);
|
|
2685
|
+
}
|
|
2686
|
+
return {
|
|
2687
|
+
boundaryId: randomUUID(),
|
|
2688
|
+
// An adopted runtime keeps one durable effect-owner identity across
|
|
2689
|
+
// every later target generation. New boundaries must join that same
|
|
2690
|
+
// lineage; otherwise a boundary started by an intermediate target is
|
|
2691
|
+
// no longer discoverable after the next adoption.
|
|
2692
|
+
runtimeSessionId: retainedEffectSourceSessionId ?? requireSession().sessionId,
|
|
2693
|
+
turnId,
|
|
2694
|
+
callId,
|
|
2695
|
+
roleId,
|
|
2696
|
+
sourceStateId: input.stateId,
|
|
2697
|
+
sourceOutcomeSchema: snapshotJsonValue(input.result, `${label} governed player source outcome schema`),
|
|
2698
|
+
dispositions,
|
|
2699
|
+
correctionBudget: { limit: 1, spent: false },
|
|
2700
|
+
};
|
|
2701
|
+
}
|
|
2702
|
+
function boundPendingQuestion(input, roleId, output) {
|
|
2703
|
+
if (output.guard !== 'needsBossReply' || typeof output.question !== 'string') {
|
|
2704
|
+
throw new TypeError(`${label} deferred outcome must carry one exact Boss question`);
|
|
2705
|
+
}
|
|
2706
|
+
return {
|
|
2707
|
+
questionId: input.stateId,
|
|
2708
|
+
resumeStateId: input.stateId,
|
|
2709
|
+
sourceItem: input.sourceItem,
|
|
2710
|
+
asker: { kind: 'role', roleId },
|
|
2711
|
+
question: output.question,
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
function detachedPlayerContinuation(roleId, playerId) {
|
|
2715
|
+
return snapshotJsonValue(selectPlayerResume(roleId, playerId), `${label} deferred player continuation`);
|
|
2716
|
+
}
|
|
2717
|
+
function completionEvidenceFor(input, roleId, playerId, signal, operationId) {
|
|
2718
|
+
return async (completion) => {
|
|
2719
|
+
const { operation } = completion;
|
|
2720
|
+
let evidence;
|
|
2721
|
+
if (operation.status !== 'fulfilled' ||
|
|
2722
|
+
operation.value.status !== 'ok' ||
|
|
2723
|
+
isEmptyFinalText(operation.value.finalText)) {
|
|
2724
|
+
evidence = operation.status === 'fulfilled' &&
|
|
2725
|
+
operation.value.status === 'ok' &&
|
|
2726
|
+
operation.value.finalText !== undefined
|
|
2727
|
+
? { finalText: operation.value.finalText }
|
|
2728
|
+
: {};
|
|
2729
|
+
}
|
|
2730
|
+
else {
|
|
2731
|
+
const finalText = operation.value.finalText;
|
|
2732
|
+
evidence = await reconcileGovernedCompletion(input, roleId, playerId, finalText, signal, operationId, completion);
|
|
2733
|
+
}
|
|
2734
|
+
rememberGovernedCompletionEvidence(completion.boundary.boundaryId, evidence);
|
|
2735
|
+
return evidence;
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
function rememberGovernedCompletionEvidence(boundaryId, evidence) {
|
|
2739
|
+
const previous = governedCompletionEvidenceByBoundaryId.get(boundaryId);
|
|
2740
|
+
governedCompletionEvidenceByBoundaryId.set(boundaryId, {
|
|
2741
|
+
...previous,
|
|
2742
|
+
boundaryEvidence: {
|
|
2743
|
+
...(Object.prototype.hasOwnProperty.call(evidence, 'finalText')
|
|
2744
|
+
? { finalText: evidence.finalText }
|
|
2745
|
+
: {}),
|
|
2746
|
+
...(Object.prototype.hasOwnProperty.call(evidence, 'semanticCandidate')
|
|
2747
|
+
? { semanticCandidate: evidence.semanticCandidate }
|
|
2748
|
+
: {}),
|
|
2749
|
+
},
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
function unresolvedGovernedSettlement(reason, error, signal = activeSignal) {
|
|
2753
|
+
if (error !== undefined && signal?.aborted && Object.is(error, signal.reason)) {
|
|
2754
|
+
return { status: 'unresolved', error };
|
|
2755
|
+
}
|
|
2756
|
+
const failure = error instanceof Error
|
|
2757
|
+
? error
|
|
2758
|
+
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
2759
|
+
return {
|
|
2760
|
+
status: 'unresolved',
|
|
2761
|
+
error: markFsmResultFailure(failure),
|
|
2762
|
+
};
|
|
2763
|
+
}
|
|
2764
|
+
async function spendSemanticCorrectionBudget(completedBoundary, receipt, finalText, semanticCandidate) {
|
|
2765
|
+
if (effectLedgerCapability === undefined)
|
|
2766
|
+
return undefined;
|
|
2767
|
+
const currentLedger = currentEffectLedger();
|
|
2768
|
+
const current = currentLedger.boundaries.find(({ boundaryId }) => boundaryId === completedBoundary.boundaryId);
|
|
2769
|
+
if (current === undefined ||
|
|
2770
|
+
current.correctionBudget.limit !== 1 ||
|
|
2771
|
+
current.correctionBudget.spent) {
|
|
2772
|
+
return undefined;
|
|
2773
|
+
}
|
|
2774
|
+
if (current.finalText !== undefined &&
|
|
2775
|
+
current.finalText !== finalText) {
|
|
2776
|
+
throw new TypeError(`${label} correction budget boundary conflicts with retained finalText`);
|
|
2777
|
+
}
|
|
2778
|
+
if (current.physicalReceipt !== undefined &&
|
|
2779
|
+
!isDeepStrictEqual(current.physicalReceipt, receipt)) {
|
|
2780
|
+
throw new TypeError(`${label} correction budget boundary conflicts with its repository receipt`);
|
|
2781
|
+
}
|
|
2782
|
+
if (semanticCandidate !== undefined &&
|
|
2783
|
+
current.semanticCandidate !== undefined &&
|
|
2784
|
+
!isDeepStrictEqual(current.semanticCandidate, semanticCandidate)) {
|
|
2785
|
+
throw new TypeError(`${label} correction budget boundary conflicts with its retained semantic candidate`);
|
|
2786
|
+
}
|
|
2787
|
+
const next = {
|
|
2788
|
+
...current,
|
|
2789
|
+
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
2790
|
+
physicalReceipt: receipt,
|
|
2791
|
+
finalText,
|
|
2792
|
+
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
2793
|
+
correctionBudget: { limit: 1, spent: true },
|
|
2794
|
+
};
|
|
2795
|
+
const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
|
|
2796
|
+
{
|
|
2797
|
+
kind: 'replace-boundaries',
|
|
2798
|
+
replacements: [{ expected: current, next }],
|
|
2799
|
+
},
|
|
2800
|
+
]), `${label} semantic correction budget acknowledgement`);
|
|
2801
|
+
effectLedgerMirror = acknowledged;
|
|
2802
|
+
refreshRetainedEffectReconciliation(acknowledged);
|
|
2803
|
+
syncDeferredReconciliationOverlay();
|
|
2804
|
+
refreshUnresolvedSemanticReconciliation(acknowledged);
|
|
2805
|
+
const spent = acknowledged.boundaries.find(({ boundaryId }) => boundaryId === completedBoundary.boundaryId);
|
|
2806
|
+
if (spent === undefined ||
|
|
2807
|
+
!isDeepStrictEqual(spent, next)) {
|
|
2808
|
+
throw new TypeError(`${label} semantic correction budget spend was not acknowledged exactly`);
|
|
2809
|
+
}
|
|
2810
|
+
return spent;
|
|
2811
|
+
}
|
|
2812
|
+
async function reconcileGovernedCompletion(input, roleId, playerId, finalText, signal, operationId, completion) {
|
|
2813
|
+
const outcomes = outcomeAuthority?.governedPlayerStates[input.stateId];
|
|
2814
|
+
if (outcomes === undefined) {
|
|
2815
|
+
throw new TypeError(`${label} governed semantic reconciliation has no authority for ${input.stateId}`);
|
|
2816
|
+
}
|
|
2817
|
+
if (completion.boundary.sourceStateId !== input.stateId ||
|
|
2818
|
+
!isDeepStrictEqual(completion.boundary.sourceOutcomeSchema, input.result)) {
|
|
2819
|
+
throw new TypeError(`${label} governed semantic reconciliation source schema changed`);
|
|
2820
|
+
}
|
|
2821
|
+
let raw;
|
|
2822
|
+
try {
|
|
2823
|
+
raw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes), signal);
|
|
2824
|
+
}
|
|
2825
|
+
catch (error) {
|
|
2826
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('judge transport failed', error, signal));
|
|
2827
|
+
return { finalText, unresolved: true };
|
|
2828
|
+
}
|
|
2829
|
+
let candidate;
|
|
2830
|
+
let retainedSemanticCandidate;
|
|
2831
|
+
const retainSemanticCandidate = (value) => {
|
|
2832
|
+
try {
|
|
2833
|
+
retainedSemanticCandidate = snapshotJsonValue(value, `${label} recoverable governed semantic candidate`);
|
|
2834
|
+
}
|
|
2835
|
+
catch {
|
|
2836
|
+
// A malformed or non-detachable reply supplies no durable
|
|
2837
|
+
// candidate; presentation and receipt evidence still survive.
|
|
2838
|
+
}
|
|
2839
|
+
};
|
|
2840
|
+
const unresolvedEvidence = () => ({
|
|
2841
|
+
finalText,
|
|
2842
|
+
...(retainedSemanticCandidate === undefined
|
|
2843
|
+
? {}
|
|
2844
|
+
: { semanticCandidate: retainedSemanticCandidate }),
|
|
2845
|
+
unresolved: true,
|
|
2846
|
+
});
|
|
2847
|
+
let reconciliation;
|
|
2848
|
+
let structuralError;
|
|
2849
|
+
try {
|
|
2850
|
+
candidate = parseGovernedSemanticCandidate(raw);
|
|
2851
|
+
retainSemanticCandidate(candidate);
|
|
2852
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
2853
|
+
outcomes,
|
|
2854
|
+
semanticCandidate: candidate,
|
|
2855
|
+
finalText,
|
|
2856
|
+
receipt: completion.outcomeReceipt,
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
catch (error) {
|
|
2860
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
2861
|
+
throw error;
|
|
2862
|
+
}
|
|
2863
|
+
structuralError = error;
|
|
2864
|
+
}
|
|
2865
|
+
if (structuralError !== undefined) {
|
|
2866
|
+
let spent;
|
|
2867
|
+
try {
|
|
2868
|
+
spent = await spendSemanticCorrectionBudget(completion.boundary, completion.receipt, finalText, retainedSemanticCandidate);
|
|
2869
|
+
}
|
|
2870
|
+
catch (error) {
|
|
2871
|
+
// A failed or indeterminate spend cannot authorize another judge.
|
|
2872
|
+
// Let the repository coordinator quarantine its still-owned claim;
|
|
2873
|
+
// an acknowledged write remains durable and one-way on recovery.
|
|
2874
|
+
throw error;
|
|
2875
|
+
}
|
|
2876
|
+
if (spent === undefined) {
|
|
2877
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('semantic correction budget is unavailable'));
|
|
2878
|
+
return unresolvedEvidence();
|
|
2879
|
+
}
|
|
2880
|
+
if (signal.aborted) {
|
|
2881
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('semantic correction was aborted before its judge call', signal.reason, signal));
|
|
2882
|
+
return unresolvedEvidence();
|
|
2883
|
+
}
|
|
2884
|
+
let correctiveRaw;
|
|
2885
|
+
try {
|
|
2886
|
+
correctiveRaw = await boundary.callJudge('player-output-adjudication', input.stateId, buildGovernedJudgePrompt(input, finalText, outcomes, {
|
|
2887
|
+
reply: raw,
|
|
2888
|
+
error: structuralError.message,
|
|
2889
|
+
}), signal);
|
|
2890
|
+
}
|
|
2891
|
+
catch (error) {
|
|
2892
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('corrective judge failed', error, signal));
|
|
2893
|
+
return unresolvedEvidence();
|
|
2894
|
+
}
|
|
2895
|
+
try {
|
|
2896
|
+
candidate = parseGovernedSemanticCandidate(correctiveRaw);
|
|
2897
|
+
retainSemanticCandidate(candidate);
|
|
2898
|
+
reconciliation = reconcilePlaybookSemanticEvidence({
|
|
2899
|
+
outcomes,
|
|
2900
|
+
semanticCandidate: candidate,
|
|
2901
|
+
finalText,
|
|
2902
|
+
receipt: completion.outcomeReceipt,
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
catch (error) {
|
|
2906
|
+
if (!(error instanceof PlaybookSemanticCandidateStructureError)) {
|
|
2907
|
+
throw error;
|
|
2908
|
+
}
|
|
2909
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('corrective semantic candidate is invalid'));
|
|
2910
|
+
return unresolvedEvidence();
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
if (reconciliation === undefined) {
|
|
2914
|
+
throw new Error(`${label} semantic reconciliation produced no decision`);
|
|
2915
|
+
}
|
|
2916
|
+
const semanticCandidate = snapshotJsonValue(reconciliation.evidence.semanticCandidate, `${label} governed semantic candidate`);
|
|
2917
|
+
if (reconciliation.status === 'unresolved') {
|
|
2918
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement(reconciliation.reason));
|
|
2919
|
+
return { finalText, semanticCandidate, unresolved: true };
|
|
2920
|
+
}
|
|
2921
|
+
const output = reconciliation.output;
|
|
2922
|
+
validateBossReplyOutput(input, output, resumableStateIds);
|
|
2923
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, {
|
|
2924
|
+
status: 'resolved',
|
|
2925
|
+
output,
|
|
2926
|
+
});
|
|
2927
|
+
governedCompletionEvidenceByBoundaryId.set(completion.boundary.boundaryId, {
|
|
2928
|
+
boundaryEvidence: {},
|
|
2929
|
+
reconciliationStatus: reconciliation.status,
|
|
2930
|
+
output,
|
|
2931
|
+
});
|
|
2932
|
+
if (reconciliation.status !== 'deferred') {
|
|
2933
|
+
return { finalText, semanticCandidate };
|
|
2934
|
+
}
|
|
2935
|
+
const pending = boundPendingQuestion(input, roleId, output);
|
|
2936
|
+
const bindingId = operationId ?? randomUUID();
|
|
2937
|
+
expectedBoundPendingQuestion = pending;
|
|
2938
|
+
return {
|
|
2939
|
+
finalText,
|
|
2940
|
+
semanticCandidate,
|
|
2941
|
+
deferred: {
|
|
2942
|
+
operationId: bindingId,
|
|
2943
|
+
pendingQuestion: {
|
|
2944
|
+
questionId: pending.questionId,
|
|
2945
|
+
asker: pending.asker,
|
|
2946
|
+
question: pending.question,
|
|
2947
|
+
sourceItem: pending.sourceItem,
|
|
2948
|
+
},
|
|
2949
|
+
playerContinuation: detachedPlayerContinuation(roleId, playerId),
|
|
2950
|
+
},
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
async function deferredContinuationCompletionEvidence(completion) {
|
|
2954
|
+
const remember = (evidence) => {
|
|
2955
|
+
rememberGovernedCompletionEvidence(completion.boundary.boundaryId, evidence);
|
|
2956
|
+
return evidence;
|
|
2957
|
+
};
|
|
2958
|
+
const continuation = activeDeferredContinuation;
|
|
2959
|
+
if (continuation === undefined) {
|
|
2960
|
+
throw new Error(`${label} deferred continuation completed without active runtime context`);
|
|
2961
|
+
}
|
|
2962
|
+
const result = continuation.result;
|
|
2963
|
+
if (completion.operation.status !== 'fulfilled' ||
|
|
2964
|
+
completion.operation.value !== null ||
|
|
2965
|
+
result === undefined ||
|
|
2966
|
+
result.status !== 'ok' ||
|
|
2967
|
+
isEmptyFinalText(result.finalText)) {
|
|
2968
|
+
if (completion.outcomeReceipt.classification === 'unchanged' &&
|
|
2969
|
+
(continuation.callError !== undefined ||
|
|
2970
|
+
(result !== undefined && result.status !== 'ok'))) {
|
|
2971
|
+
return remember({});
|
|
2972
|
+
}
|
|
2973
|
+
governedSettlementsByBoundaryId.set(completion.boundary.boundaryId, unresolvedGovernedSettlement('deferred player result has no semantic evidence', continuation.callError));
|
|
2974
|
+
return remember({
|
|
2975
|
+
...(result?.status !== 'ok' || result.finalText === undefined
|
|
2976
|
+
? {}
|
|
2977
|
+
: { finalText: result.finalText }),
|
|
2978
|
+
unresolved: true,
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
const input = continuation.input;
|
|
2982
|
+
const roleId = continuation.roleId;
|
|
2983
|
+
const signal = continuation.signal;
|
|
2984
|
+
if (input === undefined || roleId === undefined || signal === undefined) {
|
|
2985
|
+
throw new Error(`${label} deferred continuation lost its bound player identity or signal`);
|
|
2986
|
+
}
|
|
2987
|
+
return remember(await reconcileGovernedCompletion(input, roleId, continuation.playerId, result.finalText, signal, continuation.operationId, completion));
|
|
2988
|
+
}
|
|
2989
|
+
function assertAcknowledgedGovernedEvidence(completed, settlement, ledger) {
|
|
2990
|
+
const expected = governedCompletionEvidenceByBoundaryId.get(completed.boundaryId);
|
|
2991
|
+
if (expected === undefined ||
|
|
2992
|
+
(Object.prototype.hasOwnProperty.call(expected.boundaryEvidence, 'finalText')
|
|
2993
|
+
? completed.finalText !== expected.boundaryEvidence.finalText
|
|
2994
|
+
: completed.finalText !== undefined) ||
|
|
2995
|
+
(Object.prototype.hasOwnProperty.call(expected.boundaryEvidence, 'semanticCandidate')
|
|
2996
|
+
? !isDeepStrictEqual(completed.semanticCandidate, expected.boundaryEvidence.semanticCandidate)
|
|
2997
|
+
: completed.semanticCandidate !== undefined)) {
|
|
2998
|
+
throw new TypeError(`${label} repository did not acknowledge the exact governed semantic evidence`);
|
|
2999
|
+
}
|
|
3000
|
+
if (settlement?.status !== 'resolved')
|
|
3001
|
+
return;
|
|
3002
|
+
const persisted = persistedBoundaryReconciliation(completed, ledger);
|
|
3003
|
+
if (persisted === undefined ||
|
|
3004
|
+
persisted.historicalDeferred ||
|
|
3005
|
+
persisted.reconciliation.status !== expected.reconciliationStatus ||
|
|
3006
|
+
!isDeepStrictEqual(persisted.reconciliation.output, expected.output) ||
|
|
3007
|
+
!isDeepStrictEqual(expected.output, settlement.output)) {
|
|
3008
|
+
throw new TypeError(`${label} repository did not acknowledge the exact governed semantic evidence`);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
function recordActiveGovernedAttempt(boundary) {
|
|
3012
|
+
if (activeGovernedAttemptId !== undefined &&
|
|
3013
|
+
activeGovernedAttemptId !== boundary.attemptId) {
|
|
3014
|
+
throw new Error(`${label} governed calls in one runtime boundary used different host attempt ids`);
|
|
3015
|
+
}
|
|
3016
|
+
activeGovernedAttemptId = boundary.attemptId;
|
|
3017
|
+
}
|
|
3018
|
+
function refreshGovernedBoundaryStart(boundaryId) {
|
|
3019
|
+
try {
|
|
3020
|
+
const current = currentEffectLedger();
|
|
3021
|
+
const boundary = current.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
3022
|
+
if (boundary === undefined)
|
|
3023
|
+
return;
|
|
3024
|
+
effectLedgerMirror = current;
|
|
3025
|
+
refreshRetainedEffectReconciliation(current);
|
|
3026
|
+
recordActiveGovernedAttempt(boundary);
|
|
3027
|
+
}
|
|
3028
|
+
catch {
|
|
3029
|
+
// Preserve the repository failure. A mirror that cannot be read or
|
|
3030
|
+
// validated supplies no evidence authorizing replay.
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function acknowledgeGovernedPlayerResult(value, boundaryId, source = 'runExclusive') {
|
|
3034
|
+
if (!isPlainObject(value) || !isPlainObject(value.operation)) {
|
|
3035
|
+
throw new TypeError(`${label} repository ${source} returned an invalid settlement`);
|
|
3036
|
+
}
|
|
3037
|
+
const ledger = assertPlaybookEffectLedger(value.effectLedger, `${label} repository ${source} effect ledger`);
|
|
3038
|
+
const completed = ledger.boundaries.find((candidate) => candidate.boundaryId === boundaryId);
|
|
3039
|
+
if (completed === undefined ||
|
|
3040
|
+
completed.physicalReceipt === undefined ||
|
|
3041
|
+
!isDeepStrictEqual(completed.physicalReceipt, value.receipt)) {
|
|
3042
|
+
throw new TypeError(`${label} repository ${source} did not acknowledge its completed boundary`);
|
|
3043
|
+
}
|
|
3044
|
+
effectLedgerMirror = ledger;
|
|
3045
|
+
refreshRetainedEffectReconciliation(ledger);
|
|
3046
|
+
syncDeferredReconciliationOverlay();
|
|
3047
|
+
refreshUnresolvedSemanticReconciliation(ledger);
|
|
3048
|
+
recordActiveGovernedAttempt(completed);
|
|
3049
|
+
if (value.operation.status === 'rejected') {
|
|
3050
|
+
if (!Object.prototype.hasOwnProperty.call(value.operation, 'reason')) {
|
|
3051
|
+
throw new TypeError(`${label} repository ${source} rejection omitted its reason`);
|
|
3052
|
+
}
|
|
3053
|
+
throw value.operation.reason;
|
|
3054
|
+
}
|
|
3055
|
+
if (value.operation.status !== 'fulfilled' ||
|
|
3056
|
+
!Object.prototype.hasOwnProperty.call(value.operation, 'value')) {
|
|
3057
|
+
throw new TypeError(`${label} repository ${source} returned an invalid operation settlement`);
|
|
3058
|
+
}
|
|
3059
|
+
const result = validatePlayerResult(value.operation.value);
|
|
3060
|
+
playerBoundaryReceipts.set(result, {
|
|
3061
|
+
boundaryId: completed.boundaryId,
|
|
3062
|
+
attemptId: completed.attemptId,
|
|
3063
|
+
});
|
|
3064
|
+
let governedSettlement = governedSettlementsByBoundaryId.get(boundaryId);
|
|
3065
|
+
if (governedSettlement === undefined &&
|
|
3066
|
+
result.status === 'ok' &&
|
|
3067
|
+
!isEmptyFinalText(result.finalText)) {
|
|
3068
|
+
governedSettlement = unresolvedGovernedSettlement('host omitted governed semantic settlement');
|
|
3069
|
+
}
|
|
3070
|
+
assertAcknowledgedGovernedEvidence(completed, governedSettlement, ledger);
|
|
3071
|
+
governedCompletionEvidenceByBoundaryId.delete(boundaryId);
|
|
3072
|
+
let governedOutput = governedSettlement?.status === 'resolved'
|
|
3073
|
+
? governedSettlement.output
|
|
3074
|
+
: undefined;
|
|
3075
|
+
const governedDisposition = governedOutput === undefined
|
|
3076
|
+
? undefined
|
|
3077
|
+
: governedOutcomesForBoundary(completed)?.[governedOutput.guard]
|
|
3078
|
+
?.repositoryDisposition;
|
|
3079
|
+
if (source === 'runExclusive' && governedDisposition === 'deferred') {
|
|
3080
|
+
if (value.deferredStatus !== 'bound' &&
|
|
3081
|
+
value.deferredStatus !== 'unresolved') {
|
|
3082
|
+
throw new TypeError(`${label} deferred settlement omitted its durable binding status`);
|
|
3083
|
+
}
|
|
3084
|
+
const operationId = completed.logicalOperationId;
|
|
3085
|
+
if (operationId === undefined) {
|
|
3086
|
+
throw new TypeError(`${label} deferred settlement omitted its logical operation`);
|
|
3087
|
+
}
|
|
3088
|
+
if (value.deferredStatus === 'bound') {
|
|
3089
|
+
if (expectedBoundPendingQuestion === undefined ||
|
|
3090
|
+
currentBoundDeferredOperation(expectedBoundPendingQuestion)
|
|
3091
|
+
?.operationId !== operationId) {
|
|
3092
|
+
throw new TypeError(`${label} deferred settlement did not acknowledge its exact bound question`);
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
else {
|
|
3096
|
+
if (deferredReconciliationOperationId !== operationId) {
|
|
3097
|
+
throw new TypeError(`${label} unresolved deferred settlement is not structurally unresolved`);
|
|
3098
|
+
}
|
|
3099
|
+
expectedBoundPendingQuestion = undefined;
|
|
3100
|
+
governedSettlement = unresolvedGovernedSettlement('deferred question did not receive an eligible durable binding');
|
|
3101
|
+
governedOutput = undefined;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
else if (source === 'runExclusive' && value.deferredStatus !== undefined) {
|
|
3105
|
+
throw new TypeError(`${label} non-deferred settlement returned a deferred binding status`);
|
|
3106
|
+
}
|
|
3107
|
+
if (governedSettlement !== undefined) {
|
|
3108
|
+
governedSettlementsByBoundaryId.delete(boundaryId);
|
|
3109
|
+
governedPlayerSettlements.set(result, governedSettlement);
|
|
3110
|
+
if (governedSettlement.status === 'unresolved') {
|
|
3111
|
+
unresolvedSemanticBoundaryIds.add(boundaryId);
|
|
3112
|
+
}
|
|
3113
|
+
else {
|
|
3114
|
+
unresolvedSemanticBoundaryIds.delete(boundaryId);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
return result;
|
|
3118
|
+
}
|
|
3119
|
+
function acknowledgedBoundaryIsUnchanged(result) {
|
|
3120
|
+
if (outcomeAuthority === undefined)
|
|
3121
|
+
return true;
|
|
3122
|
+
const identity = playerBoundaryReceipts.get(result);
|
|
3123
|
+
if (identity === undefined)
|
|
3124
|
+
return false;
|
|
3125
|
+
const boundary = effectLedgerMirror.boundaries.find((candidate) => candidate.boundaryId === identity.boundaryId);
|
|
3126
|
+
return (boundary?.attemptId === identity.attemptId &&
|
|
3127
|
+
boundary.physicalReceipt?.classification === 'unchanged');
|
|
3128
|
+
}
|
|
3129
|
+
function failedAttemptMatchesCurrentLedger(current) {
|
|
3130
|
+
const boundaryPrefix = failedEffectBoundaryPrefix;
|
|
3131
|
+
if (failedGovernedAttemptUnknown ||
|
|
3132
|
+
boundaryPrefix === undefined) {
|
|
3133
|
+
return false;
|
|
3134
|
+
}
|
|
3135
|
+
const causalBoundaries = current.boundaries.filter(({ sequence }) => sequence > boundaryPrefix);
|
|
3136
|
+
const matches = failedGovernedAttemptId === undefined
|
|
3137
|
+
? causalBoundaries.length === 0
|
|
3138
|
+
: causalBoundaries.length > 0 &&
|
|
3139
|
+
causalBoundaries.every(({ attemptId }) => attemptId === failedGovernedAttemptId);
|
|
3140
|
+
if (!matches)
|
|
3141
|
+
failedGovernedAttemptUnknown = true;
|
|
3142
|
+
return matches;
|
|
3143
|
+
}
|
|
3144
|
+
function failedAttemptAllowsReplay() {
|
|
3145
|
+
if (!hasGovernedPlayerStates)
|
|
3146
|
+
return true;
|
|
3147
|
+
if (unresolvedSemanticBoundaryIds.size > 0)
|
|
3148
|
+
return false;
|
|
3149
|
+
let current;
|
|
3150
|
+
try {
|
|
3151
|
+
current = currentEffectLedger();
|
|
3152
|
+
effectLedgerMirror = current;
|
|
3153
|
+
refreshRetainedEffectReconciliation(current);
|
|
3154
|
+
refreshUnresolvedSemanticReconciliation(current);
|
|
3155
|
+
}
|
|
3156
|
+
catch {
|
|
3157
|
+
failedGovernedAttemptUnknown = true;
|
|
3158
|
+
return false;
|
|
3159
|
+
}
|
|
3160
|
+
if (unresolvedSemanticBoundaryIds.size > 0)
|
|
3161
|
+
return false;
|
|
3162
|
+
if (!failedAttemptMatchesCurrentLedger(current))
|
|
3163
|
+
return false;
|
|
3164
|
+
if (failedGovernedAttemptId === undefined)
|
|
3165
|
+
return true;
|
|
3166
|
+
const boundaries = current.boundaries.filter(({ attemptId }) => attemptId === failedGovernedAttemptId);
|
|
3167
|
+
return (boundaries.length > 0 &&
|
|
3168
|
+
boundaries.every(({ physicalReceipt }) => physicalReceipt?.classification === 'unchanged'));
|
|
3169
|
+
}
|
|
3170
|
+
function captureEffectLedgerPrefixSequence() {
|
|
3171
|
+
if (!hasGovernedPlayerStates)
|
|
3172
|
+
return undefined;
|
|
3173
|
+
try {
|
|
3174
|
+
const current = currentEffectLedger();
|
|
3175
|
+
effectLedgerMirror = current;
|
|
3176
|
+
refreshRetainedEffectReconciliation(current);
|
|
3177
|
+
return current.boundaries.at(-1)?.sequence ?? 0;
|
|
3178
|
+
}
|
|
3179
|
+
catch {
|
|
3180
|
+
return undefined;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function bindAutomaticReplayBoundary(prefixSequence) {
|
|
3184
|
+
activeGovernedBoundarySeen = false;
|
|
3185
|
+
activeGovernedAttemptId = undefined;
|
|
3186
|
+
activeEffectLedgerPrefixSequence = prefixSequence;
|
|
3187
|
+
}
|
|
3188
|
+
function beginAutomaticReplayBoundary() {
|
|
3189
|
+
bindAutomaticReplayBoundary(captureEffectLedgerPrefixSequence());
|
|
3190
|
+
}
|
|
3191
|
+
function latchFailedGovernedAttempt() {
|
|
3192
|
+
if (!hasGovernedPlayerStates) {
|
|
3193
|
+
failedGovernedAttemptUnknown = false;
|
|
3194
|
+
failedEffectBoundaryPrefix = undefined;
|
|
3195
|
+
failedGovernedAttemptId = undefined;
|
|
3196
|
+
return;
|
|
3197
|
+
}
|
|
3198
|
+
if (activeEffectLedgerPrefixSequence === undefined) {
|
|
3199
|
+
failedGovernedAttemptUnknown = true;
|
|
3200
|
+
failedEffectBoundaryPrefix = undefined;
|
|
3201
|
+
failedGovernedAttemptId = undefined;
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
let current;
|
|
3205
|
+
try {
|
|
3206
|
+
current = currentEffectLedger();
|
|
3207
|
+
effectLedgerMirror = current;
|
|
3208
|
+
refreshRetainedEffectReconciliation(current);
|
|
3209
|
+
}
|
|
3210
|
+
catch {
|
|
3211
|
+
failedGovernedAttemptUnknown = true;
|
|
3212
|
+
failedEffectBoundaryPrefix = undefined;
|
|
3213
|
+
failedGovernedAttemptId = undefined;
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
const attemptIds = new Set(current.boundaries
|
|
3217
|
+
.filter(({ sequence }) => sequence > activeEffectLedgerPrefixSequence)
|
|
3218
|
+
.map(({ attemptId }) => attemptId));
|
|
3219
|
+
if (activeGovernedAttemptId !== undefined) {
|
|
3220
|
+
attemptIds.add(activeGovernedAttemptId);
|
|
3221
|
+
}
|
|
3222
|
+
if (attemptIds.size > 1) {
|
|
3223
|
+
failedGovernedAttemptUnknown = true;
|
|
3224
|
+
failedEffectBoundaryPrefix = undefined;
|
|
3225
|
+
failedGovernedAttemptId = undefined;
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
failedGovernedAttemptUnknown =
|
|
3229
|
+
attemptIds.size === 0 && activeGovernedBoundarySeen;
|
|
3230
|
+
failedEffectBoundaryPrefix = failedGovernedAttemptUnknown
|
|
3231
|
+
? undefined
|
|
3232
|
+
: activeEffectLedgerPrefixSequence;
|
|
3233
|
+
failedGovernedAttemptId = attemptIds.values().next().value;
|
|
3234
|
+
}
|
|
3235
|
+
const boundary = {
|
|
3236
|
+
async callPlayer(input, roleId, prompt, signal) {
|
|
3237
|
+
// State-entry telemetry/status must precede the call they describe.
|
|
3238
|
+
await drainEmissions();
|
|
3239
|
+
signal.throwIfAborted();
|
|
3240
|
+
const deferredContinuation = activeDeferredContinuation;
|
|
3241
|
+
const reconstructed = takeReconstructedGovernedPlayerResult(input, roleId);
|
|
3242
|
+
if (reconstructed !== undefined)
|
|
3243
|
+
return reconstructed;
|
|
3244
|
+
if (deferredContinuation === undefined &&
|
|
3245
|
+
hasUnresolvedReconciliation()) {
|
|
3246
|
+
throw markFsmResultFailure(new Error(`${label} governed semantic reconciliation remains unresolved`));
|
|
3247
|
+
}
|
|
3248
|
+
const turnId = activeTurnId;
|
|
3249
|
+
const stateId = input.stateId;
|
|
3250
|
+
const playerId = resolvedPlayerId(roleId);
|
|
3251
|
+
let selectedResume;
|
|
3252
|
+
try {
|
|
3253
|
+
signal.throwIfAborted();
|
|
3254
|
+
selectedResume =
|
|
3255
|
+
deferredContinuation?.playerContinuation ??
|
|
3256
|
+
selectPlayerResume(roleId, playerId);
|
|
3257
|
+
}
|
|
3258
|
+
catch (error) {
|
|
3259
|
+
if (!isAbortFailure(error, signal))
|
|
3260
|
+
controlPlaneError ??= error;
|
|
3261
|
+
throw error;
|
|
3262
|
+
}
|
|
3263
|
+
const callId = deferredContinuation?.effectBoundary.callId ??
|
|
3264
|
+
`player-${++playerCallSequence}`;
|
|
3265
|
+
const callIdentity = (resume) => ({
|
|
3266
|
+
...stateIdentity(stateId),
|
|
3267
|
+
sourceItem: input.sourceItem,
|
|
3268
|
+
roleId,
|
|
1794
3269
|
...(playerId === undefined ? {} : { playerId }),
|
|
1795
3270
|
resume,
|
|
1796
|
-
};
|
|
3271
|
+
});
|
|
1797
3272
|
const position = {
|
|
1798
3273
|
...(turnId !== undefined ? { turnId } : {}),
|
|
1799
3274
|
callId,
|
|
@@ -1801,92 +3276,182 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1801
3276
|
const playerKey = continuationKey(roleId, playerId);
|
|
1802
3277
|
if (activePlayerKeys.has(playerKey)) {
|
|
1803
3278
|
const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
|
|
1804
|
-
await emitCallStarted('player.call.started', 'player.call.finished', { ...
|
|
1805
|
-
await emitTrace('player.call.finished', {
|
|
3279
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, signal);
|
|
3280
|
+
await emitTrace('player.call.finished', {
|
|
3281
|
+
...callIdentity(selectedResume),
|
|
3282
|
+
status: 'error',
|
|
3283
|
+
error: normalizeError(error),
|
|
3284
|
+
}, position);
|
|
1806
3285
|
throw error;
|
|
1807
3286
|
}
|
|
1808
3287
|
activePlayerKeys.add(playerKey);
|
|
1809
3288
|
try {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
// (e.g. fired from the trace sink itself); the host call must
|
|
1815
|
-
// never start after abort, so settle the already-started pair
|
|
1816
|
-
// as `aborted` through the catch below.
|
|
1817
|
-
signal.throwIfAborted();
|
|
1818
|
-
rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
|
|
1819
|
-
// A host promise is not required to honor cancellation. Do not let
|
|
1820
|
-
// a late result mutate continuity or publish a successful finish.
|
|
1821
|
-
signal.throwIfAborted();
|
|
1822
|
-
}
|
|
1823
|
-
catch (error) {
|
|
1824
|
-
if (!isAbortFailure(error, signal))
|
|
1825
|
-
controlPlaneError ??= error;
|
|
3289
|
+
const runTracedPlayerCall = async (resume = selectedResume) => {
|
|
3290
|
+
const identity = callIdentity(resume);
|
|
3291
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt }, position, signal);
|
|
3292
|
+
let rawResult;
|
|
1826
3293
|
try {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
3294
|
+
// An abort may land while the awaited started emission drains
|
|
3295
|
+
// (e.g. fired from the trace sink itself); the host call must
|
|
3296
|
+
// never start after abort, so settle the already-started pair
|
|
3297
|
+
// as `aborted` through the catch below.
|
|
3298
|
+
signal.throwIfAborted();
|
|
3299
|
+
rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
|
|
3300
|
+
// A host promise is not required to honor cancellation. Do not
|
|
3301
|
+
// let a late result mutate continuity or publish a successful
|
|
3302
|
+
// finish.
|
|
3303
|
+
signal.throwIfAborted();
|
|
1832
3304
|
}
|
|
1833
|
-
catch {
|
|
1834
|
-
|
|
3305
|
+
catch (error) {
|
|
3306
|
+
if (!isAbortFailure(error, signal))
|
|
3307
|
+
controlPlaneError ??= error;
|
|
3308
|
+
try {
|
|
3309
|
+
await emitTrace('player.call.finished', {
|
|
3310
|
+
...identity,
|
|
3311
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
3312
|
+
error: normalizeError(error),
|
|
3313
|
+
}, position);
|
|
3314
|
+
}
|
|
3315
|
+
catch {
|
|
3316
|
+
// The original non-abort port rejection remains authoritative.
|
|
3317
|
+
}
|
|
3318
|
+
// A thrown port call carries no authoritative result, so the
|
|
3319
|
+
// prior token remains available for a later explicit resume.
|
|
3320
|
+
throw error;
|
|
1835
3321
|
}
|
|
1836
|
-
|
|
1837
|
-
// prior token remains available for a later explicit resume.
|
|
1838
|
-
throw error;
|
|
1839
|
-
}
|
|
1840
|
-
let result;
|
|
1841
|
-
try {
|
|
1842
|
-
result = validatePlayerResult(rawResult);
|
|
1843
|
-
}
|
|
1844
|
-
catch (error) {
|
|
1845
|
-
if (!isAbortFailure(error, signal))
|
|
1846
|
-
controlPlaneError ??= error;
|
|
3322
|
+
let result;
|
|
1847
3323
|
try {
|
|
1848
|
-
|
|
3324
|
+
result = validatePlayerResult(rawResult);
|
|
1849
3325
|
}
|
|
1850
|
-
catch {
|
|
1851
|
-
|
|
3326
|
+
catch (error) {
|
|
3327
|
+
if (!isAbortFailure(error, signal))
|
|
3328
|
+
controlPlaneError ??= error;
|
|
3329
|
+
try {
|
|
3330
|
+
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
3331
|
+
}
|
|
3332
|
+
catch {
|
|
3333
|
+
// The malformed host result remains authoritative.
|
|
3334
|
+
}
|
|
3335
|
+
throw error;
|
|
1852
3336
|
}
|
|
1853
|
-
|
|
3337
|
+
try {
|
|
3338
|
+
updatePlayerResume(roleId, playerId, result);
|
|
3339
|
+
}
|
|
3340
|
+
catch (error) {
|
|
3341
|
+
if (!isAbortFailure(error, signal))
|
|
3342
|
+
controlPlaneError ??= error;
|
|
3343
|
+
try {
|
|
3344
|
+
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
3345
|
+
}
|
|
3346
|
+
catch {
|
|
3347
|
+
// The continuation-store failure remains authoritative.
|
|
3348
|
+
}
|
|
3349
|
+
throw error;
|
|
3350
|
+
}
|
|
3351
|
+
await emitTrace('player.call.finished', {
|
|
3352
|
+
...identity,
|
|
3353
|
+
status: result.status,
|
|
3354
|
+
...(result.finalText !== undefined
|
|
3355
|
+
? { finalText: result.finalText }
|
|
3356
|
+
: {}),
|
|
3357
|
+
...(result.error !== undefined
|
|
3358
|
+
? { error: normalizeError(result.error) }
|
|
3359
|
+
: {}),
|
|
3360
|
+
...(result.resumeToken !== undefined
|
|
3361
|
+
? { resumeToken: result.resumeToken }
|
|
3362
|
+
: {}),
|
|
3363
|
+
}, position);
|
|
3364
|
+
return result;
|
|
3365
|
+
};
|
|
3366
|
+
const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
|
|
3367
|
+
if (deferredContinuation !== undefined) {
|
|
3368
|
+
if (effectBoundary === undefined ||
|
|
3369
|
+
effectBoundary.runtimeSessionId !==
|
|
3370
|
+
deferredContinuation.effectBoundary.runtimeSessionId ||
|
|
3371
|
+
effectBoundary.turnId !== deferredContinuation.effectBoundary.turnId ||
|
|
3372
|
+
effectBoundary.callId !== deferredContinuation.effectBoundary.callId ||
|
|
3373
|
+
effectBoundary.roleId !== deferredContinuation.effectBoundary.roleId ||
|
|
3374
|
+
effectBoundary.sourceStateId !==
|
|
3375
|
+
deferredContinuation.effectBoundary.sourceStateId ||
|
|
3376
|
+
!isDeepStrictEqual(effectBoundary.sourceOutcomeSchema, deferredContinuation.effectBoundary.sourceOutcomeSchema) ||
|
|
3377
|
+
!isDeepStrictEqual(effectBoundary.dispositions, deferredContinuation.effectBoundary.dispositions)) {
|
|
3378
|
+
throw new TypeError(`${label} deferred continuation did not invoke its bound player boundary`);
|
|
3379
|
+
}
|
|
3380
|
+
activeGovernedBoundarySeen = true;
|
|
3381
|
+
deferredContinuation.input = input;
|
|
3382
|
+
deferredContinuation.roleId = roleId;
|
|
3383
|
+
deferredContinuation.playerId = playerId;
|
|
3384
|
+
deferredContinuation.signal = signal;
|
|
3385
|
+
try {
|
|
3386
|
+
deferredContinuation.result = await runTracedPlayerCall(selectedResume);
|
|
3387
|
+
}
|
|
3388
|
+
catch (error) {
|
|
3389
|
+
deferredContinuation.callError = error;
|
|
3390
|
+
}
|
|
3391
|
+
finally {
|
|
3392
|
+
deferredContinuation.rawPlayerSettled.resolve();
|
|
3393
|
+
}
|
|
3394
|
+
return await deferredContinuation.delivery.promise;
|
|
1854
3395
|
}
|
|
3396
|
+
// Await inside this try so its finally retains the player-key
|
|
3397
|
+
// exclusion until the host operation actually settles.
|
|
3398
|
+
if (effectBoundary === undefined)
|
|
3399
|
+
return await runTracedPlayerCall();
|
|
3400
|
+
if (repositoryCapability === undefined) {
|
|
3401
|
+
throw new Error(`${label} governed player call requires repository.runExclusive`);
|
|
3402
|
+
}
|
|
3403
|
+
activeGovernedBoundarySeen = true;
|
|
1855
3404
|
try {
|
|
1856
|
-
|
|
3405
|
+
const exclusive = await repositoryCapability.runExclusive({
|
|
3406
|
+
signal,
|
|
3407
|
+
effectBoundary,
|
|
3408
|
+
operation: () => runTracedPlayerCall(),
|
|
3409
|
+
completeEffectBoundary: completionEvidenceFor(input, roleId, playerId, signal, undefined),
|
|
3410
|
+
});
|
|
3411
|
+
return acknowledgeGovernedPlayerResult(exclusive, effectBoundary.boundaryId);
|
|
1857
3412
|
}
|
|
1858
3413
|
catch (error) {
|
|
3414
|
+
if (expectedBoundPendingQuestion !== undefined) {
|
|
3415
|
+
closeAfterIndeterminateDeferredSettlement(undefined, error);
|
|
3416
|
+
}
|
|
3417
|
+
expectedBoundPendingQuestion = undefined;
|
|
3418
|
+
governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
|
|
3419
|
+
governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
|
|
3420
|
+
refreshGovernedBoundaryStart(effectBoundary.boundaryId);
|
|
1859
3421
|
if (!isAbortFailure(error, signal))
|
|
1860
3422
|
controlPlaneError ??= error;
|
|
1861
|
-
try {
|
|
1862
|
-
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1863
|
-
}
|
|
1864
|
-
catch {
|
|
1865
|
-
// The continuation-store failure remains authoritative.
|
|
1866
|
-
}
|
|
1867
3423
|
throw error;
|
|
1868
3424
|
}
|
|
1869
|
-
await emitTrace('player.call.finished', {
|
|
1870
|
-
...identity,
|
|
1871
|
-
status: result.status,
|
|
1872
|
-
...(result.finalText !== undefined
|
|
1873
|
-
? { finalText: result.finalText }
|
|
1874
|
-
: {}),
|
|
1875
|
-
...(result.error !== undefined
|
|
1876
|
-
? { error: normalizeError(result.error) }
|
|
1877
|
-
: {}),
|
|
1878
|
-
...(result.resumeToken !== undefined
|
|
1879
|
-
? { resumeToken: result.resumeToken }
|
|
1880
|
-
: {}),
|
|
1881
|
-
}, position);
|
|
1882
|
-
return result;
|
|
1883
3425
|
}
|
|
1884
3426
|
finally {
|
|
1885
3427
|
activePlayerKeys.delete(playerKey);
|
|
1886
3428
|
}
|
|
1887
3429
|
},
|
|
3430
|
+
takeGovernedPlayerOutput(result) {
|
|
3431
|
+
const settlement = governedPlayerSettlements.get(result);
|
|
3432
|
+
if (settlement !== undefined)
|
|
3433
|
+
governedPlayerSettlements.delete(result);
|
|
3434
|
+
return settlement;
|
|
3435
|
+
},
|
|
3436
|
+
recordGovernedPlayerOutput(result, output) {
|
|
3437
|
+
const reconstructed = reconstructedGovernedResults.get(result);
|
|
3438
|
+
if (reconstructed === undefined)
|
|
3439
|
+
return;
|
|
3440
|
+
reconstructedGovernedResults.delete(result);
|
|
3441
|
+
const persisted = persistedBoundaryReconciliation(reconstructed, effectLedgerMirror);
|
|
3442
|
+
if (persisted === undefined ||
|
|
3443
|
+
persisted.reconciliation.status !== 'resolved' ||
|
|
3444
|
+
!isDeepStrictEqual(persisted.reconciliation.output, output)) {
|
|
3445
|
+
unresolvedSemanticBoundaryIds.add(reconstructed.boundaryId);
|
|
3446
|
+
throw markFsmResultFailure(new Error(`${label} reconstructed governed output changed before FSM acceptance`));
|
|
3447
|
+
}
|
|
3448
|
+
reconstructedAcceptancePending = reconstructed;
|
|
3449
|
+
},
|
|
1888
3450
|
async callJudge(purpose, stateId, prompt, signal) {
|
|
1889
3451
|
return judgeQueue.add(async () => {
|
|
3452
|
+
const governedSemanticJudge = purpose === 'player-output-adjudication' &&
|
|
3453
|
+
stateId !== undefined &&
|
|
3454
|
+
outcomeAuthority.governedPlayerStates[stateId] !== undefined;
|
|
1890
3455
|
signal.throwIfAborted();
|
|
1891
3456
|
// A transition/status queued synchronously by XState must reach
|
|
1892
3457
|
// the host before the judge call that follows it.
|
|
@@ -1911,7 +3476,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1911
3476
|
signal.throwIfAborted();
|
|
1912
3477
|
}
|
|
1913
3478
|
catch (error) {
|
|
1914
|
-
if (!isAbortFailure(error, signal)) {
|
|
3479
|
+
if (!isAbortFailure(error, signal) && !governedSemanticJudge) {
|
|
1915
3480
|
controlPlaneError ??= error;
|
|
1916
3481
|
}
|
|
1917
3482
|
await emitTrace('judge.call.finished', {
|
|
@@ -1923,7 +3488,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1923
3488
|
}
|
|
1924
3489
|
if (typeof reply !== 'string') {
|
|
1925
3490
|
const error = new TypeError('judge reply must be a string');
|
|
1926
|
-
|
|
3491
|
+
if (!governedSemanticJudge)
|
|
3492
|
+
controlPlaneError ??= error;
|
|
1927
3493
|
await emitTrace('judge.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1928
3494
|
throw error;
|
|
1929
3495
|
}
|
|
@@ -2050,9 +3616,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2050
3616
|
function playerActor(ports) {
|
|
2051
3617
|
return createPlayerBridge({
|
|
2052
3618
|
resolveRoleId: requireRoleId,
|
|
3619
|
+
validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
|
|
2053
3620
|
composePlayerPrompt: composeBoundPlayerPrompt,
|
|
2054
3621
|
adjudication,
|
|
2055
3622
|
resumableStateIds,
|
|
3623
|
+
allowsCorrectiveReplay: acknowledgedBoundaryIsUnchanged,
|
|
2056
3624
|
}, ports, () => activeSignal, boundary, (error) => {
|
|
2057
3625
|
if (activeSignal === undefined || !isAbortFailure(error, activeSignal)) {
|
|
2058
3626
|
controlPlaneError ??= error;
|
|
@@ -2316,6 +3884,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2316
3884
|
callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
|
|
2317
3885
|
emitStarted: async (event, aborts) => {
|
|
2318
3886
|
playbookCallTurnIds.set(event.callId, activeTurnId);
|
|
3887
|
+
if (hasGovernedPlayerStates) {
|
|
3888
|
+
playbookCallEffectPrefixes.set(event.callId, activeEffectLedgerPrefixSequence);
|
|
3889
|
+
}
|
|
2319
3890
|
await emitTrace('playbook.call.started', {
|
|
2320
3891
|
stateId: event.stateId,
|
|
2321
3892
|
playbookId: event.playbookId,
|
|
@@ -2340,6 +3911,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2340
3911
|
}
|
|
2341
3912
|
finally {
|
|
2342
3913
|
playbookCallTurnIds.delete(event.callId);
|
|
3914
|
+
playbookCallEffectPrefixes.delete(event.callId);
|
|
2343
3915
|
}
|
|
2344
3916
|
},
|
|
2345
3917
|
drain: drainEmissions,
|
|
@@ -2378,7 +3950,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2378
3950
|
state,
|
|
2379
3951
|
};
|
|
2380
3952
|
const pendingBossQuestion = pendingBossQuestionForState(state, context);
|
|
2381
|
-
if (pendingBossQuestion !== undefined
|
|
3953
|
+
if (pendingBossQuestion !== undefined &&
|
|
3954
|
+
!hasUnresolvedReconciliation()) {
|
|
2382
3955
|
payload.pendingBossQuestion = pendingBossQuestion;
|
|
2383
3956
|
}
|
|
2384
3957
|
if (state.stateId === 'failed') {
|
|
@@ -2388,9 +3961,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2388
3961
|
}
|
|
2389
3962
|
return snapshotJsonValue(payload, 'FSM telemetry payload');
|
|
2390
3963
|
}
|
|
2391
|
-
function enqueueTransitionEmission(payload, state, statuses, position, aborts) {
|
|
3964
|
+
function enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, position, aborts) {
|
|
2392
3965
|
const currentSession = requireSession();
|
|
2393
3966
|
const transitionTrace = createTraceEvent('fsm.transition', payload, position);
|
|
3967
|
+
const acceptedOutcomeTraces = acceptedOutcomes.map((acceptedOutcome) => createTraceEvent('outcome.accepted', acceptedOutcome, position));
|
|
2394
3968
|
const statusEmissions = statuses.map(({ message, data }) => ({
|
|
2395
3969
|
message,
|
|
2396
3970
|
data,
|
|
@@ -2410,6 +3984,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2410
3984
|
topic: 'playbook.fsm.state',
|
|
2411
3985
|
payload,
|
|
2412
3986
|
});
|
|
3987
|
+
for (const acceptedOutcome of acceptedOutcomeTraces) {
|
|
3988
|
+
await currentSession.ports.emitTelemetry({
|
|
3989
|
+
topic: 'playbook.trace',
|
|
3990
|
+
payload: acceptedOutcome,
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
2413
3993
|
for (const status of statusEmissions) {
|
|
2414
3994
|
await currentSession.ports.emitTelemetry({
|
|
2415
3995
|
topic: 'playbook.trace',
|
|
@@ -2462,10 +4042,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2462
4042
|
if (!actor)
|
|
2463
4043
|
return;
|
|
2464
4044
|
suppressInspectionEmissions = true;
|
|
4045
|
+
acceptedOutcomeConsumer.reset();
|
|
2465
4046
|
actor.stop();
|
|
2466
4047
|
}
|
|
2467
4048
|
function buildActor(ports, machineSnapshot) {
|
|
2468
4049
|
priorState = undefined;
|
|
4050
|
+
acceptedOutcomeConsumer.reset();
|
|
2469
4051
|
const actors = {};
|
|
2470
4052
|
if (declaredActors.has('player'))
|
|
2471
4053
|
actors.player = playerActor(ports);
|
|
@@ -2488,11 +4070,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2488
4070
|
? {}
|
|
2489
4071
|
: { snapshot: machineSnapshot }),
|
|
2490
4072
|
inspect: (inspectionEvent) => {
|
|
2491
|
-
if (inspectionEvent.type !== '@xstate.snapshot')
|
|
2492
|
-
return;
|
|
2493
4073
|
if (inspectionEvent.actorRef !== builtActor)
|
|
2494
4074
|
return;
|
|
2495
|
-
if (suppressInspectionEmissions)
|
|
4075
|
+
if (suppressInspectionEmissions)
|
|
4076
|
+
return;
|
|
4077
|
+
if (inspectionEvent.type === '@xstate.action') {
|
|
4078
|
+
try {
|
|
4079
|
+
acceptedOutcomeConsumer.capture(inspectionEvent.action);
|
|
4080
|
+
}
|
|
4081
|
+
catch (error) {
|
|
4082
|
+
latchRuntimeError(error);
|
|
4083
|
+
}
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
if (inspectionEvent.type !== '@xstate.snapshot')
|
|
2496
4087
|
return;
|
|
2497
4088
|
const settlementAborts = consumeActorSettlementAborts(true);
|
|
2498
4089
|
try {
|
|
@@ -2502,14 +4093,56 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2502
4093
|
throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
|
|
2503
4094
|
}
|
|
2504
4095
|
const previousState = priorState;
|
|
4096
|
+
acceptReconstructedGovernedDelivery(state);
|
|
4097
|
+
let acceptedOutcomes = [];
|
|
4098
|
+
try {
|
|
4099
|
+
acceptedOutcomes = acceptedOutcomeConsumer.confirm(previousState, state);
|
|
4100
|
+
}
|
|
4101
|
+
catch (error) {
|
|
4102
|
+
latchRuntimeError(error);
|
|
4103
|
+
}
|
|
4104
|
+
if (state.stateId === 'failed') {
|
|
4105
|
+
if (previousState?.stateId !== 'failed' ||
|
|
4106
|
+
activeGovernedBoundarySeen) {
|
|
4107
|
+
latchFailedGovernedAttempt();
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
else if (previousState?.stateId === 'failed') {
|
|
4111
|
+
failedEffectBoundaryPrefix = undefined;
|
|
4112
|
+
failedGovernedAttemptId = undefined;
|
|
4113
|
+
failedGovernedAttemptUnknown = false;
|
|
4114
|
+
}
|
|
2505
4115
|
const context = (snap.context ??
|
|
2506
4116
|
{});
|
|
4117
|
+
if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
|
|
4118
|
+
deferredReconciliationOperationId !== undefined) {
|
|
4119
|
+
priorState = state;
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
|
|
4123
|
+
expectedBoundPendingQuestion !== undefined &&
|
|
4124
|
+
!deferInspectionEmissions) {
|
|
4125
|
+
validateBoundQuestionProjection();
|
|
4126
|
+
}
|
|
2507
4127
|
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
2508
|
-
const
|
|
2509
|
-
|
|
4128
|
+
const stateStatuses = statusesForState(state, context, inspectionEvent.event);
|
|
4129
|
+
const outcomeStatuses = usesDefaultStatuses
|
|
4130
|
+
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
4131
|
+
message: `→ ${acceptedOutcome}`,
|
|
4132
|
+
}))
|
|
4133
|
+
: [];
|
|
4134
|
+
const statuses = [...outcomeStatuses, ...stateStatuses];
|
|
4135
|
+
const publish = () => enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, tracePositionForActiveTurn(), settlementAborts);
|
|
4136
|
+
if (deferInspectionEmissions) {
|
|
4137
|
+
deferredInspectionEmissions.push(publish);
|
|
4138
|
+
}
|
|
4139
|
+
else {
|
|
4140
|
+
publish();
|
|
4141
|
+
}
|
|
2510
4142
|
priorState = state;
|
|
2511
4143
|
}
|
|
2512
4144
|
catch (error) {
|
|
4145
|
+
acceptedOutcomeConsumer.reset();
|
|
2513
4146
|
latchRuntimeError(error, settlementAborts);
|
|
2514
4147
|
}
|
|
2515
4148
|
},
|
|
@@ -2531,6 +4164,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2531
4164
|
if (outcome === 'quiescent' || outcome === 'no-action') {
|
|
2532
4165
|
return { outcome, state };
|
|
2533
4166
|
}
|
|
4167
|
+
if (outcome === 'unresolved-effect') {
|
|
4168
|
+
return { outcome, state };
|
|
4169
|
+
}
|
|
2534
4170
|
if (outcome === 'suspended') {
|
|
2535
4171
|
const pendingCall = nestedBridge.getPendingCall();
|
|
2536
4172
|
if (!pendingCall) {
|
|
@@ -2540,7 +4176,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2540
4176
|
}
|
|
2541
4177
|
if (outcome === 'terminal') {
|
|
2542
4178
|
const output = actor?.getSnapshot()?.output;
|
|
2543
|
-
const stateDescription =
|
|
4179
|
+
const stateDescription = !hasUnresolvedReconciliation()
|
|
4180
|
+
? stateDescriptionFor(state)
|
|
4181
|
+
: undefined;
|
|
2544
4182
|
return {
|
|
2545
4183
|
outcome,
|
|
2546
4184
|
state,
|
|
@@ -2595,12 +4233,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2595
4233
|
...stateIdentity(result.state.stateId),
|
|
2596
4234
|
};
|
|
2597
4235
|
}
|
|
2598
|
-
// Shared failed-start cleanup for init and
|
|
2599
|
-
// abort/drain nested and host work, optionally emit one best-effort
|
|
4236
|
+
// Shared failed-start cleanup for init, restore, and adoption: stop the
|
|
4237
|
+
// actor, abort/drain nested and host work, optionally emit one best-effort
|
|
2600
4238
|
// session.disposed boundary, and unbind every closure field so dispose
|
|
2601
|
-
// stays callable. The caller rethrows its original failure. A
|
|
2602
|
-
// failure skips the disposal trace — the parked
|
|
2603
|
-
// re-bound in this process, so its persisted snapshot stays
|
|
4239
|
+
// stays callable. The caller rethrows its original failure. A snapshot
|
|
4240
|
+
// start failure skips the disposal trace — the parked generation was
|
|
4241
|
+
// never re-bound in this process, so its persisted snapshot stays
|
|
2604
4242
|
// authoritative (DR-014 §2).
|
|
2605
4243
|
async function cleanupFailedStart(cause, options) {
|
|
2606
4244
|
let finalState;
|
|
@@ -2648,6 +4286,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2648
4286
|
privateResumeTokens.clear();
|
|
2649
4287
|
activePlayerKeys.clear();
|
|
2650
4288
|
playbookCallTurnIds.clear();
|
|
4289
|
+
playbookCallEffectPrefixes.clear();
|
|
2651
4290
|
activeEmissionCalls.clear();
|
|
2652
4291
|
emissionQueue.clear();
|
|
2653
4292
|
judgeQueue.clear();
|
|
@@ -2662,9 +4301,30 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2662
4301
|
actorSettlementErrorAborts = undefined;
|
|
2663
4302
|
activeAbortEmission = undefined;
|
|
2664
4303
|
activeTurnId = undefined;
|
|
4304
|
+
activeGovernedBoundarySeen = false;
|
|
4305
|
+
activeGovernedAttemptId = undefined;
|
|
4306
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
4307
|
+
failedGovernedAttemptUnknown = false;
|
|
4308
|
+
failedEffectBoundaryPrefix = undefined;
|
|
4309
|
+
failedGovernedAttemptId = undefined;
|
|
2665
4310
|
controlPlaneError = undefined;
|
|
2666
4311
|
emissionFailure = undefined;
|
|
2667
4312
|
priorState = undefined;
|
|
4313
|
+
retainedEffectSourceSessionId = undefined;
|
|
4314
|
+
retainedEffectReconciliation = undefined;
|
|
4315
|
+
retainedEffectReconciliationRequired = false;
|
|
4316
|
+
reconstructedGovernedDelivery = undefined;
|
|
4317
|
+
reconstructedGovernedPrefixSequence = undefined;
|
|
4318
|
+
reconstructedAcceptancePending = undefined;
|
|
4319
|
+
governedSettlementsByBoundaryId.clear();
|
|
4320
|
+
governedCompletionEvidenceByBoundaryId.clear();
|
|
4321
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
4322
|
+
deferredReconciliationOperationId = undefined;
|
|
4323
|
+
deferredSettlementClosure = undefined;
|
|
4324
|
+
expectedBoundPendingQuestion = undefined;
|
|
4325
|
+
activeDeferredContinuation = undefined;
|
|
4326
|
+
deferInspectionEmissions = false;
|
|
4327
|
+
deferredInspectionEmissions = [];
|
|
2668
4328
|
lastBossEvent = undefined;
|
|
2669
4329
|
suppressInspectionEmissions = false;
|
|
2670
4330
|
initialized = false;
|
|
@@ -2715,6 +4375,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2715
4375
|
function retryActionFor(snapshot, stateId) {
|
|
2716
4376
|
if (stateId !== 'failed')
|
|
2717
4377
|
return undefined;
|
|
4378
|
+
if (!failedAttemptAllowsReplay())
|
|
4379
|
+
return undefined;
|
|
2718
4380
|
const retryEvent = retryEventFrom(snapshot);
|
|
2719
4381
|
if (retryEvent === undefined)
|
|
2720
4382
|
return undefined;
|
|
@@ -2771,6 +4433,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2771
4433
|
return [];
|
|
2772
4434
|
}
|
|
2773
4435
|
const derived = [];
|
|
4436
|
+
if (hasUnresolvedReconciliation()) {
|
|
4437
|
+
const operation = deferredReconciliationOperationId === undefined
|
|
4438
|
+
? undefined
|
|
4439
|
+
: effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === deferredReconciliationOperationId);
|
|
4440
|
+
const deferredRestoreOperationId = operation?.checkpointRestorationEligible === true
|
|
4441
|
+
? operation.operationId
|
|
4442
|
+
: undefined;
|
|
4443
|
+
derived.push({
|
|
4444
|
+
action: {
|
|
4445
|
+
id: UNRESOLVED_EFFECT_RECONCILIATION_ACTION_ID,
|
|
4446
|
+
label: 'Retry unresolved effect reconciliation',
|
|
4447
|
+
},
|
|
4448
|
+
unresolvedEffectAction: 'reconcile',
|
|
4449
|
+
...(deferredRestoreOperationId === undefined
|
|
4450
|
+
? {}
|
|
4451
|
+
: { deferredRestoreOperationId }),
|
|
4452
|
+
}, {
|
|
4453
|
+
action: {
|
|
4454
|
+
id: UNRESOLVED_EFFECT_ABANDONMENT_ACTION_ID,
|
|
4455
|
+
label: 'Abandon unresolved workflow attempt',
|
|
4456
|
+
},
|
|
4457
|
+
unresolvedEffectAction: 'abandon',
|
|
4458
|
+
});
|
|
4459
|
+
return derived;
|
|
4460
|
+
}
|
|
2774
4461
|
const retry = retryActionFor(snapshot, state.stateId);
|
|
2775
4462
|
if (retry !== undefined)
|
|
2776
4463
|
derived.push(retry);
|
|
@@ -2856,7 +4543,517 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2856
4543
|
...(receipt.disposition === 'executed' ? { run: receipt.run } : {}),
|
|
2857
4544
|
};
|
|
2858
4545
|
}
|
|
4546
|
+
// DR-014 / DR-038: restore and adoption share one transactional snapshot
|
|
4547
|
+
// start. Adoption deliberately differs at the public boundary so a host
|
|
4548
|
+
// can feature-detect permission to bind a retained generation to a fresh
|
|
4549
|
+
// engagement identity; the runtime-visible schema, playbook, and actor
|
|
4550
|
+
// state remain exact, while adoption deliberately re-keys a suspended
|
|
4551
|
+
// bridge into the fresh target counter and session lineage.
|
|
4552
|
+
async function rehydrateSnapshot(kind, nextSession, snapshot, context) {
|
|
4553
|
+
if (initialized || disposed || disposalPromise !== undefined) {
|
|
4554
|
+
throw new Error(`createPlaybookRuntime.${kind}: already initialized`);
|
|
4555
|
+
}
|
|
4556
|
+
const boundSession = bindSession(nextSession);
|
|
4557
|
+
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
|
|
4558
|
+
if (kind === 'adopt' &&
|
|
4559
|
+
effectLedgerCapability !== undefined &&
|
|
4560
|
+
(boundSnapshot.retainedEffectReconciliation?.checkpoint ??
|
|
4561
|
+
boundSnapshot.effectLedger).boundaries.some(({ physicalReceipt }) => physicalReceipt === undefined)) {
|
|
4562
|
+
throw new TypeError('retained runtime checkpoint contains an incomplete physical boundary');
|
|
4563
|
+
}
|
|
4564
|
+
const hostEffectLedger = currentEffectLedger();
|
|
4565
|
+
if (kind === 'restore' &&
|
|
4566
|
+
!isDeepStrictEqual(boundSnapshot.effectLedger, hostEffectLedger)) {
|
|
4567
|
+
throw new TypeError('runtime snapshot effectLedger does not equal the current host mirror');
|
|
4568
|
+
}
|
|
4569
|
+
if (kind === 'adopt' &&
|
|
4570
|
+
!isPlaybookEffectLedgerMonotonicExtension(boundSnapshot.effectLedger, hostEffectLedger)) {
|
|
4571
|
+
throw new TypeError('retained runtime snapshot effectLedger is not a monotonic prefix of the current host mirror');
|
|
4572
|
+
}
|
|
4573
|
+
effectLedgerMirror = hostEffectLedger;
|
|
4574
|
+
if (declaredActors.has('captain') &&
|
|
4575
|
+
boundSnapshot.sequences.captainCall === undefined) {
|
|
4576
|
+
throw new TypeError('runtime snapshot sequences.captainCall is required for a direct-Captain artifact');
|
|
4577
|
+
}
|
|
4578
|
+
const adoptionContext = kind === 'adopt'
|
|
4579
|
+
? snapshotAdoptionContext(context, boundSession, boundSnapshot)
|
|
4580
|
+
: undefined;
|
|
4581
|
+
if (adoptionContext !== undefined &&
|
|
4582
|
+
effectLedgerCapability !== undefined &&
|
|
4583
|
+
!RETAINED_EFFECT_SESSION_ID_PATTERN.test(adoptionContext.sourceSessionId)) {
|
|
4584
|
+
throw new TypeError('schema-3 retained adoption sourceSessionId must be a canonical UUID');
|
|
4585
|
+
}
|
|
4586
|
+
const retainedReconciliation = boundSnapshot.retainedEffectReconciliation ??
|
|
4587
|
+
(adoptionContext !== undefined &&
|
|
4588
|
+
effectLedgerCapability !== undefined &&
|
|
4589
|
+
!retainedAdoptionCheckpointIsSafe(boundSnapshot.effectLedger, hostEffectLedger)
|
|
4590
|
+
? Object.freeze({
|
|
4591
|
+
sourceSessionId: boundSnapshot.retainedEffectSourceSessionId ??
|
|
4592
|
+
adoptionContext.sourceSessionId,
|
|
4593
|
+
checkpoint: boundSnapshot.effectLedger,
|
|
4594
|
+
})
|
|
4595
|
+
: undefined);
|
|
4596
|
+
retainedEffectSourceSessionId =
|
|
4597
|
+
boundSnapshot.retainedEffectSourceSessionId ??
|
|
4598
|
+
boundSnapshot.retainedEffectReconciliation?.sourceSessionId ??
|
|
4599
|
+
(adoptionContext !== undefined && effectLedgerCapability !== undefined
|
|
4600
|
+
? adoptionContext.sourceSessionId
|
|
4601
|
+
: undefined);
|
|
4602
|
+
bindRetainedEffectReconciliation(retainedReconciliation, hostEffectLedger);
|
|
4603
|
+
const sourceSuspendedCall = boundSnapshot.suspendedCall;
|
|
4604
|
+
const suspendedCall = adoptionContext !== undefined && sourceSuspendedCall !== undefined
|
|
4605
|
+
? Object.freeze({
|
|
4606
|
+
callId: 'playbook-1',
|
|
4607
|
+
stateId: sourceSuspendedCall.stateId,
|
|
4608
|
+
playbookId: sourceSuspendedCall.playbookId,
|
|
4609
|
+
text: sourceSuspendedCall.text,
|
|
4610
|
+
childSessionId: adoptionContext.targetChildSessionId,
|
|
4611
|
+
})
|
|
4612
|
+
: sourceSuspendedCall;
|
|
4613
|
+
let priorExternalPlayerTokens;
|
|
4614
|
+
let externalStoreRestoreAttempted = false;
|
|
4615
|
+
let adoptionStartAttempted = false;
|
|
4616
|
+
initialized = true;
|
|
4617
|
+
let finishInitialization;
|
|
4618
|
+
const initialization = new Promise((resolve) => {
|
|
4619
|
+
finishInitialization = resolve;
|
|
4620
|
+
});
|
|
4621
|
+
initInFlight = initialization;
|
|
4622
|
+
const initTask = (async () => {
|
|
4623
|
+
session = boundSession;
|
|
4624
|
+
syncDeferredReconciliationOverlay();
|
|
4625
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
4626
|
+
prepareReconstructedGovernedDelivery(boundSnapshot.state, effectLedgerMirror);
|
|
4627
|
+
savedPorts = boundSession.ports;
|
|
4628
|
+
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
4629
|
+
if (adoptionContext === undefined) {
|
|
4630
|
+
traceSequence = boundSnapshot.sequences.trace;
|
|
4631
|
+
turnSequence = boundSnapshot.sequences.turn;
|
|
4632
|
+
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
4633
|
+
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
4634
|
+
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
4635
|
+
captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
|
|
4636
|
+
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
4637
|
+
// apply boundary consumed trace numbers, so the persisted trace
|
|
4638
|
+
// counter is a collision-safe id floor here too, keeping
|
|
4639
|
+
// `apply-<n>` call ids unique across a snapshot start.
|
|
4640
|
+
applyCallSequence = boundSnapshot.sequences.trace;
|
|
4641
|
+
}
|
|
4642
|
+
else {
|
|
4643
|
+
// DR-038 §5: a new engagement owns a new counter space. A
|
|
4644
|
+
// rebased live child consumes the first target-local playbook id;
|
|
4645
|
+
// all other counters begin before their first target boundary.
|
|
4646
|
+
traceSequence = 0;
|
|
4647
|
+
turnSequence = 0;
|
|
4648
|
+
judgeCallSequence = 0;
|
|
4649
|
+
playerCallSequence = 0;
|
|
4650
|
+
playbookCallSequence = suspendedCall === undefined ? 0 : 1;
|
|
4651
|
+
captainCallSequence = 0;
|
|
4652
|
+
applyCallSequence = 0;
|
|
4653
|
+
}
|
|
4654
|
+
// Same-engagement restore owns the snapshot's token projection.
|
|
4655
|
+
// Adoption leaves it inert: the fresh engagement's player ledger (or
|
|
4656
|
+
// the absence of one) is authoritative, and its binding rules land
|
|
4657
|
+
// independently under DR-038 §4.
|
|
4658
|
+
if (kind === 'restore') {
|
|
4659
|
+
if (boundSession.playerSessions) {
|
|
4660
|
+
priorExternalPlayerTokens = snapshotRoleResumeTokens();
|
|
4661
|
+
externalStoreRestoreAttempted = true;
|
|
4662
|
+
}
|
|
4663
|
+
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
4664
|
+
}
|
|
4665
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
4666
|
+
if (suspendedCall !== undefined) {
|
|
4667
|
+
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
4668
|
+
if (hasGovernedPlayerStates) {
|
|
4669
|
+
const savedPrefix = kind === 'restore'
|
|
4670
|
+
? suspendedCall.effectBoundaryPrefixSequence
|
|
4671
|
+
: undefined;
|
|
4672
|
+
// Pre-task-5 snapshots and retained-generation adoption have no
|
|
4673
|
+
// target-local causal prefix. Zero is conservative; an explicit
|
|
4674
|
+
// null preserves an observation failure as unknown/fail-closed.
|
|
4675
|
+
playbookCallEffectPrefixes.set(suspendedCall.callId, savedPrefix === null ? undefined : (savedPrefix ?? 0));
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
suppressInspectionEmissions = true;
|
|
4679
|
+
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
4680
|
+
if (adoptionContext !== undefined) {
|
|
4681
|
+
adoptionStartAttempted = true;
|
|
4682
|
+
await emitTrace('session.started', {
|
|
4683
|
+
state: boundSnapshot.state,
|
|
4684
|
+
...stateIdentity(boundSnapshot.state.stateId),
|
|
4685
|
+
adoption: {
|
|
4686
|
+
sourceSessionId: adoptionContext.sourceSessionId,
|
|
4687
|
+
sourceGenerationId: adoptionContext.sourceGenerationId,
|
|
4688
|
+
...(sourceSuspendedCall === undefined ||
|
|
4689
|
+
suspendedCall === undefined
|
|
4690
|
+
? {}
|
|
4691
|
+
: {
|
|
4692
|
+
sourceCallId: sourceSuspendedCall.callId,
|
|
4693
|
+
sourceChildSessionId: sourceSuspendedCall.childSessionId,
|
|
4694
|
+
targetCallId: suspendedCall.callId,
|
|
4695
|
+
targetChildSessionId: suspendedCall.childSessionId,
|
|
4696
|
+
}),
|
|
4697
|
+
},
|
|
4698
|
+
}, suspendedCall === undefined
|
|
4699
|
+
? {}
|
|
4700
|
+
: { callId: suspendedCall.callId });
|
|
4701
|
+
}
|
|
4702
|
+
actor.start();
|
|
4703
|
+
// A start-time actor error rides the startup emission channel
|
|
4704
|
+
// (latchRuntimeError); consume both latches here so the original
|
|
4705
|
+
// error outranks the derived status check below.
|
|
4706
|
+
{
|
|
4707
|
+
const startupFailure = emissionFailure;
|
|
4708
|
+
if (controlPlaneError !== undefined ||
|
|
4709
|
+
startupFailure !== undefined) {
|
|
4710
|
+
const startupError = controlPlaneError !== undefined
|
|
4711
|
+
? controlPlaneError
|
|
4712
|
+
: startupFailure.error;
|
|
4713
|
+
controlPlaneError = undefined;
|
|
4714
|
+
if (emissionFailure === startupFailure) {
|
|
4715
|
+
emissionFailure = undefined;
|
|
4716
|
+
}
|
|
4717
|
+
throw startupError;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
|
|
4721
|
+
? {}
|
|
4722
|
+
: {
|
|
4723
|
+
pendingCall: {
|
|
4724
|
+
callId: suspendedCall.callId,
|
|
4725
|
+
playbookId: suspendedCall.playbookId,
|
|
4726
|
+
childSessionId: suspendedCall.childSessionId,
|
|
4727
|
+
},
|
|
4728
|
+
});
|
|
4729
|
+
if (restoredState.status !== 'active') {
|
|
4730
|
+
throw new Error(`createPlaybookRuntime.${kind}: restored actor status is ${restoredState.status}, expected active`);
|
|
4731
|
+
}
|
|
4732
|
+
if (stableJson(restoredState, 'restored runtime state') !==
|
|
4733
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
4734
|
+
throw new Error(`createPlaybookRuntime.${kind}: restored actor state does not match snapshot state`);
|
|
4735
|
+
}
|
|
4736
|
+
const restoredFailedEffectAttempt = restoredState.stateId === 'failed' && kind === 'restore'
|
|
4737
|
+
? boundSnapshot.failedEffectAttempt
|
|
4738
|
+
: undefined;
|
|
4739
|
+
failedEffectBoundaryPrefix =
|
|
4740
|
+
restoredFailedEffectAttempt?.boundaryPrefix;
|
|
4741
|
+
failedGovernedAttemptId =
|
|
4742
|
+
typeof restoredFailedEffectAttempt?.attemptId === 'string'
|
|
4743
|
+
? restoredFailedEffectAttempt.attemptId
|
|
4744
|
+
: undefined;
|
|
4745
|
+
activeGovernedBoundarySeen = false;
|
|
4746
|
+
activeGovernedAttemptId = undefined;
|
|
4747
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
4748
|
+
failedGovernedAttemptUnknown =
|
|
4749
|
+
restoredState.stateId === 'failed' &&
|
|
4750
|
+
kind === 'restore' &&
|
|
4751
|
+
hasGovernedPlayerStates &&
|
|
4752
|
+
restoredFailedEffectAttempt === undefined;
|
|
4753
|
+
priorState = restoredState;
|
|
4754
|
+
await drainEmissions();
|
|
4755
|
+
suppressInspectionEmissions = false;
|
|
4756
|
+
acceptReconstructedGovernedDelivery(currentState());
|
|
4757
|
+
// Final fallible step: after this publication the authoritative
|
|
4758
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
4759
|
+
// snapshot-start validation may trigger failed-start rollback.
|
|
4760
|
+
nestedBridge.confirmRestore();
|
|
4761
|
+
})();
|
|
4762
|
+
try {
|
|
4763
|
+
await initTask;
|
|
4764
|
+
}
|
|
4765
|
+
catch (error) {
|
|
4766
|
+
let failure = error;
|
|
4767
|
+
if (externalStoreRestoreAttempted &&
|
|
4768
|
+
priorExternalPlayerTokens !== undefined) {
|
|
4769
|
+
try {
|
|
4770
|
+
boundSession.playerSessions.restore(priorExternalPlayerTokens);
|
|
4771
|
+
}
|
|
4772
|
+
catch (rollbackError) {
|
|
4773
|
+
failure = new AggregateError([error, rollbackError], `createPlaybookRuntime.${kind} and player continuation rollback failed`);
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
await cleanupFailedStart(failure, {
|
|
4777
|
+
emitDisposal: adoptionStartAttempted,
|
|
4778
|
+
});
|
|
4779
|
+
throw failure;
|
|
4780
|
+
}
|
|
4781
|
+
finally {
|
|
4782
|
+
finishInitialization();
|
|
4783
|
+
if (initInFlight === initialization)
|
|
4784
|
+
initInFlight = undefined;
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
function validateBoundQuestionProjection() {
|
|
4788
|
+
const expected = expectedBoundPendingQuestion;
|
|
4789
|
+
if (expected === undefined)
|
|
4790
|
+
return;
|
|
4791
|
+
const snapshot = actor?.getSnapshot();
|
|
4792
|
+
const state = snapshot === undefined
|
|
4793
|
+
? undefined
|
|
4794
|
+
: normalizePlaybookSnapshot(snapshot);
|
|
4795
|
+
const context = (snapshot
|
|
4796
|
+
?.context ?? {});
|
|
4797
|
+
const actual = state?.stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
4798
|
+
? pendingBossQuestionFromContext(context)
|
|
4799
|
+
: undefined;
|
|
4800
|
+
if (!isDeepStrictEqual(actual, expected)) {
|
|
4801
|
+
throw new Error(`${label} deferred FSM question does not equal its durable binding`);
|
|
4802
|
+
}
|
|
4803
|
+
const operation = currentBoundDeferredOperation(expected);
|
|
4804
|
+
if (operation === undefined) {
|
|
4805
|
+
throw new Error(`${label} deferred FSM question has no exact durable operation`);
|
|
4806
|
+
}
|
|
4807
|
+
expectedBoundPendingQuestion = undefined;
|
|
4808
|
+
}
|
|
4809
|
+
function settleDeferredInspectionBuffer(publish) {
|
|
4810
|
+
const buffered = deferredInspectionEmissions;
|
|
4811
|
+
deferredInspectionEmissions = [];
|
|
4812
|
+
deferInspectionEmissions = false;
|
|
4813
|
+
if (publish) {
|
|
4814
|
+
for (const emission of buffered)
|
|
4815
|
+
emission();
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
async function continueBoundDeferredOperation(operation, event, signal, turnId, classificationLine) {
|
|
4819
|
+
if (repositoryCapability === undefined) {
|
|
4820
|
+
throw new Error(`${label} deferred continuation requires repository.runDeferred`);
|
|
4821
|
+
}
|
|
4822
|
+
const effectBoundary = continuationBoundarySeed(operation, turnId);
|
|
4823
|
+
const continuation = {
|
|
4824
|
+
operationId: operation.operationId,
|
|
4825
|
+
effectBoundary,
|
|
4826
|
+
rawPlayerSettled: deferredValue(),
|
|
4827
|
+
delivery: deferredValue(),
|
|
4828
|
+
};
|
|
4829
|
+
activeDeferredContinuation = continuation;
|
|
4830
|
+
deferInspectionEmissions = true;
|
|
4831
|
+
let continuationStarted = false;
|
|
4832
|
+
let deliverySettled = false;
|
|
4833
|
+
deferredInspectionEmissions =
|
|
4834
|
+
classificationLine === undefined
|
|
4835
|
+
? []
|
|
4836
|
+
: [() => void runtimePorts.emitStatus(classificationLine)];
|
|
4837
|
+
try {
|
|
4838
|
+
const result = await repositoryCapability.runDeferred({
|
|
4839
|
+
mode: 'continue',
|
|
4840
|
+
signal,
|
|
4841
|
+
operationId: operation.operationId,
|
|
4842
|
+
effectBoundary,
|
|
4843
|
+
operation: async ({ playerContinuation }) => {
|
|
4844
|
+
const selectedContinuation = retainedEffectSourceSessionId === undefined
|
|
4845
|
+
? playerContinuation
|
|
4846
|
+
: selectPlayerResume(effectBoundary.roleId, resolvedPlayerId(effectBoundary.roleId));
|
|
4847
|
+
if (selectedContinuation !== false &&
|
|
4848
|
+
(typeof selectedContinuation !== 'string' ||
|
|
4849
|
+
selectedContinuation.trim().length === 0)) {
|
|
4850
|
+
throw new TypeError(`${label} bound deferred player continuation is invalid`);
|
|
4851
|
+
}
|
|
4852
|
+
// Retained adoption owns a fresh Captain-session player ledger;
|
|
4853
|
+
// no source token becomes target ownership. Same-engagement
|
|
4854
|
+
// continuation still uses the exact durable binding.
|
|
4855
|
+
continuation.playerContinuation = selectedContinuation;
|
|
4856
|
+
continuationStarted = true;
|
|
4857
|
+
actor.send(event);
|
|
4858
|
+
// The invoked player remains gated inside boundary.callPlayer.
|
|
4859
|
+
// Return to the host only after the raw player call settles so it
|
|
4860
|
+
// can capture and persist the receipt before any actor output or
|
|
4861
|
+
// error reaches XState.
|
|
4862
|
+
await continuation.rawPlayerSettled.promise;
|
|
4863
|
+
return null;
|
|
4864
|
+
},
|
|
4865
|
+
completeEffectBoundary: deferredContinuationCompletionEvidence,
|
|
4866
|
+
});
|
|
4867
|
+
effectLedgerMirror = assertPlaybookEffectLedger(result.effectLedger, `${label} deferred continuation effect ledger`);
|
|
4868
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
4869
|
+
syncDeferredReconciliationOverlay();
|
|
4870
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
4871
|
+
if (result.status !== 'continued') {
|
|
4872
|
+
if (continuationStarted) {
|
|
4873
|
+
deliverySettled = true;
|
|
4874
|
+
continuation.delivery.reject(markFsmResultFailure(new Error(`${label} deferred continuation remains unresolved`)));
|
|
4875
|
+
await waitForPlaybookQuiescence(actor, {
|
|
4876
|
+
pendingCalls: nestedBridge,
|
|
4877
|
+
});
|
|
4878
|
+
if (controlPlaneError !== undefined)
|
|
4879
|
+
throw controlPlaneError;
|
|
4880
|
+
}
|
|
4881
|
+
expectedBoundPendingQuestion = undefined;
|
|
4882
|
+
settleDeferredInspectionBuffer(false);
|
|
4883
|
+
return 'unresolved';
|
|
4884
|
+
}
|
|
4885
|
+
const completed = effectLedgerMirror.boundaries.find(({ boundaryId }) => boundaryId === effectBoundary.boundaryId);
|
|
4886
|
+
if (completed?.physicalReceipt === undefined ||
|
|
4887
|
+
!isDeepStrictEqual(completed.physicalReceipt, result.receipt)) {
|
|
4888
|
+
throw new TypeError(`${label} deferred continuation did not acknowledge its physical boundary`);
|
|
4889
|
+
}
|
|
4890
|
+
recordActiveGovernedAttempt(completed);
|
|
4891
|
+
let settlement = governedSettlementsByBoundaryId.get(effectBoundary.boundaryId);
|
|
4892
|
+
if (settlement === undefined &&
|
|
4893
|
+
continuation.result?.status === 'ok' &&
|
|
4894
|
+
!isEmptyFinalText(continuation.result.finalText)) {
|
|
4895
|
+
settlement = unresolvedGovernedSettlement('host omitted governed semantic settlement');
|
|
4896
|
+
}
|
|
4897
|
+
assertAcknowledgedGovernedEvidence(completed, settlement, effectLedgerMirror);
|
|
4898
|
+
governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
|
|
4899
|
+
governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
|
|
4900
|
+
if (settlement?.status === 'resolved' &&
|
|
4901
|
+
settlement.output.guard === 'needsBossReply' &&
|
|
4902
|
+
result.deferredStatus !== 'bound') {
|
|
4903
|
+
settlement = unresolvedGovernedSettlement('deferred question did not receive an eligible durable binding');
|
|
4904
|
+
}
|
|
4905
|
+
if (settlement?.status === 'unresolved') {
|
|
4906
|
+
unresolvedSemanticBoundaryIds.add(effectBoundary.boundaryId);
|
|
4907
|
+
}
|
|
4908
|
+
else if (settlement?.status === 'resolved') {
|
|
4909
|
+
unresolvedSemanticBoundaryIds.delete(effectBoundary.boundaryId);
|
|
4910
|
+
}
|
|
4911
|
+
if (result.logicalReceipt !== undefined) {
|
|
4912
|
+
const completedOperation = effectLedgerMirror.logicalOperations.find(({ operationId }) => operationId === operation.operationId);
|
|
4913
|
+
if (completedOperation?.logicalReceipt === undefined ||
|
|
4914
|
+
!isDeepStrictEqual(completedOperation.logicalReceipt, result.logicalReceipt)) {
|
|
4915
|
+
throw new TypeError(`${label} deferred continuation did not acknowledge its cumulative receipt`);
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
if (settlement?.status === 'resolved' &&
|
|
4919
|
+
settlement.output.guard === 'needsBossReply') {
|
|
4920
|
+
if (result.deferredStatus !== 'bound' &&
|
|
4921
|
+
result.deferredStatus !== 'unresolved') {
|
|
4922
|
+
throw new TypeError(`${label} repeated deferred settlement omitted its durable binding status`);
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
else if (settlement?.status === 'resolved' &&
|
|
4926
|
+
result.logicalReceipt === undefined) {
|
|
4927
|
+
throw new TypeError(`${label} final deferred settlement omitted its cumulative receipt`);
|
|
4928
|
+
}
|
|
4929
|
+
if (continuation.callError !== undefined) {
|
|
4930
|
+
deliverySettled = true;
|
|
4931
|
+
continuation.delivery.reject(continuation.callError);
|
|
4932
|
+
}
|
|
4933
|
+
else if (continuation.result === undefined) {
|
|
4934
|
+
deliverySettled = true;
|
|
4935
|
+
continuation.delivery.reject(markFsmResultFailure(new Error(`${label} deferred player returned no result`)));
|
|
4936
|
+
}
|
|
4937
|
+
else {
|
|
4938
|
+
if (settlement !== undefined) {
|
|
4939
|
+
governedPlayerSettlements.set(continuation.result, settlement);
|
|
4940
|
+
}
|
|
4941
|
+
// The bound answer authorizes exactly this one player call. Clear
|
|
4942
|
+
// its live delivery scope before XState can advance through a
|
|
4943
|
+
// nested call and invoke a later governed player in the same turn.
|
|
4944
|
+
activeDeferredContinuation = undefined;
|
|
4945
|
+
deliverySettled = true;
|
|
4946
|
+
continuation.delivery.resolve(continuation.result);
|
|
4947
|
+
}
|
|
4948
|
+
await waitForPlaybookQuiescence(actor, {
|
|
4949
|
+
pendingCalls: nestedBridge,
|
|
4950
|
+
});
|
|
4951
|
+
if (controlPlaneError !== undefined)
|
|
4952
|
+
throw controlPlaneError;
|
|
4953
|
+
if (!hasUnresolvedReconciliation()) {
|
|
4954
|
+
validateBoundQuestionProjection();
|
|
4955
|
+
settleDeferredInspectionBuffer(true);
|
|
4956
|
+
}
|
|
4957
|
+
else {
|
|
4958
|
+
expectedBoundPendingQuestion = undefined;
|
|
4959
|
+
settleDeferredInspectionBuffer(false);
|
|
4960
|
+
}
|
|
4961
|
+
return 'continued';
|
|
4962
|
+
}
|
|
4963
|
+
catch (error) {
|
|
4964
|
+
let failure = error;
|
|
4965
|
+
governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
|
|
4966
|
+
governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
|
|
4967
|
+
if (continuationStarted && !deliverySettled) {
|
|
4968
|
+
deliverySettled = true;
|
|
4969
|
+
continuation.delivery.reject(error);
|
|
4970
|
+
try {
|
|
4971
|
+
await waitForPlaybookQuiescence(actor, {
|
|
4972
|
+
pendingCalls: nestedBridge,
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
catch (drainError) {
|
|
4976
|
+
failure = new AggregateError([error, drainError], `${label} deferred continuation rejection and actor drain both failed`);
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
if (continuationStarted) {
|
|
4980
|
+
closeAfterIndeterminateDeferredSettlement(operation.operationId, failure);
|
|
4981
|
+
}
|
|
4982
|
+
settleDeferredInspectionBuffer(false);
|
|
4983
|
+
throw failure;
|
|
4984
|
+
}
|
|
4985
|
+
finally {
|
|
4986
|
+
activeDeferredContinuation = undefined;
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
function isExactDeferredBossReply(snapshot, event, pending) {
|
|
4990
|
+
const candidate = event;
|
|
4991
|
+
return (candidate.type === 'BOSS_REPLY' &&
|
|
4992
|
+
(candidate.questionId === undefined ||
|
|
4993
|
+
candidate.questionId === pending.questionId) &&
|
|
4994
|
+
typeof candidate.answer === 'string' &&
|
|
4995
|
+
candidate.answer.trim().length > 0 &&
|
|
4996
|
+
snapshotCan(snapshot, event));
|
|
4997
|
+
}
|
|
4998
|
+
async function parkBoundDeferredOperation(operationId, signal) {
|
|
4999
|
+
if (repositoryCapability === undefined) {
|
|
5000
|
+
throw new Error(`${label} deferred parking requires repository.runDeferred`);
|
|
5001
|
+
}
|
|
5002
|
+
const parked = await repositoryCapability.runDeferred({
|
|
5003
|
+
mode: 'park',
|
|
5004
|
+
signal,
|
|
5005
|
+
operationId,
|
|
5006
|
+
});
|
|
5007
|
+
if (parked.status !== 'parked') {
|
|
5008
|
+
throw new TypeError(`${label} repository refused to park its deferred operation`);
|
|
5009
|
+
}
|
|
5010
|
+
effectLedgerMirror = assertPlaybookEffectLedger(parked.effectLedger, `${label} parked deferred effect ledger`);
|
|
5011
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
5012
|
+
syncDeferredReconciliationOverlay();
|
|
5013
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
5014
|
+
if (deferredReconciliationOperationId !== operationId) {
|
|
5015
|
+
throw new TypeError(`${label} parked deferred operation is not structurally unresolved`);
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
async function restoreBoundDeferredOperation(operationId, signal) {
|
|
5019
|
+
if (repositoryCapability === undefined) {
|
|
5020
|
+
throw new Error(`${label} deferred restoration requires repository.runDeferred`);
|
|
5021
|
+
}
|
|
5022
|
+
const restored = await repositoryCapability.runDeferred({
|
|
5023
|
+
mode: 'restore',
|
|
5024
|
+
signal,
|
|
5025
|
+
operationId,
|
|
5026
|
+
});
|
|
5027
|
+
if (restored.status === 'parked') {
|
|
5028
|
+
throw new TypeError(`${label} repository returned a park result for deferred restoration`);
|
|
5029
|
+
}
|
|
5030
|
+
effectLedgerMirror = assertPlaybookEffectLedger(restored.effectLedger, `${label} restored deferred effect ledger`);
|
|
5031
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
5032
|
+
syncDeferredReconciliationOverlay();
|
|
5033
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
5034
|
+
if (restored.status === 'restored') {
|
|
5035
|
+
if (deferredReconciliationOperationId !== undefined) {
|
|
5036
|
+
throw new TypeError(`${label} restored deferred operation remained unresolved`);
|
|
5037
|
+
}
|
|
5038
|
+
const snapshot = actor.getSnapshot();
|
|
5039
|
+
const state = normalizePlaybookSnapshot(snapshot);
|
|
5040
|
+
const context = (snapshot.context ??
|
|
5041
|
+
{});
|
|
5042
|
+
const pending = pendingBossQuestionForState(state, context);
|
|
5043
|
+
if (pending === undefined ||
|
|
5044
|
+
currentBoundDeferredOperation(pending)?.operationId !== operationId) {
|
|
5045
|
+
throw new TypeError(`${label} restored deferred wait does not equal its FSM question`);
|
|
5046
|
+
}
|
|
5047
|
+
}
|
|
5048
|
+
else if (deferredReconciliationOperationId !== operationId) {
|
|
5049
|
+
throw new TypeError(`${label} unresolved deferred restoration lost its operation identity`);
|
|
5050
|
+
}
|
|
5051
|
+
return restored.status;
|
|
5052
|
+
}
|
|
2859
5053
|
const runtime = {
|
|
5054
|
+
...(retainedGenerationMetadata === undefined
|
|
5055
|
+
? {}
|
|
5056
|
+
: { retainedGenerationMetadata }),
|
|
2860
5057
|
async init(nextSession) {
|
|
2861
5058
|
if (initialized || disposed || disposalPromise !== undefined) {
|
|
2862
5059
|
throw new Error('createPlaybookRuntime.init: already initialized');
|
|
@@ -2870,6 +5067,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2870
5067
|
initInFlight = initialization;
|
|
2871
5068
|
const initTask = (async () => {
|
|
2872
5069
|
session = boundSession;
|
|
5070
|
+
syncDeferredReconciliationOverlay();
|
|
5071
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
2873
5072
|
savedPorts = boundSession.ports;
|
|
2874
5073
|
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
2875
5074
|
suppressInspectionEmissions = false;
|
|
@@ -2902,6 +5101,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2902
5101
|
}
|
|
2903
5102
|
if (activeSignal !== undefined)
|
|
2904
5103
|
return undefined;
|
|
5104
|
+
if (deferredSettlementClosure !== undefined)
|
|
5105
|
+
return undefined;
|
|
2905
5106
|
const pendingCall = nestedBridge.getPendingCall();
|
|
2906
5107
|
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
2907
5108
|
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
@@ -2925,6 +5126,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2925
5126
|
suspendedCall = {
|
|
2926
5127
|
...bridgeSuspendedCall,
|
|
2927
5128
|
...(turnId === undefined ? {} : { turnId }),
|
|
5129
|
+
...(hasGovernedPlayerStates
|
|
5130
|
+
? {
|
|
5131
|
+
effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
|
|
5132
|
+
}
|
|
5133
|
+
: {}),
|
|
2928
5134
|
};
|
|
2929
5135
|
}
|
|
2930
5136
|
const state = currentState();
|
|
@@ -2933,9 +5139,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2933
5139
|
const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
2934
5140
|
const context = actor.getSnapshot()
|
|
2935
5141
|
.context;
|
|
2936
|
-
|
|
5142
|
+
effectLedgerMirror = currentEffectLedger();
|
|
5143
|
+
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
5144
|
+
syncDeferredReconciliationOverlay();
|
|
5145
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
5146
|
+
const pending = !hasUnresolvedReconciliation()
|
|
5147
|
+
? pendingBossQuestionForState(state, context ?? {})
|
|
5148
|
+
: undefined;
|
|
5149
|
+
const failedEffectAttempt = hasGovernedPlayerStates &&
|
|
5150
|
+
state.stateId === 'failed' &&
|
|
5151
|
+
failedAttemptMatchesCurrentLedger(effectLedgerMirror)
|
|
5152
|
+
? {
|
|
5153
|
+
boundaryPrefix: failedEffectBoundaryPrefix,
|
|
5154
|
+
attemptId: failedGovernedAttemptId ?? null,
|
|
5155
|
+
}
|
|
5156
|
+
: undefined;
|
|
2937
5157
|
return {
|
|
2938
|
-
schemaVersion:
|
|
5158
|
+
schemaVersion: 4,
|
|
2939
5159
|
playbookId: session.playbookId,
|
|
2940
5160
|
machine: machineSnapshot,
|
|
2941
5161
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -2960,6 +5180,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2960
5180
|
sourceItem: pending.sourceItem,
|
|
2961
5181
|
},
|
|
2962
5182
|
],
|
|
5183
|
+
effectLedger: effectLedgerMirror,
|
|
5184
|
+
...(retainedEffectSourceSessionId === undefined
|
|
5185
|
+
? {}
|
|
5186
|
+
: { retainedEffectSourceSessionId }),
|
|
5187
|
+
...(retainedEffectReconciliation === undefined
|
|
5188
|
+
? {}
|
|
5189
|
+
: { retainedEffectReconciliation }),
|
|
5190
|
+
...(failedEffectAttempt === undefined
|
|
5191
|
+
? {}
|
|
5192
|
+
: { failedEffectAttempt }),
|
|
2963
5193
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2964
5194
|
};
|
|
2965
5195
|
},
|
|
@@ -2969,114 +5199,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2969
5199
|
// the session already started; the next public boundary continues
|
|
2970
5200
|
// the contiguous trace sequence.
|
|
2971
5201
|
async restore(nextSession, snapshot) {
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
const suspendedCall = boundSnapshot.suspendedCall;
|
|
2982
|
-
let priorExternalPlayerTokens;
|
|
2983
|
-
let externalStoreRestoreAttempted = false;
|
|
2984
|
-
initialized = true;
|
|
2985
|
-
let finishInitialization;
|
|
2986
|
-
const initialization = new Promise((resolve) => {
|
|
2987
|
-
finishInitialization = resolve;
|
|
2988
|
-
});
|
|
2989
|
-
initInFlight = initialization;
|
|
2990
|
-
const initTask = (async () => {
|
|
2991
|
-
session = boundSession;
|
|
2992
|
-
savedPorts = boundSession.ports;
|
|
2993
|
-
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
2994
|
-
traceSequence = boundSnapshot.sequences.trace;
|
|
2995
|
-
turnSequence = boundSnapshot.sequences.turn;
|
|
2996
|
-
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
2997
|
-
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
2998
|
-
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
2999
|
-
captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
|
|
3000
|
-
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
3001
|
-
// apply boundary consumed trace numbers, so the persisted trace
|
|
3002
|
-
// counter is a collision-safe id floor here too, keeping
|
|
3003
|
-
// `apply-<n>` call ids unique across restore.
|
|
3004
|
-
applyCallSequence = boundSnapshot.sequences.trace;
|
|
3005
|
-
if (boundSession.playerSessions) {
|
|
3006
|
-
priorExternalPlayerTokens = snapshotRoleResumeTokens();
|
|
3007
|
-
externalStoreRestoreAttempted = true;
|
|
3008
|
-
}
|
|
3009
|
-
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
3010
|
-
nestedBridge.prepareRestore(suspendedCall);
|
|
3011
|
-
if (suspendedCall !== undefined) {
|
|
3012
|
-
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
3013
|
-
}
|
|
3014
|
-
suppressInspectionEmissions = true;
|
|
3015
|
-
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
3016
|
-
actor.start();
|
|
3017
|
-
// A start-time actor error rides the startup emission channel
|
|
3018
|
-
// (latchRuntimeError); consume both latches here so the original
|
|
3019
|
-
// error outranks the derived status check below.
|
|
3020
|
-
{
|
|
3021
|
-
const startupFailure = emissionFailure;
|
|
3022
|
-
if (controlPlaneError !== undefined ||
|
|
3023
|
-
startupFailure !== undefined) {
|
|
3024
|
-
const startupError = controlPlaneError !== undefined
|
|
3025
|
-
? controlPlaneError
|
|
3026
|
-
: startupFailure.error;
|
|
3027
|
-
controlPlaneError = undefined;
|
|
3028
|
-
if (emissionFailure === startupFailure) {
|
|
3029
|
-
emissionFailure = undefined;
|
|
3030
|
-
}
|
|
3031
|
-
throw startupError;
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
|
|
3035
|
-
? {}
|
|
3036
|
-
: {
|
|
3037
|
-
pendingCall: {
|
|
3038
|
-
callId: suspendedCall.callId,
|
|
3039
|
-
playbookId: suspendedCall.playbookId,
|
|
3040
|
-
childSessionId: suspendedCall.childSessionId,
|
|
3041
|
-
},
|
|
3042
|
-
});
|
|
3043
|
-
if (restoredState.status !== 'active') {
|
|
3044
|
-
throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
|
|
3045
|
-
}
|
|
3046
|
-
if (stableJson(restoredState, 'restored runtime state') !==
|
|
3047
|
-
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
3048
|
-
throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
|
|
3049
|
-
}
|
|
3050
|
-
priorState = restoredState;
|
|
3051
|
-
await drainEmissions();
|
|
3052
|
-
suppressInspectionEmissions = false;
|
|
3053
|
-
// Final fallible step: after this publication the authoritative
|
|
3054
|
-
// child has rejoined ordinary resume/abort ownership, so no later
|
|
3055
|
-
// restore validation may trigger failed-start rollback.
|
|
3056
|
-
nestedBridge.confirmRestore();
|
|
3057
|
-
})();
|
|
3058
|
-
try {
|
|
3059
|
-
await initTask;
|
|
3060
|
-
}
|
|
3061
|
-
catch (error) {
|
|
3062
|
-
let failure = error;
|
|
3063
|
-
if (externalStoreRestoreAttempted &&
|
|
3064
|
-
priorExternalPlayerTokens !== undefined) {
|
|
3065
|
-
try {
|
|
3066
|
-
boundSession.playerSessions.restore(priorExternalPlayerTokens);
|
|
3067
|
-
}
|
|
3068
|
-
catch (rollbackError) {
|
|
3069
|
-
failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
3073
|
-
throw failure;
|
|
3074
|
-
}
|
|
3075
|
-
finally {
|
|
3076
|
-
finishInitialization();
|
|
3077
|
-
if (initInFlight === initialization)
|
|
3078
|
-
initInFlight = undefined;
|
|
3079
|
-
}
|
|
5202
|
+
await rehydrateSnapshot('restore', nextSession, snapshot);
|
|
5203
|
+
},
|
|
5204
|
+
// DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
|
|
5205
|
+
// engagement identity and counter lineage, exposed separately so
|
|
5206
|
+
// capability-less bespoke runtimes can omit it. Runtime-visible
|
|
5207
|
+
// preflight mismatches reject before effects; after preflight the new
|
|
5208
|
+
// session.started boundary owns failed-start cleanup just like init.
|
|
5209
|
+
async adopt(nextSession, snapshot, context) {
|
|
5210
|
+
await rehydrateSnapshot('adopt', nextSession, snapshot, context);
|
|
3080
5211
|
},
|
|
3081
5212
|
// DR-029 / PBRT-52: side-effect-free control view over the live
|
|
3082
5213
|
// snapshot, valid at parked quiescence outside an active boundary.
|
|
@@ -3092,14 +5223,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3092
5223
|
if (activeSignal !== undefined) {
|
|
3093
5224
|
throw new Error('createPlaybookRuntime.describe: another runtime turn is active');
|
|
3094
5225
|
}
|
|
5226
|
+
assertDeferredSettlementOpen('describe');
|
|
5227
|
+
refreshRetainedEffectFenceFromHost();
|
|
3095
5228
|
const snapshot = actor.getSnapshot();
|
|
3096
5229
|
const state = currentState();
|
|
3097
5230
|
const context = (snapshot.context ??
|
|
3098
5231
|
{});
|
|
3099
|
-
const pending =
|
|
5232
|
+
const pending = !hasUnresolvedReconciliation()
|
|
5233
|
+
? pendingBossQuestionForState(state, context)
|
|
5234
|
+
: undefined;
|
|
3100
5235
|
const lastError = normalizeErrorFull(context.lastError);
|
|
3101
5236
|
const projectedContext = projectControlContext(context);
|
|
3102
|
-
const stateDescription =
|
|
5237
|
+
const stateDescription = !hasUnresolvedReconciliation()
|
|
5238
|
+
? stateDescriptionFor(state)
|
|
5239
|
+
: undefined;
|
|
3103
5240
|
return deepFreeze({
|
|
3104
5241
|
state,
|
|
3105
5242
|
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
@@ -3132,6 +5269,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3132
5269
|
if (input === null || typeof input !== 'object') {
|
|
3133
5270
|
throw new TypeError('createPlaybookRuntime.apply: input must be an object');
|
|
3134
5271
|
}
|
|
5272
|
+
assertDeferredSettlementOpen('apply');
|
|
3135
5273
|
const { actionId, key, signal } = input;
|
|
3136
5274
|
if (typeof actionId !== 'string' || actionId.length === 0) {
|
|
3137
5275
|
throw new TypeError('createPlaybookRuntime.apply: actionId must be a non-empty string');
|
|
@@ -3159,10 +5297,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3159
5297
|
// An abort before acceptance ends the call with no receipt
|
|
3160
5298
|
// recorded, like every other pre-acceptance failure.
|
|
3161
5299
|
signal.throwIfAborted();
|
|
5300
|
+
refreshRetainedEffectFenceFromHost();
|
|
3162
5301
|
const turnId = ++turnSequence;
|
|
3163
5302
|
const callId = `apply-${++applyCallSequence}`;
|
|
3164
5303
|
const position = { turnId, callId };
|
|
3165
5304
|
activeTurnId = turnId;
|
|
5305
|
+
beginAutomaticReplayBoundary();
|
|
3166
5306
|
activeSignal = signal;
|
|
3167
5307
|
activeAborts = abortReasonClassifier(signal);
|
|
3168
5308
|
activeAbortEmission = undefined;
|
|
@@ -3274,13 +5414,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3274
5414
|
// the key, so the action can never execute twice.
|
|
3275
5415
|
accepted = true;
|
|
3276
5416
|
try {
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
5417
|
+
let run;
|
|
5418
|
+
if (candidate.unresolvedEffectAction === 'abandon') {
|
|
5419
|
+
signal.throwIfAborted();
|
|
5420
|
+
run = runResultFor('unresolved-effect');
|
|
5421
|
+
}
|
|
5422
|
+
else if (candidate.unresolvedEffectAction === 'reconcile') {
|
|
5423
|
+
if (candidate.deferredRestoreOperationId !== undefined) {
|
|
5424
|
+
await restoreBoundDeferredOperation(candidate.deferredRestoreOperationId, signal);
|
|
5425
|
+
}
|
|
5426
|
+
else {
|
|
5427
|
+
// Receipt reconstruction itself belongs to the host. The
|
|
5428
|
+
// runtime may only re-read that authoritative mirror; it
|
|
5429
|
+
// never replays a player to manufacture missing evidence.
|
|
5430
|
+
refreshRetainedEffectFenceFromHost();
|
|
5431
|
+
}
|
|
5432
|
+
signal.throwIfAborted();
|
|
5433
|
+
run = runResultFor(hasUnresolvedReconciliation()
|
|
5434
|
+
? 'no-action'
|
|
5435
|
+
: 'quiescent');
|
|
5436
|
+
}
|
|
5437
|
+
else {
|
|
5438
|
+
actor.send(candidate.event);
|
|
5439
|
+
await waitForPlaybookQuiescence(actor, {
|
|
5440
|
+
pendingCalls: nestedBridge,
|
|
5441
|
+
});
|
|
5442
|
+
run = runResultFor(settledOutcome(signal));
|
|
5443
|
+
}
|
|
3281
5444
|
if (controlPlaneError !== undefined)
|
|
3282
5445
|
throw controlPlaneError;
|
|
3283
|
-
const run = runResultFor(settledOutcome(signal));
|
|
3284
5446
|
receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
|
|
3285
5447
|
? {
|
|
3286
5448
|
disposition: 'failed',
|
|
@@ -3365,6 +5527,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3365
5527
|
activeAborts = undefined;
|
|
3366
5528
|
activeAbortEmission = undefined;
|
|
3367
5529
|
activeTurnId = undefined;
|
|
5530
|
+
activeGovernedBoundarySeen = false;
|
|
5531
|
+
activeGovernedAttemptId = undefined;
|
|
5532
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
3368
5533
|
controlPlaneError = undefined;
|
|
3369
5534
|
}
|
|
3370
5535
|
// Past acceptance every settlement failure has been folded into the
|
|
@@ -3386,6 +5551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3386
5551
|
}
|
|
3387
5552
|
return receipt;
|
|
3388
5553
|
},
|
|
5554
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
3389
5555
|
async handleBossInput({ text, signal, }) {
|
|
3390
5556
|
if (!actor || !savedPorts) {
|
|
3391
5557
|
throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
|
|
@@ -3396,8 +5562,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3396
5562
|
if (activeSignal !== undefined) {
|
|
3397
5563
|
throw new Error('createPlaybookRuntime.handleBossInput: another runtime turn is active');
|
|
3398
5564
|
}
|
|
5565
|
+
assertDeferredSettlementOpen('handleBossInput');
|
|
5566
|
+
refreshRetainedEffectFenceFromHost();
|
|
3399
5567
|
const turnId = ++turnSequence;
|
|
3400
5568
|
activeTurnId = turnId;
|
|
5569
|
+
beginAutomaticReplayBoundary();
|
|
3401
5570
|
activeSignal = signal;
|
|
3402
5571
|
activeAborts = abortReasonClassifier(signal);
|
|
3403
5572
|
activeAbortEmission = undefined;
|
|
@@ -3420,11 +5589,26 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3420
5589
|
// where applicable (slc/link.md §Boss-event mapping), judge
|
|
3421
5590
|
// classification otherwise.
|
|
3422
5591
|
let event;
|
|
5592
|
+
let classifiedSnapshot;
|
|
5593
|
+
let deferredPending;
|
|
5594
|
+
let deferredOperation;
|
|
5595
|
+
let pendingRequiresDeferredBinding = false;
|
|
3423
5596
|
const trimmed = text.trim();
|
|
3424
5597
|
if (trimmed !== '') {
|
|
3425
5598
|
const snapshot = actor.getSnapshot();
|
|
5599
|
+
classifiedSnapshot = snapshot;
|
|
3426
5600
|
const terminal = snapshot.status === 'done';
|
|
3427
5601
|
const stateId = normalizePlaybookSnapshot(snapshot).stateId;
|
|
5602
|
+
const snapshotContext = (snapshot
|
|
5603
|
+
.context ?? {});
|
|
5604
|
+
deferredPending = pendingBossQuestionForState(normalizePlaybookSnapshot(snapshot), snapshotContext);
|
|
5605
|
+
pendingRequiresDeferredBinding =
|
|
5606
|
+
deferredPending !== undefined &&
|
|
5607
|
+
outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred';
|
|
5608
|
+
deferredOperation =
|
|
5609
|
+
!pendingRequiresDeferredBinding
|
|
5610
|
+
? undefined
|
|
5611
|
+
: currentBoundDeferredOperation(deferredPending);
|
|
3428
5612
|
// PBRT-1 / slc/link.md §Boss-event mapping: the idle entry, the
|
|
3429
5613
|
// recoverable failure state, and the reconstructed terminal all
|
|
3430
5614
|
// accept exactly one ordinary textual entry event, so delivered
|
|
@@ -3432,7 +5616,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3432
5616
|
// classifier whim to settle a restart as no action. Every other
|
|
3433
5617
|
// parked state — a reply wait or an authored mid-workflow
|
|
3434
5618
|
// checkpoint — classifies under its own Boss-event contracts.
|
|
3435
|
-
if (
|
|
5619
|
+
if (hasUnresolvedReconciliation()) {
|
|
5620
|
+
event = undefined;
|
|
5621
|
+
}
|
|
5622
|
+
else if (stateId === 'failed' &&
|
|
5623
|
+
!failedAttemptAllowsReplay()) {
|
|
5624
|
+
event = undefined;
|
|
5625
|
+
}
|
|
5626
|
+
else if (spec.entryEvent !== undefined &&
|
|
3436
5627
|
(stateId === 'ready' || stateId === 'failed' || terminal)) {
|
|
3437
5628
|
event = {
|
|
3438
5629
|
type: spec.entryEvent.type,
|
|
@@ -3444,9 +5635,61 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3444
5635
|
}
|
|
3445
5636
|
signal.throwIfAborted();
|
|
3446
5637
|
}
|
|
5638
|
+
if (event !== undefined &&
|
|
5639
|
+
deferredPending !== undefined &&
|
|
5640
|
+
pendingRequiresDeferredBinding &&
|
|
5641
|
+
deferredOperation === undefined &&
|
|
5642
|
+
hasGovernedPlayerStates &&
|
|
5643
|
+
deferredReconciliationOperationId === undefined) {
|
|
5644
|
+
throw new Error(`${label} pending governed Boss question has no durable logical operation`);
|
|
5645
|
+
}
|
|
5646
|
+
let handledDeferred = false;
|
|
5647
|
+
if (event !== undefined &&
|
|
5648
|
+
classifiedSnapshot !== undefined &&
|
|
5649
|
+
deferredPending !== undefined &&
|
|
5650
|
+
deferredOperation !== undefined) {
|
|
5651
|
+
if (isExactDeferredBossReply(classifiedSnapshot, event, deferredPending)) {
|
|
5652
|
+
const statusLine = classificationStatus(event);
|
|
5653
|
+
const continuation = await continueBoundDeferredOperation(deferredOperation, event, signal, turnId, statusLine);
|
|
5654
|
+
if (continuation === 'continued') {
|
|
5655
|
+
try {
|
|
5656
|
+
lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
|
|
5657
|
+
}
|
|
5658
|
+
catch {
|
|
5659
|
+
lastBossEvent = undefined;
|
|
5660
|
+
}
|
|
5661
|
+
if (controlPlaneError !== undefined) {
|
|
5662
|
+
throw controlPlaneError;
|
|
5663
|
+
}
|
|
5664
|
+
result = runResultFor(!hasUnresolvedReconciliation()
|
|
5665
|
+
? settledOutcome(signal)
|
|
5666
|
+
: 'no-action');
|
|
5667
|
+
}
|
|
5668
|
+
else {
|
|
5669
|
+
lastBossEvent = undefined;
|
|
5670
|
+
result = runResultFor('no-action');
|
|
5671
|
+
}
|
|
5672
|
+
handledDeferred = true;
|
|
5673
|
+
}
|
|
5674
|
+
else if (event.type === 'BOSS_REPLY') {
|
|
5675
|
+
// A malformed, empty, or mismatched answer does not consume
|
|
5676
|
+
// the durable wait and starts no repository or player work.
|
|
5677
|
+
event = undefined;
|
|
5678
|
+
}
|
|
5679
|
+
else {
|
|
5680
|
+
await parkBoundDeferredOperation(deferredOperation.operationId, signal);
|
|
5681
|
+
lastBossEvent = undefined;
|
|
5682
|
+
result = runResultFor('no-action');
|
|
5683
|
+
handledDeferred = true;
|
|
5684
|
+
}
|
|
5685
|
+
}
|
|
3447
5686
|
// Empty input, no-action classifier output, or invalid classifier
|
|
3448
5687
|
// output — nothing to send.
|
|
3449
|
-
if (
|
|
5688
|
+
if (handledDeferred) {
|
|
5689
|
+
// The deferred host transaction already decided whether the
|
|
5690
|
+
// authored continuation ran; never send its event a second time.
|
|
5691
|
+
}
|
|
5692
|
+
else if (event === undefined) {
|
|
3450
5693
|
result = runResultFor('no-action');
|
|
3451
5694
|
}
|
|
3452
5695
|
else {
|
|
@@ -3506,15 +5749,24 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3506
5749
|
((operationError !== undefined &&
|
|
3507
5750
|
isAbortFailure(operationError, signal)) ||
|
|
3508
5751
|
(drainAbort && operationError === undefined));
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
5752
|
+
// A deferred continuation whose actor advanced before the host's
|
|
5753
|
+
// completion write became authoritative has no safe public FSM
|
|
5754
|
+
// settlement. The durable uncertain record is the only recovery
|
|
5755
|
+
// source, so do not project the actor's advanced snapshot into a
|
|
5756
|
+
// `boss.input.settled` event.
|
|
5757
|
+
const settlementResult = deferredSettlementClosure !== undefined
|
|
5758
|
+
? undefined
|
|
5759
|
+
: primaryError === undefined
|
|
5760
|
+
? (result ?? runResultFor('no-action'))
|
|
5761
|
+
: runResultFor(abortError ? 'aborted' : 'failed', primaryError);
|
|
3512
5762
|
let settlementEmissionError;
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
5763
|
+
if (settlementResult !== undefined) {
|
|
5764
|
+
try {
|
|
5765
|
+
await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
|
|
5766
|
+
}
|
|
5767
|
+
catch (error) {
|
|
5768
|
+
settlementEmissionError = error;
|
|
5769
|
+
}
|
|
3518
5770
|
}
|
|
3519
5771
|
try {
|
|
3520
5772
|
await drainEmissions();
|
|
@@ -3536,6 +5788,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3536
5788
|
!(abortError && settlementEmissionError === undefined)) {
|
|
3537
5789
|
throw failure;
|
|
3538
5790
|
}
|
|
5791
|
+
if (settlementResult === undefined) {
|
|
5792
|
+
throw deferredSettlementClosure;
|
|
5793
|
+
}
|
|
3539
5794
|
return settlementResult;
|
|
3540
5795
|
}
|
|
3541
5796
|
finally {
|
|
@@ -3543,6 +5798,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3543
5798
|
activeAborts = undefined;
|
|
3544
5799
|
activeAbortEmission = undefined;
|
|
3545
5800
|
activeTurnId = undefined;
|
|
5801
|
+
activeGovernedBoundarySeen = false;
|
|
5802
|
+
activeGovernedAttemptId = undefined;
|
|
5803
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
3546
5804
|
controlPlaneError = undefined;
|
|
3547
5805
|
}
|
|
3548
5806
|
},
|
|
@@ -3556,7 +5814,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3556
5814
|
if (activeSignal !== undefined) {
|
|
3557
5815
|
throw new Error('createPlaybookRuntime.resumePlaybookCall: another runtime turn is active');
|
|
3558
5816
|
}
|
|
5817
|
+
refreshRetainedEffectFenceFromHost();
|
|
5818
|
+
if (hasUnresolvedReconciliation()) {
|
|
5819
|
+
return runResultFor('no-action');
|
|
5820
|
+
}
|
|
3559
5821
|
activeTurnId = playbookCallTurnIds.get(input.callId);
|
|
5822
|
+
bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
|
|
5823
|
+
? playbookCallEffectPrefixes.get(input.callId)
|
|
5824
|
+
: hasGovernedPlayerStates
|
|
5825
|
+
? 0
|
|
5826
|
+
: undefined);
|
|
3560
5827
|
activeSignal = input.signal;
|
|
3561
5828
|
activeAborts = abortReasonClassifier(input.signal);
|
|
3562
5829
|
activeAbortEmission = undefined;
|
|
@@ -3635,6 +5902,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3635
5902
|
activeAborts = undefined;
|
|
3636
5903
|
activeAbortEmission = undefined;
|
|
3637
5904
|
activeTurnId = undefined;
|
|
5905
|
+
activeGovernedBoundarySeen = false;
|
|
5906
|
+
activeGovernedAttemptId = undefined;
|
|
5907
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
3638
5908
|
controlPlaneError = undefined;
|
|
3639
5909
|
}
|
|
3640
5910
|
},
|
|
@@ -3699,6 +5969,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3699
5969
|
}
|
|
3700
5970
|
activePlayerKeys.clear();
|
|
3701
5971
|
playbookCallTurnIds.clear();
|
|
5972
|
+
playbookCallEffectPrefixes.clear();
|
|
3702
5973
|
activeEmissionCalls.clear();
|
|
3703
5974
|
emissionQueue.clear();
|
|
3704
5975
|
judgeQueue.clear();
|
|
@@ -3710,9 +5981,30 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3710
5981
|
actorSettlementErrorAborts = undefined;
|
|
3711
5982
|
activeAbortEmission = undefined;
|
|
3712
5983
|
activeTurnId = undefined;
|
|
5984
|
+
activeGovernedBoundarySeen = false;
|
|
5985
|
+
activeGovernedAttemptId = undefined;
|
|
5986
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
5987
|
+
failedGovernedAttemptUnknown = false;
|
|
5988
|
+
failedEffectBoundaryPrefix = undefined;
|
|
5989
|
+
failedGovernedAttemptId = undefined;
|
|
3713
5990
|
controlPlaneError = undefined;
|
|
3714
5991
|
emissionFailure = undefined;
|
|
3715
5992
|
lastBossEvent = undefined;
|
|
5993
|
+
retainedEffectSourceSessionId = undefined;
|
|
5994
|
+
retainedEffectReconciliation = undefined;
|
|
5995
|
+
retainedEffectReconciliationRequired = false;
|
|
5996
|
+
reconstructedGovernedDelivery = undefined;
|
|
5997
|
+
reconstructedGovernedPrefixSequence = undefined;
|
|
5998
|
+
reconstructedAcceptancePending = undefined;
|
|
5999
|
+
governedSettlementsByBoundaryId.clear();
|
|
6000
|
+
governedCompletionEvidenceByBoundaryId.clear();
|
|
6001
|
+
unresolvedSemanticBoundaryIds.clear();
|
|
6002
|
+
deferredReconciliationOperationId = undefined;
|
|
6003
|
+
deferredSettlementClosure = undefined;
|
|
6004
|
+
expectedBoundPendingQuestion = undefined;
|
|
6005
|
+
activeDeferredContinuation = undefined;
|
|
6006
|
+
deferInspectionEmissions = false;
|
|
6007
|
+
deferredInspectionEmissions = [];
|
|
3716
6008
|
savedPorts = undefined;
|
|
3717
6009
|
runtimePorts = undefined;
|
|
3718
6010
|
session = undefined;
|
|
@@ -3742,4 +6034,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3742
6034
|
};
|
|
3743
6035
|
return runtime;
|
|
3744
6036
|
};
|
|
6037
|
+
Object.defineProperty(createPlaybookRuntime, 'compat', {
|
|
6038
|
+
value: Object.freeze({ artifactSchema, runtimeAbi: RUNTIME_ABI }),
|
|
6039
|
+
enumerable: true,
|
|
6040
|
+
writable: false,
|
|
6041
|
+
configurable: false,
|
|
6042
|
+
});
|
|
6043
|
+
return createPlaybookRuntime;
|
|
3745
6044
|
}
|