@sublang/playbook 8.0.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/docs/cli.md +66 -23
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +45 -14
- package/package.json +7 -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 +151 -10
- package/reference/sdlc/captain.playbook/captain.playbook.ts +200 -14
- package/reference/sdlc/code.md +0 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +170 -17
- package/reference/sdlc/code.playbook/bin/launch-config.js +136 -4
- package/reference/sdlc/code.playbook/bin/playbook.js +81 -4
- package/reference/sdlc/code.playbook/bin/repository-effects.js +2930 -0
- package/reference/sdlc/code.playbook/bin/run.js +365 -63
- package/reference/sdlc/code.playbook/bin/session-store.js +2877 -209
- package/reference/sdlc/code.playbook/code.fsm.d.ts +11 -1
- package/reference/sdlc/code.playbook/code.fsm.js +85 -29
- package/reference/sdlc/code.playbook/code.fsm.ts +95 -33
- 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 +67 -4
- package/reference/sdlc/code.playbook/code.playbook.ts +87 -8
- 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 +99 -7
- package/reference/sdlc/code.playbook/playbook-captain.js +1894 -82
- package/reference/sdlc/code.playbook/playbook-captain.ts +2809 -109
- package/reference/sdlc/decide.md +0 -1
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +8 -1
- 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 +15 -5
- package/reference/sdlc/decide.playbook/decide.playbook.js +1994 -191
- package/reference/sdlc/decide.playbook/decide.playbook.ts +3209 -404
- 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.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 +78 -4
- package/reference/sdlc/review.playbook/review.playbook.ts +95 -8
- 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 +25 -7
- package/slc/link.md +727 -82
- 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 +165 -3
- package/src/runtime.ts +214 -2
- package/src/xstate-playbook-runtime.d.ts +162 -13
- package/src/xstate-playbook-runtime.js +3344 -564
- package/src/xstate-playbook-runtime.ts +4873 -637
- package/src/xstate-runtime.d.ts +76 -8
- package/src/xstate-runtime.js +1001 -64
- package/src/xstate-runtime.ts +1640 -91
|
@@ -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).
|
|
@@ -233,9 +327,37 @@ export function normalizeErrorFull(err) {
|
|
|
233
327
|
return undefined;
|
|
234
328
|
return normalizeError(err);
|
|
235
329
|
}
|
|
330
|
+
// slc/link.md §Abort: cancellation is causal identity with the applicable
|
|
331
|
+
// signal's reason — never an `AbortError` name, never bare signal state. A
|
|
332
|
+
// distinct failure observed while the signal is aborted stays a non-abort
|
|
333
|
+
// control error and takes precedence (mirrors DECIDE's bespoke reference).
|
|
236
334
|
function isAbortFailure(error, signal) {
|
|
237
|
-
return
|
|
238
|
-
|
|
335
|
+
return signal.aborted && Object.is(error, signal.reason);
|
|
336
|
+
}
|
|
337
|
+
function abortReasonClassifier(...sources) {
|
|
338
|
+
const captured = sources.filter((source) => source !== undefined);
|
|
339
|
+
return Object.freeze({
|
|
340
|
+
isAbortReason: (error) => captured.some((source) => source instanceof AbortSignal
|
|
341
|
+
? isAbortFailure(error, source)
|
|
342
|
+
: source.isAbortReason(error)),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* gears2fsm's canonical Boss-reply wait state. On the runtime's Boss-facing
|
|
347
|
+
* surfaces — state telemetry, status lines, the exported snapshot, and the
|
|
348
|
+
* control view — a context question counts as *pending* only while the
|
|
349
|
+
* machine sits in this state awaiting the reply. Later states retain the
|
|
350
|
+
* answered question in context (the resumed player prompt is composed from
|
|
351
|
+
* it), so an unconditional projection would resurrect it: a failure the
|
|
352
|
+
* resumed player reached would export a question nobody is waiting on,
|
|
353
|
+
* disagreeing with the gated telemetry a mirroring host's ledger follows
|
|
354
|
+
* and failing the shell's snapshot-equality settlement check.
|
|
355
|
+
*/
|
|
356
|
+
const BOSS_REPLY_WAIT_STATE_ID = 'awaitBossReply';
|
|
357
|
+
function pendingBossQuestionForState(state, context) {
|
|
358
|
+
if (state.stateId !== BOSS_REPLY_WAIT_STATE_ID)
|
|
359
|
+
return undefined;
|
|
360
|
+
return pendingBossQuestionFromContext(context);
|
|
239
361
|
}
|
|
240
362
|
/** Read the FSM context's single pending Boss question, when well-formed. */
|
|
241
363
|
export function pendingBossQuestionFromContext(context) {
|
|
@@ -333,6 +455,97 @@ function sortJson(value) {
|
|
|
333
455
|
function stableJson(value, path) {
|
|
334
456
|
return JSON.stringify(sortJson(snapshotJsonValue(value, path)));
|
|
335
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
|
+
}
|
|
336
549
|
/**
|
|
337
550
|
* Default direct-Captain prompt composer (slc/link.md §Captain prompt
|
|
338
551
|
* composition). Placeholder substitution is presence-based: string fields
|
|
@@ -402,6 +615,43 @@ export function defaultBuildJudgePrompt(input, finalText) {
|
|
|
402
615
|
}
|
|
403
616
|
return lines.join('\n');
|
|
404
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
|
+
}
|
|
405
655
|
const NO_VERBATIM_FIELDS = new Set();
|
|
406
656
|
/**
|
|
407
657
|
* LLM-judge adjudicator for delegated players. Coerces the player's
|
|
@@ -434,7 +684,12 @@ export async function adjudicatePlayerOutput(spec, input, finalText, ports, sign
|
|
|
434
684
|
const verbatim = finalText.trim();
|
|
435
685
|
for (const field of extractFields(input.result[guard])) {
|
|
436
686
|
if (verbatimFields.has(field)) {
|
|
437
|
-
obj
|
|
687
|
+
Object.defineProperty(obj, field, {
|
|
688
|
+
value: verbatim,
|
|
689
|
+
enumerable: true,
|
|
690
|
+
configurable: true,
|
|
691
|
+
writable: true,
|
|
692
|
+
});
|
|
438
693
|
continue;
|
|
439
694
|
}
|
|
440
695
|
if (typeof obj[field] !== 'string') {
|
|
@@ -462,18 +717,23 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
462
717
|
let roleId;
|
|
463
718
|
let prompt;
|
|
464
719
|
try {
|
|
720
|
+
spec.validateInput?.(input);
|
|
465
721
|
roleId = spec.resolveRoleId(input);
|
|
466
722
|
prompt = spec.composePlayerPrompt(input);
|
|
467
723
|
}
|
|
468
724
|
catch (error) {
|
|
469
|
-
|
|
725
|
+
if (!isAbortFailure(error, activeSignal)) {
|
|
726
|
+
onControlPlaneError?.(error);
|
|
727
|
+
}
|
|
470
728
|
throw error;
|
|
471
729
|
}
|
|
472
730
|
const callPlayer = (resume) => boundary
|
|
473
731
|
? boundary.callPlayer(input, roleId, prompt, activeSignal)
|
|
474
732
|
: ports.callPlayer(roleId, prompt, activeSignal, { resume });
|
|
475
733
|
let result = await callPlayer(false);
|
|
476
|
-
if (result.status === 'ok' &&
|
|
734
|
+
if (result.status === 'ok' &&
|
|
735
|
+
isEmptyFinalText(result.finalText) &&
|
|
736
|
+
(spec.allowsCorrectiveReplay?.(result) ?? true)) {
|
|
477
737
|
// An abort that lands between the empty first result and the
|
|
478
738
|
// corrective call ends the turn as ordinary abort settlement with
|
|
479
739
|
// no second host call — aborts are never retried (DR-028 via
|
|
@@ -501,12 +761,22 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
501
761
|
throw new Error('captainBridge: callPlayer returned status=ok with no finalText');
|
|
502
762
|
}
|
|
503
763
|
try {
|
|
504
|
-
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);
|
|
505
772
|
validateBossReplyOutput(input, output, spec.resumableStateIds);
|
|
506
773
|
return output;
|
|
507
774
|
}
|
|
508
775
|
catch (error) {
|
|
509
|
-
|
|
776
|
+
if (!isAbortFailure(error, activeSignal) &&
|
|
777
|
+
!isFsmResultFailure(error)) {
|
|
778
|
+
onControlPlaneError?.(error);
|
|
779
|
+
}
|
|
510
780
|
throw error;
|
|
511
781
|
}
|
|
512
782
|
});
|
|
@@ -670,7 +940,7 @@ export function resumableStateIdsFromMachine(machine) {
|
|
|
670
940
|
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
671
941
|
return new Set();
|
|
672
942
|
}
|
|
673
|
-
const awaitState = config.states
|
|
943
|
+
const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
|
|
674
944
|
if (!isPlainObject(awaitState) || !isPlainObject(awaitState.on)) {
|
|
675
945
|
return new Set();
|
|
676
946
|
}
|
|
@@ -772,6 +1042,28 @@ function deepFreeze(value) {
|
|
|
772
1042
|
// Default transition/status derivation.
|
|
773
1043
|
// ---------------------------------------------------------------------------
|
|
774
1044
|
const SUPPRESSED_ENTRY_STATES = new Set(['ready', 'done']);
|
|
1045
|
+
// Bounded escalation for aborted script process groups: SIGTERM first, then
|
|
1046
|
+
// SIGKILL after this grace, so settlement (gated on the shell's own exit)
|
|
1047
|
+
// stays bounded even for TERM-immune commands.
|
|
1048
|
+
const SCRIPT_ABORT_KILL_GRACE_MS = 2000;
|
|
1049
|
+
class ScriptProcessGroupTeardownError extends Error {
|
|
1050
|
+
constructor(pid, message, cause) {
|
|
1051
|
+
super(`script process group ${pid} teardown could not be confirmed: ${message}`, cause === undefined ? undefined : { cause });
|
|
1052
|
+
this.name = 'ScriptProcessGroupTeardownError';
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
function isNoSuchProcess(error) {
|
|
1056
|
+
return (typeof error === 'object' &&
|
|
1057
|
+
error !== null &&
|
|
1058
|
+
'code' in error &&
|
|
1059
|
+
error.code === 'ESRCH');
|
|
1060
|
+
}
|
|
1061
|
+
function isProcessPermissionDenied(error) {
|
|
1062
|
+
return (typeof error === 'object' &&
|
|
1063
|
+
error !== null &&
|
|
1064
|
+
'code' in error &&
|
|
1065
|
+
error.code === 'EPERM');
|
|
1066
|
+
}
|
|
775
1067
|
function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
|
|
776
1068
|
return (event) => {
|
|
777
1069
|
if (event === null || typeof event !== 'object') {
|
|
@@ -796,7 +1088,7 @@ function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
|
|
|
796
1088
|
}
|
|
797
1089
|
function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
|
|
798
1090
|
if (value === undefined) {
|
|
799
|
-
throw new TypeError(`${label} roleStates must be supplied for schema
|
|
1091
|
+
throw new TypeError(`${label} roleStates must be supplied for schema 3`);
|
|
800
1092
|
}
|
|
801
1093
|
const captured = snapshotJsonValue(value, `${label} roleStates`);
|
|
802
1094
|
if (!isPlainObject(captured)) {
|
|
@@ -840,28 +1132,194 @@ function snapshotRoleStateStatuses(value, label, machine, stateDescriptions) {
|
|
|
840
1132
|
}
|
|
841
1133
|
return statuses;
|
|
842
1134
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
+
}
|
|
850
1311
|
}
|
|
851
1312
|
function askerLabel(asker) {
|
|
852
1313
|
return asker.kind === 'captain' ? 'Captain' : asker.roleId;
|
|
853
1314
|
}
|
|
854
1315
|
function makeDefaultStatusesForState(roleStates) {
|
|
855
|
-
return (state, context
|
|
1316
|
+
return (state, context) => {
|
|
856
1317
|
const statuses = [];
|
|
857
|
-
const guard = settlingGuard(event);
|
|
858
|
-
if (guard !== undefined)
|
|
859
|
-
statuses.push({ message: `→ ${guard}` });
|
|
860
1318
|
const stateId = state.stateId;
|
|
861
1319
|
if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) {
|
|
862
1320
|
return statuses;
|
|
863
1321
|
}
|
|
864
|
-
if (stateId ===
|
|
1322
|
+
if (stateId === BOSS_REPLY_WAIT_STATE_ID) {
|
|
865
1323
|
const pending = pendingBossQuestionFromContext(context);
|
|
866
1324
|
if (pending === undefined) {
|
|
867
1325
|
return [...statuses, { message: 'Awaiting Boss reply.' }];
|
|
@@ -1063,7 +1521,14 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
1063
1521
|
const state = classifierState(snapshotOrState);
|
|
1064
1522
|
const stateId = typeof state.value === 'string' ? state.value : undefined;
|
|
1065
1523
|
const currentState = stateId ?? JSON.stringify(state.value ?? null);
|
|
1066
|
-
|
|
1524
|
+
// The classifier shares the reply-wait pendingness of every other
|
|
1525
|
+
// surface: outside the wait, a context question a later state retains
|
|
1526
|
+
// is answered history, so the prompt must not present it as pending —
|
|
1527
|
+
// a judge told a question awaits at the failure state is steered toward
|
|
1528
|
+
// a reply it cannot select or toward no action at all.
|
|
1529
|
+
const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
1530
|
+
? pendingBossQuestionFromContext(state.context)
|
|
1531
|
+
: undefined;
|
|
1067
1532
|
const configuredTypes = configuredEventTypesForState(machine, stateId);
|
|
1068
1533
|
const applicable = [...contracts.values()].filter((contract) => configuredTypes.has(contract.type) &&
|
|
1069
1534
|
(contract.type !== 'BOSS_REPLY' || pending !== undefined));
|
|
@@ -1179,32 +1644,114 @@ function machineDeclaresParallelState(machine) {
|
|
|
1179
1644
|
};
|
|
1180
1645
|
return visit(machine.config);
|
|
1181
1646
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1647
|
+
// PBRT-52: the factory's domain is FLAT single-region machines — every
|
|
1648
|
+
// state a direct child of the root, so each snapshot exposes exactly one
|
|
1649
|
+
// playbook state id and every state-keyed lookup (deterministic entries,
|
|
1650
|
+
// retry, reply-wait pendingness, configured events, descriptions) indexes
|
|
1651
|
+
// one unambiguous identity. A compound child would be accepted and then
|
|
1652
|
+
// silently misbehave on all of those gates, so it is rejected up front
|
|
1653
|
+
// exactly like a parallel region.
|
|
1654
|
+
function machineDeclaresNestedState(machine) {
|
|
1655
|
+
const config = machine.config;
|
|
1656
|
+
if (!isPlainObject(config) || !isPlainObject(config.states))
|
|
1657
|
+
return false;
|
|
1658
|
+
return Object.values(config.states).some((stateDef) => isPlainObject(stateDef) &&
|
|
1659
|
+
isPlainObject(stateDef.states) &&
|
|
1660
|
+
Object.keys(stateDef.states).length > 0);
|
|
1661
|
+
}
|
|
1662
|
+
// PBRT-52: the factory's lookups index states by their root key, and the
|
|
1663
|
+
// published playbook identity is `meta.playbook.stateId` — the two must
|
|
1664
|
+
// coincide or a machine can advertise a pending question or retry under an
|
|
1665
|
+
// identity no lookup resolves. A state with no string stateId is just as
|
|
1666
|
+
// dead: every snapshot identity derives from that member, so the first
|
|
1667
|
+
// entry would fail the exactly-one-state-id inspection at runtime.
|
|
1668
|
+
// gears2fsm keeps identity and key equal by construction; a hand-authored
|
|
1669
|
+
// artifact that splits or omits them fails here instead of at a silently
|
|
1670
|
+
// dead gate.
|
|
1671
|
+
function assertFlatStateIdentity(machine, label) {
|
|
1672
|
+
const config = machine.config;
|
|
1673
|
+
const states = isPlainObject(config) && isPlainObject(config.states)
|
|
1674
|
+
? config.states
|
|
1675
|
+
: undefined;
|
|
1676
|
+
// A machine with no root states has no playbook identity to expose; its
|
|
1677
|
+
// first snapshot would fail the exactly-one-state-id inspection, so it
|
|
1678
|
+
// fails construction with the defect named instead.
|
|
1679
|
+
if (states === undefined || Object.keys(states).length === 0) {
|
|
1680
|
+
throw new Error(`${label} declares no root states; the shared runtime requires at ` +
|
|
1681
|
+
'least one flat playbook state');
|
|
1682
|
+
}
|
|
1683
|
+
for (const [key, stateDef] of Object.entries(states)) {
|
|
1684
|
+
if (!isPlainObject(stateDef))
|
|
1685
|
+
continue;
|
|
1686
|
+
const meta = isPlainObject(stateDef.meta) ? stateDef.meta : undefined;
|
|
1687
|
+
const playbook = meta !== undefined && isPlainObject(meta.playbook)
|
|
1688
|
+
? meta.playbook
|
|
1689
|
+
: undefined;
|
|
1690
|
+
const stateId = playbook?.stateId;
|
|
1691
|
+
if (typeof stateId !== 'string') {
|
|
1692
|
+
throw new Error(`${label} state ${key} declares no string meta.playbook.stateId; ` +
|
|
1693
|
+
'the shared runtime derives every playbook state identity from it');
|
|
1694
|
+
}
|
|
1695
|
+
if (stateId !== key) {
|
|
1696
|
+
throw new Error(`${label} state ${key} declares meta.playbook.stateId ${stateId}; ` +
|
|
1697
|
+
'the shared runtime requires the playbook state id to equal the state key');
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
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
|
+
}
|
|
1193
1728
|
export function createXStatePlaybookRuntime(machine, spec) {
|
|
1194
1729
|
const label = spec.label ?? 'playbook';
|
|
1195
1730
|
// DR-022 / PBRT-50: reject an incompatible artifact declaration before any
|
|
1196
1731
|
// machine interpretation, against this loaded engine's own self-report.
|
|
1197
|
-
assertRuntimeCompat(spec.compat, label);
|
|
1732
|
+
const artifactSchema = assertRuntimeCompat(spec.compat, label);
|
|
1198
1733
|
const specDescriptors = Object.getOwnPropertyDescriptors(spec);
|
|
1199
1734
|
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'playerStates')) {
|
|
1200
|
-
throw new TypeError(`${label}
|
|
1735
|
+
throw new TypeError(`${label} artifacts must supply roleStates, not playerStates`);
|
|
1201
1736
|
}
|
|
1202
1737
|
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
|
|
1203
|
-
throw new TypeError(`${label}
|
|
1738
|
+
throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
|
|
1204
1739
|
}
|
|
1205
1740
|
if (machineDeclaresParallelState(machine)) {
|
|
1206
1741
|
throw new Error(`${label} uses a parallel state; the shared runtime supports only single-region FSMs`);
|
|
1207
1742
|
}
|
|
1743
|
+
if (machineDeclaresNestedState(machine)) {
|
|
1744
|
+
throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
|
|
1745
|
+
}
|
|
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
|
+
});
|
|
1208
1755
|
const declaredActors = collectInvokeSources(machine);
|
|
1209
1756
|
const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
|
|
1210
1757
|
// DR-029: source state descriptions label the control actions the
|
|
@@ -1237,18 +1784,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1237
1784
|
((input) => defaultComposePlayerPrompt(input, spec.placeholderFields));
|
|
1238
1785
|
const composeCaptainPrompt = spec.composeCaptainPrompt ??
|
|
1239
1786
|
((input) => defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
1787
|
+
const extractFields = spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
1788
|
+
const verbatimPayloadFields = new Set(spec.verbatimPayloadFields ?? NO_VERBATIM_FIELDS);
|
|
1240
1789
|
const adjudication = {
|
|
1241
1790
|
...(spec.buildJudgePrompt !== undefined
|
|
1242
1791
|
? { buildJudgePrompt: spec.buildJudgePrompt }
|
|
1243
1792
|
: {}),
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
: {}),
|
|
1247
|
-
...(spec.verbatimPayloadFields !== undefined
|
|
1248
|
-
? { verbatimPayloadFields: spec.verbatimPayloadFields }
|
|
1249
|
-
: {}),
|
|
1793
|
+
extractRequiredFields: extractFields,
|
|
1794
|
+
verbatimPayloadFields,
|
|
1250
1795
|
};
|
|
1251
|
-
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
|
+
}
|
|
1252
1803
|
// Build the derived classifier unconditionally: it is the sole validator of
|
|
1253
1804
|
// supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
|
|
1254
1805
|
// fail factory construction whether or not this spec overrides the
|
|
@@ -1259,6 +1810,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1259
1810
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
1260
1811
|
const statusesForState = spec.statusesForState ??
|
|
1261
1812
|
makeDefaultStatusesForState(roleStates);
|
|
1813
|
+
const usesDefaultStatuses = spec.statusesForState === undefined;
|
|
1262
1814
|
const classificationStatus = spec.classificationStatus ??
|
|
1263
1815
|
((event) => event.type);
|
|
1264
1816
|
const machineInput = spec.machineInput ?? ((options) => options);
|
|
@@ -1267,8 +1819,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1267
1819
|
const cwd = options?.cwd;
|
|
1268
1820
|
return typeof cwd === 'string' ? cwd : undefined;
|
|
1269
1821
|
});
|
|
1270
|
-
|
|
1271
|
-
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);
|
|
1272
1855
|
const boundScriptCwd = scriptCwd(boundOptions);
|
|
1273
1856
|
let actor;
|
|
1274
1857
|
let session;
|
|
@@ -1282,7 +1865,37 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1282
1865
|
// ports.callPlayer / callCaptain / callJudge see the right cancellation
|
|
1283
1866
|
// source. undefined between turns; set by the public boundaries.
|
|
1284
1867
|
let activeSignal;
|
|
1868
|
+
// Immutable cancellation provenance for the active public boundary. A
|
|
1869
|
+
// nested resume widens it to include both invocation and resume signals;
|
|
1870
|
+
// mutable `activeSignal` alone cannot classify a late invocation reason.
|
|
1871
|
+
let activeAborts;
|
|
1872
|
+
// The bridge binds the provenance of a child result immediately before
|
|
1873
|
+
// its promise actor settles. The next root snapshot/error consumes this
|
|
1874
|
+
// one-shot so background settlement emissions retain their owner.
|
|
1875
|
+
let actorSettlementAborts;
|
|
1876
|
+
let actorSettlementErrorAborts;
|
|
1877
|
+
// Exact cancellation observed by an emission owned by the active
|
|
1878
|
+
// boundary. Ordinary runs settle from their signal/state; apply also
|
|
1879
|
+
// needs this phase-local evidence to fold a pre-publication failure into
|
|
1880
|
+
// its accepted receipt.
|
|
1881
|
+
let activeAbortEmission;
|
|
1285
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 = [];
|
|
1286
1899
|
let controlPlaneError;
|
|
1287
1900
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
1288
1901
|
// status emitter. undefined before the first inspect firing.
|
|
@@ -1310,6 +1923,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1310
1923
|
const privateResumeTokens = new Map();
|
|
1311
1924
|
const activePlayerKeys = new Set();
|
|
1312
1925
|
const playbookCallTurnIds = new Map();
|
|
1926
|
+
const playbookCallEffectPrefixes = new Map();
|
|
1313
1927
|
// Captain and judge work share one serialized lane (slc/link.md
|
|
1314
1928
|
// §Session lifecycle).
|
|
1315
1929
|
const judgeQueue = new PQueue({ concurrency: 1 });
|
|
@@ -1319,99 +1933,520 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1319
1933
|
// Inspection callbacks enqueue a complete ordered batch synchronously;
|
|
1320
1934
|
// imperative boundaries await their queued work directly.
|
|
1321
1935
|
let emissionFailure;
|
|
1322
|
-
function
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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;
|
|
1334
1959
|
}
|
|
1335
|
-
return bound;
|
|
1336
1960
|
}
|
|
1337
|
-
function
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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;
|
|
1343
1987
|
}
|
|
1344
|
-
return roleId;
|
|
1345
1988
|
}
|
|
1346
|
-
function
|
|
1347
|
-
|
|
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;
|
|
1348
1997
|
}
|
|
1349
|
-
function
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1998
|
+
function runtimeBoundaryIsOwned(boundary) {
|
|
1999
|
+
if (session === undefined || boundary.playbookId !== session.playbookId) {
|
|
2000
|
+
return false;
|
|
1352
2001
|
}
|
|
1353
|
-
return session
|
|
2002
|
+
return (boundary.runtimeSessionId === session.sessionId ||
|
|
2003
|
+
boundary.runtimeSessionId === retainedEffectSourceSessionId);
|
|
1354
2004
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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;
|
|
1360
2019
|
}
|
|
1361
|
-
return promptIdentity(roleId);
|
|
1362
|
-
};
|
|
1363
|
-
try {
|
|
1364
|
-
return composePlayerPrompt(input, lookup);
|
|
1365
2020
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
2021
|
+
const expectedDispositions = [
|
|
2022
|
+
...new Set(Object.values(outcomes).map(({ repositoryDisposition }) => repositoryDisposition)),
|
|
2023
|
+
];
|
|
2024
|
+
if (!sameStringSet(candidate.dispositions, expectedDispositions)) {
|
|
2025
|
+
return undefined;
|
|
1368
2026
|
}
|
|
2027
|
+
return outcomes;
|
|
1369
2028
|
}
|
|
1370
|
-
function
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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;
|
|
1378
2047
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
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;
|
|
1383
2058
|
}
|
|
1384
|
-
byKey.set(key, token);
|
|
1385
|
-
}
|
|
1386
|
-
const rolesByKey = new Map();
|
|
1387
|
-
for (const roleId of declaredRoleIds) {
|
|
1388
|
-
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
1389
|
-
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
1390
2059
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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;
|
|
1397
2070
|
}
|
|
2071
|
+
return {
|
|
2072
|
+
reconciliation,
|
|
2073
|
+
historicalDeferred,
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
catch {
|
|
2077
|
+
return undefined;
|
|
1398
2078
|
}
|
|
1399
|
-
return byKey;
|
|
1400
2079
|
}
|
|
1401
|
-
function
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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;
|
|
1409
2097
|
}
|
|
1410
|
-
|
|
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';
|
|
1411
2110
|
}
|
|
1412
|
-
function
|
|
1413
|
-
|
|
1414
|
-
if (
|
|
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
|
+
}
|
|
2357
|
+
function bindSession(nextSession) {
|
|
2358
|
+
const bound = snapshotPlaybookSession(nextSession);
|
|
2359
|
+
if (bound.roleBindings === undefined)
|
|
2360
|
+
return bound;
|
|
2361
|
+
const actual = Object.keys(bound.roleBindings).sort();
|
|
2362
|
+
const expected = [...declaredRoleIds].sort();
|
|
2363
|
+
const missing = expected.filter((roleId) => !actual.includes(roleId));
|
|
2364
|
+
const extra = actual.filter((roleId) => !expected.includes(roleId));
|
|
2365
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
2366
|
+
throw new TypeError(`${label} session roleBindings must cover exactly [${expected.join(', ')}]` +
|
|
2367
|
+
`${missing.length === 0 ? '' : `; missing [${missing.join(', ')}]`}` +
|
|
2368
|
+
`${extra.length === 0 ? '' : `; extra [${extra.join(', ')}]`}`);
|
|
2369
|
+
}
|
|
2370
|
+
return bound;
|
|
2371
|
+
}
|
|
2372
|
+
function requireRoleId(input) {
|
|
2373
|
+
const roleId = input.role;
|
|
2374
|
+
if (typeof roleId !== 'string' ||
|
|
2375
|
+
roleId.trim().length === 0 ||
|
|
2376
|
+
!declaredRoleIds.includes(roleId)) {
|
|
2377
|
+
throw new TypeError(`${label} player input role must name a declared local role`);
|
|
2378
|
+
}
|
|
2379
|
+
return roleId;
|
|
2380
|
+
}
|
|
2381
|
+
function resolvedPlayerId(roleId) {
|
|
2382
|
+
return session?.roleBindings?.[roleId]?.playerId;
|
|
2383
|
+
}
|
|
2384
|
+
function promptIdentity(roleId) {
|
|
2385
|
+
if (!declaredRoleIds.includes(roleId)) {
|
|
2386
|
+
throw new TypeError(`${label} prompt identity lookup rejected undeclared role ${roleId}`);
|
|
2387
|
+
}
|
|
2388
|
+
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
2389
|
+
}
|
|
2390
|
+
function composeBoundPlayerPrompt(input) {
|
|
2391
|
+
let active = true;
|
|
2392
|
+
const lookup = (roleId) => {
|
|
2393
|
+
if (!active) {
|
|
2394
|
+
throw new Error(`${label} prompt identity lookup is no longer active`);
|
|
2395
|
+
}
|
|
2396
|
+
return promptIdentity(roleId);
|
|
2397
|
+
};
|
|
2398
|
+
try {
|
|
2399
|
+
return composePlayerPrompt(input, lookup);
|
|
2400
|
+
}
|
|
2401
|
+
finally {
|
|
2402
|
+
active = false;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
function continuationKey(roleId, playerId) {
|
|
2406
|
+
return playerId ?? roleId;
|
|
2407
|
+
}
|
|
2408
|
+
function roleTokensByContinuationKey(tokens) {
|
|
2409
|
+
const byKey = new Map();
|
|
2410
|
+
for (const [roleId, token] of Object.entries(tokens)) {
|
|
2411
|
+
if (!declaredRoleIds.includes(roleId)) {
|
|
2412
|
+
throw new TypeError(`runtime role tokens contain unknown role ${roleId}`);
|
|
2413
|
+
}
|
|
2414
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
2415
|
+
const existing = byKey.get(key);
|
|
2416
|
+
if (existing !== undefined && existing !== token) {
|
|
2417
|
+
throw new TypeError(`runtime snapshot assigns conflicting tokens to roles bound to player ${key}`);
|
|
2418
|
+
}
|
|
2419
|
+
byKey.set(key, token);
|
|
2420
|
+
}
|
|
2421
|
+
const rolesByKey = new Map();
|
|
2422
|
+
for (const roleId of declaredRoleIds) {
|
|
2423
|
+
const key = continuationKey(roleId, resolvedPlayerId(roleId));
|
|
2424
|
+
rolesByKey.set(key, [...(rolesByKey.get(key) ?? []), roleId]);
|
|
2425
|
+
}
|
|
2426
|
+
for (const [key, roles] of rolesByKey) {
|
|
2427
|
+
if (roles.length < 2)
|
|
2428
|
+
continue;
|
|
2429
|
+
const present = roles.filter((roleId) => tokens[roleId] !== undefined);
|
|
2430
|
+
if (present.length !== 0 && present.length !== roles.length) {
|
|
2431
|
+
throw new TypeError(`runtime role tokens must project player ${key} through every aliased role [${roles.join(', ')}]`);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
return byKey;
|
|
2435
|
+
}
|
|
2436
|
+
function selectPlayerResume(roleId, playerId) {
|
|
2437
|
+
const key = continuationKey(roleId, playerId);
|
|
2438
|
+
const selected = session?.playerSessions
|
|
2439
|
+
? session.playerSessions.select(roleId)
|
|
2440
|
+
: privateResumeTokens.get(key) ?? false;
|
|
2441
|
+
if (selected !== false &&
|
|
2442
|
+
(typeof selected !== 'string' || selected.trim().length === 0)) {
|
|
2443
|
+
throw new TypeError(`player session store returned an invalid resume token for role ${roleId}`);
|
|
2444
|
+
}
|
|
2445
|
+
return selected;
|
|
2446
|
+
}
|
|
2447
|
+
function updatePlayerResume(roleId, playerId, result) {
|
|
2448
|
+
const resumeToken = result.resumeToken;
|
|
2449
|
+
if (resumeToken === undefined && result.status !== 'ok')
|
|
1415
2450
|
return;
|
|
1416
2451
|
const key = continuationKey(roleId, playerId);
|
|
1417
2452
|
if (session?.playerSessions) {
|
|
@@ -1457,16 +2492,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1457
2492
|
for (const [key, token] of byKey)
|
|
1458
2493
|
privateResumeTokens.set(key, token);
|
|
1459
2494
|
}
|
|
1460
|
-
function enqueueEmission(fn) {
|
|
2495
|
+
function enqueueEmission(fn, aborts = activeAborts) {
|
|
2496
|
+
// The emission belongs to the boundary enqueueing it: a rejection
|
|
2497
|
+
// causally identical to that boundary's abort reason is the
|
|
2498
|
+
// cancellation's own evidence — never latched, so it cannot poison a
|
|
2499
|
+
// later unrelated boundary (DR-036).
|
|
2500
|
+
const enqueueAborts = aborts;
|
|
1461
2501
|
const queued = emissionQueue.add(fn).then(() => undefined);
|
|
1462
2502
|
activeEmissionCalls.add(queued);
|
|
1463
2503
|
void queued.then(() => activeEmissionCalls.delete(queued), (error) => {
|
|
1464
2504
|
activeEmissionCalls.delete(queued);
|
|
1465
|
-
|
|
2505
|
+
if (enqueueAborts?.isAbortReason(error)) {
|
|
2506
|
+
// Record evidence only when it also belongs to the public
|
|
2507
|
+
// boundary that is still active. A background A cancellation
|
|
2508
|
+
// racing an unrelated B boundary is forgiven under A and must
|
|
2509
|
+
// not change B's settlement.
|
|
2510
|
+
if (activeAborts?.isAbortReason(error)) {
|
|
2511
|
+
activeAbortEmission ??= error;
|
|
2512
|
+
}
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
emissionFailure ??= { error };
|
|
1466
2516
|
});
|
|
1467
2517
|
return queued;
|
|
1468
2518
|
}
|
|
1469
|
-
async function drainEmissions() {
|
|
2519
|
+
async function drainEmissions(_aborts = activeAborts) {
|
|
1470
2520
|
while (true) {
|
|
1471
2521
|
const active = [...activeEmissionCalls];
|
|
1472
2522
|
if (active.length > 0)
|
|
@@ -1479,8 +2529,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1479
2529
|
}
|
|
1480
2530
|
}
|
|
1481
2531
|
if (emissionFailure !== undefined) {
|
|
1482
|
-
const error = emissionFailure;
|
|
2532
|
+
const { error } = emissionFailure;
|
|
1483
2533
|
emissionFailure = undefined;
|
|
2534
|
+
// The failure was classified as distinct by its enqueue owner. If a
|
|
2535
|
+
// later public boundary drains it, retain that classification in the
|
|
2536
|
+
// boundary latch before throwing; its signal must not reinterpret
|
|
2537
|
+
// the same object as cancellation (DR-036 decision 2).
|
|
2538
|
+
if (activeSignal !== undefined)
|
|
2539
|
+
controlPlaneError ??= error;
|
|
1484
2540
|
throw error;
|
|
1485
2541
|
}
|
|
1486
2542
|
}
|
|
@@ -1500,7 +2556,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1500
2556
|
const currentSession = requireSession();
|
|
1501
2557
|
const safePayload = snapshotJsonValue(payload, `trace ${type} payload`);
|
|
1502
2558
|
return {
|
|
1503
|
-
schemaVersion:
|
|
2559
|
+
schemaVersion: 4,
|
|
1504
2560
|
sessionId: currentSession.sessionId,
|
|
1505
2561
|
playbookId: currentSession.playbookId,
|
|
1506
2562
|
rootSessionId: currentSession.rootSessionId,
|
|
@@ -1519,13 +2575,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1519
2575
|
payload: safePayload,
|
|
1520
2576
|
};
|
|
1521
2577
|
}
|
|
1522
|
-
function emitTrace(type, payload, position = {}) {
|
|
2578
|
+
function emitTrace(type, payload, position = {}, aborts) {
|
|
1523
2579
|
const currentSession = requireSession();
|
|
1524
2580
|
const event = createTraceEvent(type, payload, position);
|
|
1525
2581
|
return enqueueEmission(() => currentSession.ports.emitTelemetry({
|
|
1526
2582
|
topic: 'playbook.trace',
|
|
1527
2583
|
payload: event,
|
|
1528
|
-
}));
|
|
2584
|
+
}), aborts);
|
|
1529
2585
|
}
|
|
1530
2586
|
function stateIdentity(stateId) {
|
|
1531
2587
|
return stateId === undefined ? {} : { stateId };
|
|
@@ -1583,6 +2639,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1583
2639
|
};
|
|
1584
2640
|
}
|
|
1585
2641
|
async function emitCallStarted(startedType, finishedType, identity, position,
|
|
2642
|
+
// The applicable combined signal: a start-sink rejection causally
|
|
2643
|
+
// identical to its reason is the cancellation itself, not a control
|
|
2644
|
+
// error — the pair finishes `aborted` and nothing latches
|
|
2645
|
+
// (slc/link.md §Abort).
|
|
2646
|
+
signal,
|
|
1586
2647
|
// Base payload of the best-effort finish emitted when the start sink
|
|
1587
2648
|
// rejects; it defaults to the payload the start carried, which the
|
|
1588
2649
|
// player, judge, and captain pairs take as-is. The apply pair cannot:
|
|
@@ -1594,11 +2655,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1594
2655
|
await emitTrace(startedType, identity, position);
|
|
1595
2656
|
}
|
|
1596
2657
|
catch (error) {
|
|
1597
|
-
|
|
2658
|
+
if (!isAbortFailure(error, signal))
|
|
2659
|
+
controlPlaneError ??= error;
|
|
1598
2660
|
try {
|
|
1599
2661
|
await emitTrace(finishedType, {
|
|
1600
2662
|
...finishIdentity,
|
|
1601
|
-
status: 'error',
|
|
2663
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1602
2664
|
error: normalizeError(error),
|
|
1603
2665
|
}, position);
|
|
1604
2666
|
}
|
|
@@ -1608,31 +2670,605 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1608
2670
|
throw error;
|
|
1609
2671
|
}
|
|
1610
2672
|
}
|
|
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
|
+
}
|
|
1611
3235
|
const boundary = {
|
|
1612
3236
|
async callPlayer(input, roleId, prompt, signal) {
|
|
1613
3237
|
// State-entry telemetry/status must precede the call they describe.
|
|
1614
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
|
+
}
|
|
1615
3248
|
const turnId = activeTurnId;
|
|
1616
3249
|
const stateId = input.stateId;
|
|
1617
3250
|
const playerId = resolvedPlayerId(roleId);
|
|
1618
|
-
let
|
|
3251
|
+
let selectedResume;
|
|
1619
3252
|
try {
|
|
1620
3253
|
signal.throwIfAborted();
|
|
1621
|
-
|
|
3254
|
+
selectedResume =
|
|
3255
|
+
deferredContinuation?.playerContinuation ??
|
|
3256
|
+
selectPlayerResume(roleId, playerId);
|
|
1622
3257
|
}
|
|
1623
3258
|
catch (error) {
|
|
1624
|
-
if (!signal
|
|
3259
|
+
if (!isAbortFailure(error, signal))
|
|
1625
3260
|
controlPlaneError ??= error;
|
|
1626
3261
|
throw error;
|
|
1627
3262
|
}
|
|
1628
|
-
const callId =
|
|
1629
|
-
|
|
3263
|
+
const callId = deferredContinuation?.effectBoundary.callId ??
|
|
3264
|
+
`player-${++playerCallSequence}`;
|
|
3265
|
+
const callIdentity = (resume) => ({
|
|
1630
3266
|
...stateIdentity(stateId),
|
|
1631
3267
|
sourceItem: input.sourceItem,
|
|
1632
3268
|
roleId,
|
|
1633
3269
|
...(playerId === undefined ? {} : { playerId }),
|
|
1634
3270
|
resume,
|
|
1635
|
-
};
|
|
3271
|
+
});
|
|
1636
3272
|
const position = {
|
|
1637
3273
|
...(turnId !== undefined ? { turnId } : {}),
|
|
1638
3274
|
callId,
|
|
@@ -1640,92 +3276,182 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1640
3276
|
const playerKey = continuationKey(roleId, playerId);
|
|
1641
3277
|
if (activePlayerKeys.has(playerKey)) {
|
|
1642
3278
|
const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
|
|
1643
|
-
await emitCallStarted('player.call.started', 'player.call.finished', { ...
|
|
1644
|
-
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);
|
|
1645
3285
|
throw error;
|
|
1646
3286
|
}
|
|
1647
3287
|
activePlayerKeys.add(playerKey);
|
|
1648
3288
|
try {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
// (e.g. fired from the trace sink itself); the host call must
|
|
1654
|
-
// never start after abort, so settle the already-started pair
|
|
1655
|
-
// as `aborted` through the catch below.
|
|
1656
|
-
signal.throwIfAborted();
|
|
1657
|
-
rawResult = await requireHostPorts().callPlayer(roleId, prompt, signal, { resume });
|
|
1658
|
-
// A host promise is not required to honor cancellation. Do not let
|
|
1659
|
-
// a late result mutate continuity or publish a successful finish.
|
|
1660
|
-
signal.throwIfAborted();
|
|
1661
|
-
}
|
|
1662
|
-
catch (error) {
|
|
1663
|
-
if (!signal.aborted)
|
|
1664
|
-
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;
|
|
1665
3293
|
try {
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
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();
|
|
1671
3304
|
}
|
|
1672
|
-
catch {
|
|
1673
|
-
|
|
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;
|
|
1674
3321
|
}
|
|
1675
|
-
|
|
1676
|
-
// prior token remains available for a later explicit resume.
|
|
1677
|
-
throw error;
|
|
1678
|
-
}
|
|
1679
|
-
let result;
|
|
1680
|
-
try {
|
|
1681
|
-
result = validatePlayerResult(rawResult);
|
|
1682
|
-
}
|
|
1683
|
-
catch (error) {
|
|
1684
|
-
if (!signal.aborted)
|
|
1685
|
-
controlPlaneError ??= error;
|
|
3322
|
+
let result;
|
|
1686
3323
|
try {
|
|
1687
|
-
|
|
3324
|
+
result = validatePlayerResult(rawResult);
|
|
1688
3325
|
}
|
|
1689
|
-
catch {
|
|
1690
|
-
|
|
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;
|
|
1691
3336
|
}
|
|
1692
|
-
throw error;
|
|
1693
|
-
}
|
|
1694
|
-
try {
|
|
1695
|
-
updatePlayerResume(roleId, playerId, result);
|
|
1696
|
-
}
|
|
1697
|
-
catch (error) {
|
|
1698
|
-
if (!signal.aborted)
|
|
1699
|
-
controlPlaneError ??= error;
|
|
1700
3337
|
try {
|
|
1701
|
-
|
|
3338
|
+
updatePlayerResume(roleId, playerId, result);
|
|
1702
3339
|
}
|
|
1703
|
-
catch {
|
|
1704
|
-
|
|
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;
|
|
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;
|
|
3404
|
+
try {
|
|
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);
|
|
3412
|
+
}
|
|
3413
|
+
catch (error) {
|
|
3414
|
+
if (expectedBoundPendingQuestion !== undefined) {
|
|
3415
|
+
closeAfterIndeterminateDeferredSettlement(undefined, error);
|
|
1705
3416
|
}
|
|
3417
|
+
expectedBoundPendingQuestion = undefined;
|
|
3418
|
+
governedSettlementsByBoundaryId.delete(effectBoundary.boundaryId);
|
|
3419
|
+
governedCompletionEvidenceByBoundaryId.delete(effectBoundary.boundaryId);
|
|
3420
|
+
refreshGovernedBoundaryStart(effectBoundary.boundaryId);
|
|
3421
|
+
if (!isAbortFailure(error, signal))
|
|
3422
|
+
controlPlaneError ??= error;
|
|
1706
3423
|
throw error;
|
|
1707
3424
|
}
|
|
1708
|
-
await emitTrace('player.call.finished', {
|
|
1709
|
-
...identity,
|
|
1710
|
-
status: result.status,
|
|
1711
|
-
...(result.finalText !== undefined
|
|
1712
|
-
? { finalText: result.finalText }
|
|
1713
|
-
: {}),
|
|
1714
|
-
...(result.error !== undefined
|
|
1715
|
-
? { error: normalizeError(result.error) }
|
|
1716
|
-
: {}),
|
|
1717
|
-
...(result.resumeToken !== undefined
|
|
1718
|
-
? { resumeToken: result.resumeToken }
|
|
1719
|
-
: {}),
|
|
1720
|
-
}, position);
|
|
1721
|
-
return result;
|
|
1722
3425
|
}
|
|
1723
3426
|
finally {
|
|
1724
3427
|
activePlayerKeys.delete(playerKey);
|
|
1725
3428
|
}
|
|
1726
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
|
+
},
|
|
1727
3450
|
async callJudge(purpose, stateId, prompt, signal) {
|
|
1728
3451
|
return judgeQueue.add(async () => {
|
|
3452
|
+
const governedSemanticJudge = purpose === 'player-output-adjudication' &&
|
|
3453
|
+
stateId !== undefined &&
|
|
3454
|
+
outcomeAuthority.governedPlayerStates[stateId] !== undefined;
|
|
1729
3455
|
signal.throwIfAborted();
|
|
1730
3456
|
// A transition/status queued synchronously by XState must reach
|
|
1731
3457
|
// the host before the judge call that follows it.
|
|
@@ -1738,7 +3464,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1738
3464
|
...(turnId !== undefined ? { turnId } : {}),
|
|
1739
3465
|
callId,
|
|
1740
3466
|
};
|
|
1741
|
-
await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, position);
|
|
3467
|
+
await emitCallStarted('judge.call.started', 'judge.call.finished', { ...identity, prompt }, position, signal);
|
|
1742
3468
|
let reply;
|
|
1743
3469
|
try {
|
|
1744
3470
|
// An abort may land while the awaited started emission drains
|
|
@@ -1750,19 +3476,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1750
3476
|
signal.throwIfAborted();
|
|
1751
3477
|
}
|
|
1752
3478
|
catch (error) {
|
|
1753
|
-
if (!isAbortFailure(error, signal)) {
|
|
3479
|
+
if (!isAbortFailure(error, signal) && !governedSemanticJudge) {
|
|
1754
3480
|
controlPlaneError ??= error;
|
|
1755
3481
|
}
|
|
1756
3482
|
await emitTrace('judge.call.finished', {
|
|
1757
3483
|
...identity,
|
|
1758
|
-
status: signal
|
|
3484
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1759
3485
|
error: normalizeError(error),
|
|
1760
3486
|
}, position);
|
|
1761
3487
|
throw error;
|
|
1762
3488
|
}
|
|
1763
3489
|
if (typeof reply !== 'string') {
|
|
1764
3490
|
const error = new TypeError('judge reply must be a string');
|
|
1765
|
-
|
|
3491
|
+
if (!governedSemanticJudge)
|
|
3492
|
+
controlPlaneError ??= error;
|
|
1766
3493
|
await emitTrace('judge.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1767
3494
|
throw error;
|
|
1768
3495
|
}
|
|
@@ -1802,7 +3529,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1802
3529
|
...(turnId !== undefined ? { turnId } : {}),
|
|
1803
3530
|
callId,
|
|
1804
3531
|
};
|
|
1805
|
-
await emitCallStarted('captain.call.started', 'captain.call.finished', { ...identity, prompt }, position);
|
|
3532
|
+
await emitCallStarted('captain.call.started', 'captain.call.finished', { ...identity, prompt }, position, signal);
|
|
1806
3533
|
let rawResult;
|
|
1807
3534
|
try {
|
|
1808
3535
|
// An abort may land while the awaited started emission drains
|
|
@@ -1824,7 +3551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1824
3551
|
controlPlaneError ??= error;
|
|
1825
3552
|
await emitTrace('captain.call.finished', {
|
|
1826
3553
|
...identity,
|
|
1827
|
-
status: signal
|
|
3554
|
+
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
1828
3555
|
error: normalizeError(error),
|
|
1829
3556
|
}, position);
|
|
1830
3557
|
throw error;
|
|
@@ -1889,12 +3616,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1889
3616
|
function playerActor(ports) {
|
|
1890
3617
|
return createPlayerBridge({
|
|
1891
3618
|
resolveRoleId: requireRoleId,
|
|
3619
|
+
validateInput: (input) => assertGovernedPlayerInput(outcomeAuthority, input, extractFields, label),
|
|
1892
3620
|
composePlayerPrompt: composeBoundPlayerPrompt,
|
|
1893
3621
|
adjudication,
|
|
1894
3622
|
resumableStateIds,
|
|
3623
|
+
allowsCorrectiveReplay: acknowledgedBoundaryIsUnchanged,
|
|
1895
3624
|
}, ports, () => activeSignal, boundary, (error) => {
|
|
1896
|
-
if (!activeSignal
|
|
3625
|
+
if (activeSignal === undefined || !isAbortFailure(error, activeSignal)) {
|
|
1897
3626
|
controlPlaneError ??= error;
|
|
3627
|
+
}
|
|
1898
3628
|
});
|
|
1899
3629
|
}
|
|
1900
3630
|
// Direct-Captain actor (slc/link.md §Captain prompt composition,
|
|
@@ -1961,7 +3691,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1961
3691
|
// failure state (PBRT-47); everything else here — a drained
|
|
1962
3692
|
// emission failure, prompt composition, the port itself,
|
|
1963
3693
|
// adjudication — is control plane.
|
|
1964
|
-
if (!active
|
|
3694
|
+
if (!isAbortFailure(error, active) && !isFsmResultFailure(error)) {
|
|
1965
3695
|
controlPlaneError ??= error;
|
|
1966
3696
|
}
|
|
1967
3697
|
throw error;
|
|
@@ -1982,57 +3712,181 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1982
3712
|
const failedGuard = guards[1] ?? guards[0];
|
|
1983
3713
|
const cwd = boundScriptCwd ?? process.cwd();
|
|
1984
3714
|
const ports = runtimePorts ?? requireHostPorts();
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
3715
|
+
// slc/link.md §Script execution: an already-aborted turn spawns
|
|
3716
|
+
// nothing, and the thrown signal reason keeps the rejection
|
|
3717
|
+
// causally classified as the abort it is.
|
|
3718
|
+
active.throwIfAborted();
|
|
3719
|
+
// Abort ownership — the listener that terminates the group and
|
|
3720
|
+
// the escalation timer — spans the whole invocation body, not
|
|
3721
|
+
// just the spawn-to-close window: an abort landing during the
|
|
3722
|
+
// post-exit emission tail must still kill surviving group
|
|
3723
|
+
// members before the actor settles (slc/link.md §Script
|
|
3724
|
+
// execution). One finally releases both.
|
|
3725
|
+
let child;
|
|
3726
|
+
let killTimer;
|
|
3727
|
+
const signalGroup = (sig) => {
|
|
3728
|
+
if (child?.pid !== undefined) {
|
|
3729
|
+
try {
|
|
3730
|
+
process.kill(-child.pid, sig);
|
|
3731
|
+
}
|
|
3732
|
+
catch {
|
|
3733
|
+
// Confirmation belongs to the bounded liveness probe below:
|
|
3734
|
+
// a failed signal can mean ESRCH, EPERM, or another fault.
|
|
3735
|
+
}
|
|
1992
3736
|
}
|
|
1993
|
-
|
|
1994
|
-
|
|
3737
|
+
};
|
|
3738
|
+
// After a SIGKILL is posted, settlement waits for the group to
|
|
3739
|
+
// stop being signalable — bounded by the same grace so an
|
|
3740
|
+
// unreapable member outside the runtime's control cannot stall
|
|
3741
|
+
// the turn forever. Observed teardown is milliseconds.
|
|
3742
|
+
let groupGonePromise;
|
|
3743
|
+
const awaitGroupGone = () => {
|
|
3744
|
+
const pid = child?.pid;
|
|
3745
|
+
if (pid === undefined)
|
|
3746
|
+
return Promise.resolve();
|
|
3747
|
+
groupGonePromise ??= (async () => {
|
|
3748
|
+
const teardownFailure = (message, cause) => {
|
|
3749
|
+
const failure = new ScriptProcessGroupTeardownError(pid, message, cause);
|
|
3750
|
+
// A teardown failure is not an authored script result.
|
|
3751
|
+
// Surface it at the active public boundary even though
|
|
3752
|
+
// XState also routes the rejected actor through onError.
|
|
3753
|
+
controlPlaneError ??= failure;
|
|
3754
|
+
return failure;
|
|
3755
|
+
};
|
|
3756
|
+
const deadline = Date.now() + SCRIPT_ABORT_KILL_GRACE_MS;
|
|
3757
|
+
let lastProbeError;
|
|
3758
|
+
for (;;) {
|
|
3759
|
+
try {
|
|
3760
|
+
process.kill(-pid, 0);
|
|
3761
|
+
}
|
|
3762
|
+
catch (error) {
|
|
3763
|
+
if (isNoSuchProcess(error))
|
|
3764
|
+
return;
|
|
3765
|
+
// EPERM confirms that at least one process in the group
|
|
3766
|
+
// still exists but is not signalable by this process. Keep
|
|
3767
|
+
// waiting for ESRCH within the bound; every other probe
|
|
3768
|
+
// error makes confirmation itself unreliable immediately.
|
|
3769
|
+
if (!isProcessPermissionDenied(error)) {
|
|
3770
|
+
throw teardownFailure('the liveness probe failed', error);
|
|
3771
|
+
}
|
|
3772
|
+
lastProbeError = error;
|
|
3773
|
+
}
|
|
3774
|
+
if (Date.now() >= deadline) {
|
|
3775
|
+
throw teardownFailure(`the group remained signalable after ${SCRIPT_ABORT_KILL_GRACE_MS}ms`, lastProbeError);
|
|
3776
|
+
}
|
|
3777
|
+
await new Promise((tick) => setTimeout(tick, 5));
|
|
3778
|
+
}
|
|
3779
|
+
})();
|
|
3780
|
+
return groupGonePromise;
|
|
3781
|
+
};
|
|
3782
|
+
const onAbort = () => {
|
|
3783
|
+
signalGroup('SIGTERM');
|
|
3784
|
+
killTimer = setTimeout(() => signalGroup('SIGKILL'), SCRIPT_ABORT_KILL_GRACE_MS);
|
|
3785
|
+
};
|
|
3786
|
+
// An abort observed once the shell has already exited rejects
|
|
3787
|
+
// with the signal's reason before guard resolution and before
|
|
3788
|
+
// starting any further script emission — after killing whatever
|
|
3789
|
+
// group members outlived the shell. The shell's own exit ended
|
|
3790
|
+
// the TERM grace's purpose, so escalation is immediate here.
|
|
3791
|
+
const settleIfAborted = async () => {
|
|
3792
|
+
if (!active.aborted)
|
|
1995
3793
|
return;
|
|
3794
|
+
signalGroup('SIGKILL');
|
|
3795
|
+
await awaitGroupGone();
|
|
3796
|
+
active.throwIfAborted();
|
|
3797
|
+
};
|
|
3798
|
+
let invocationFailed = false;
|
|
3799
|
+
try {
|
|
3800
|
+
const exitStatus = await new Promise((resolve, reject) => {
|
|
3801
|
+
try {
|
|
3802
|
+
// detached: the shell leads its own POSIX process group,
|
|
3803
|
+
// so an abort can terminate the command's whole group — a
|
|
3804
|
+
// lone SIGTERM to the wrapper never reaches backgrounded
|
|
3805
|
+
// members.
|
|
3806
|
+
child = spawn('sh', ['-c', input.command], {
|
|
3807
|
+
cwd,
|
|
3808
|
+
stdio: 'ignore',
|
|
3809
|
+
detached: true,
|
|
3810
|
+
});
|
|
3811
|
+
}
|
|
3812
|
+
catch (error) {
|
|
3813
|
+
reject(error);
|
|
3814
|
+
return;
|
|
3815
|
+
}
|
|
3816
|
+
// On abort, terminate the group and escalate — but settle
|
|
3817
|
+
// only from 'close', after the shell itself has exited, so
|
|
3818
|
+
// the turn never reports quiescence while the script still
|
|
3819
|
+
// runs (slc/link.md §Abort). SIGKILL is untrappable, so
|
|
3820
|
+
// 'close' is bounded by the grace.
|
|
3821
|
+
active.addEventListener('abort', onAbort, { once: true });
|
|
3822
|
+
child.on('error', (error) => {
|
|
3823
|
+
reject(error);
|
|
3824
|
+
});
|
|
3825
|
+
child.on('close', (code) => {
|
|
3826
|
+
if (active.aborted) {
|
|
3827
|
+
// The shell may exit cooperatively on the group SIGTERM
|
|
3828
|
+
// while a TERM-immune same-group descendant survives;
|
|
3829
|
+
// the group stays addressable while any member lives,
|
|
3830
|
+
// so kill it and await its disappearance before
|
|
3831
|
+
// settling (slc/link.md §Script execution).
|
|
3832
|
+
signalGroup('SIGKILL');
|
|
3833
|
+
void awaitGroupGone().then(() => reject(active.reason), reject);
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
resolve(typeof code === 'number' ? code : 1);
|
|
3837
|
+
});
|
|
3838
|
+
});
|
|
3839
|
+
await settleIfAborted();
|
|
3840
|
+
await ports.emitStatus(`Executed script for ${input.stateId} (exit ${exitStatus}).`);
|
|
3841
|
+
await settleIfAborted();
|
|
3842
|
+
await ports.emitTelemetry({
|
|
3843
|
+
topic: 'playbook.script',
|
|
3844
|
+
payload: {
|
|
3845
|
+
stateId: input.stateId,
|
|
3846
|
+
sourceItem: input.sourceItem,
|
|
3847
|
+
exitStatus,
|
|
3848
|
+
},
|
|
3849
|
+
});
|
|
3850
|
+
await settleIfAborted();
|
|
3851
|
+
if (exitStatus === 0) {
|
|
3852
|
+
return { guard: okGuard, exitStatus: 0 };
|
|
1996
3853
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
3854
|
+
return { guard: failedGuard, exitStatus };
|
|
3855
|
+
}
|
|
3856
|
+
catch (error) {
|
|
3857
|
+
// Preserve the invocation's authoritative exact cancellation or
|
|
3858
|
+
// distinct sink failure after teardown succeeds. The finally
|
|
3859
|
+
// block may replace it only with a distinct teardown failure
|
|
3860
|
+
// when the process group cannot be confirmed gone.
|
|
3861
|
+
invocationFailed = true;
|
|
3862
|
+
throw error;
|
|
3863
|
+
}
|
|
3864
|
+
finally {
|
|
3865
|
+
try {
|
|
3866
|
+
if (active.aborted) {
|
|
3867
|
+
signalGroup('SIGKILL');
|
|
3868
|
+
await awaitGroupGone();
|
|
3869
|
+
if (!invocationFailed)
|
|
3870
|
+
active.throwIfAborted();
|
|
3871
|
+
}
|
|
2004
3872
|
}
|
|
2005
|
-
|
|
2006
|
-
child.on('error', (error) => {
|
|
2007
|
-
active.removeEventListener('abort', onAbort);
|
|
2008
|
-
reject(error);
|
|
2009
|
-
});
|
|
2010
|
-
child.on('close', (code) => {
|
|
3873
|
+
finally {
|
|
2011
3874
|
active.removeEventListener('abort', onAbort);
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
await ports.emitStatus(`Executed script for ${input.stateId} (exit ${exitStatus}).`);
|
|
2016
|
-
await ports.emitTelemetry({
|
|
2017
|
-
topic: 'playbook.script',
|
|
2018
|
-
payload: {
|
|
2019
|
-
stateId: input.stateId,
|
|
2020
|
-
sourceItem: input.sourceItem,
|
|
2021
|
-
exitStatus,
|
|
2022
|
-
},
|
|
2023
|
-
});
|
|
2024
|
-
if (exitStatus === 0) {
|
|
2025
|
-
return { guard: okGuard, exitStatus: 0 };
|
|
3875
|
+
if (killTimer !== undefined)
|
|
3876
|
+
clearTimeout(killTimer);
|
|
3877
|
+
}
|
|
2026
3878
|
}
|
|
2027
|
-
return { guard: failedGuard, exitStatus };
|
|
2028
3879
|
});
|
|
2029
3880
|
}
|
|
2030
3881
|
const nestedBridge = createNestedPlaybookBridge({
|
|
2031
3882
|
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
2032
3883
|
getBoundarySignal: () => activeSignal,
|
|
2033
3884
|
callPlaybook: (request, signal) => requireHostPorts().callPlaybook(request, signal),
|
|
2034
|
-
emitStarted: async (event) => {
|
|
3885
|
+
emitStarted: async (event, aborts) => {
|
|
2035
3886
|
playbookCallTurnIds.set(event.callId, activeTurnId);
|
|
3887
|
+
if (hasGovernedPlayerStates) {
|
|
3888
|
+
playbookCallEffectPrefixes.set(event.callId, activeEffectLedgerPrefixSequence);
|
|
3889
|
+
}
|
|
2036
3890
|
await emitTrace('playbook.call.started', {
|
|
2037
3891
|
stateId: event.stateId,
|
|
2038
3892
|
playbookId: event.playbookId,
|
|
@@ -2040,9 +3894,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2040
3894
|
}, {
|
|
2041
3895
|
...(activeTurnId !== undefined ? { turnId: activeTurnId } : {}),
|
|
2042
3896
|
callId: event.callId,
|
|
2043
|
-
});
|
|
3897
|
+
}, aborts);
|
|
2044
3898
|
},
|
|
2045
|
-
emitFinished: async (event) => {
|
|
3899
|
+
emitFinished: async (event, aborts) => {
|
|
2046
3900
|
const turnId = playbookCallTurnIds.get(event.callId);
|
|
2047
3901
|
try {
|
|
2048
3902
|
await emitTrace('playbook.call.finished', {
|
|
@@ -2053,22 +3907,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2053
3907
|
}, {
|
|
2054
3908
|
...(turnId !== undefined ? { turnId } : {}),
|
|
2055
3909
|
callId: event.callId,
|
|
2056
|
-
});
|
|
3910
|
+
}, aborts);
|
|
2057
3911
|
}
|
|
2058
3912
|
finally {
|
|
2059
3913
|
playbookCallTurnIds.delete(event.callId);
|
|
3914
|
+
playbookCallEffectPrefixes.delete(event.callId);
|
|
2060
3915
|
}
|
|
2061
3916
|
},
|
|
2062
3917
|
drain: drainEmissions,
|
|
2063
|
-
bindResumeSignal: (signal) => {
|
|
3918
|
+
bindResumeSignal: (signal, aborts) => {
|
|
2064
3919
|
activeSignal = signal;
|
|
3920
|
+
activeAborts = aborts ?? abortReasonClassifier(signal);
|
|
3921
|
+
},
|
|
3922
|
+
bindActorSettlement: (aborts) => {
|
|
3923
|
+
actorSettlementAborts = aborts;
|
|
2065
3924
|
},
|
|
2066
|
-
onControlPlaneError: (error) => {
|
|
2067
|
-
|
|
3925
|
+
onControlPlaneError: (error, aborts) => {
|
|
3926
|
+
// The shared bridge classifies before reporting against its own
|
|
3927
|
+
// invocation-and-resume signals; classify once more here against
|
|
3928
|
+
// the boundary signal so a report that is the active boundary's
|
|
3929
|
+
// exact abort reason can never masquerade as a control error
|
|
3930
|
+
// (slc/link.md §Abort).
|
|
3931
|
+
if (!aborts?.isAbortReason(error) &&
|
|
3932
|
+
!activeAborts?.isAbortReason(error)) {
|
|
2068
3933
|
controlPlaneError ??= error;
|
|
3934
|
+
}
|
|
2069
3935
|
},
|
|
2070
|
-
onBackgroundError: (error) => {
|
|
2071
|
-
|
|
3936
|
+
onBackgroundError: (error, aborts) => {
|
|
3937
|
+
if (!aborts?.isAbortReason(error))
|
|
3938
|
+
emissionFailure ??= { error };
|
|
2072
3939
|
},
|
|
2073
3940
|
});
|
|
2074
3941
|
function tracePositionForActiveTurn() {
|
|
@@ -2082,11 +3949,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2082
3949
|
previousState: previousState ?? null,
|
|
2083
3950
|
state,
|
|
2084
3951
|
};
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
}
|
|
3952
|
+
const pendingBossQuestion = pendingBossQuestionForState(state, context);
|
|
3953
|
+
if (pendingBossQuestion !== undefined &&
|
|
3954
|
+
!hasUnresolvedReconciliation()) {
|
|
3955
|
+
payload.pendingBossQuestion = pendingBossQuestion;
|
|
2090
3956
|
}
|
|
2091
3957
|
if (state.stateId === 'failed') {
|
|
2092
3958
|
const lastError = normalizeErrorFull(context.lastError);
|
|
@@ -2095,9 +3961,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2095
3961
|
}
|
|
2096
3962
|
return snapshotJsonValue(payload, 'FSM telemetry payload');
|
|
2097
3963
|
}
|
|
2098
|
-
function enqueueTransitionEmission(payload, state, statuses, position) {
|
|
3964
|
+
function enqueueTransitionEmission(payload, state, acceptedOutcomes, statuses, position, aborts) {
|
|
2099
3965
|
const currentSession = requireSession();
|
|
2100
3966
|
const transitionTrace = createTraceEvent('fsm.transition', payload, position);
|
|
3967
|
+
const acceptedOutcomeTraces = acceptedOutcomes.map((acceptedOutcome) => createTraceEvent('outcome.accepted', acceptedOutcome, position));
|
|
2101
3968
|
const statusEmissions = statuses.map(({ message, data }) => ({
|
|
2102
3969
|
message,
|
|
2103
3970
|
data,
|
|
@@ -2117,6 +3984,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2117
3984
|
topic: 'playbook.fsm.state',
|
|
2118
3985
|
payload,
|
|
2119
3986
|
});
|
|
3987
|
+
for (const acceptedOutcome of acceptedOutcomeTraces) {
|
|
3988
|
+
await currentSession.ports.emitTelemetry({
|
|
3989
|
+
topic: 'playbook.trace',
|
|
3990
|
+
payload: acceptedOutcome,
|
|
3991
|
+
});
|
|
3992
|
+
}
|
|
2120
3993
|
for (const status of statusEmissions) {
|
|
2121
3994
|
await currentSession.ports.emitTelemetry({
|
|
2122
3995
|
topic: 'playbook.trace',
|
|
@@ -2124,13 +3997,38 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2124
3997
|
});
|
|
2125
3998
|
await currentSession.ports.emitStatus(status.message, status.data);
|
|
2126
3999
|
}
|
|
2127
|
-
}).catch(() => undefined);
|
|
4000
|
+
}, aborts).catch(() => undefined);
|
|
2128
4001
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
4002
|
+
// One classifying latch for every runtime-observed error — inspection
|
|
4003
|
+
// failures and root-actor errors alike. Outside a boundary the error
|
|
4004
|
+
// rides the emission channel, which the next boundary's (or init's)
|
|
4005
|
+
// drain throws; inside a boundary it is a control-plane error unless it
|
|
4006
|
+
// is the boundary signal's own abort reason (slc/link.md §Abort).
|
|
4007
|
+
function latchRuntimeError(error, aborts = activeAborts) {
|
|
4008
|
+
if (aborts?.isAbortReason(error))
|
|
4009
|
+
return;
|
|
4010
|
+
if (activeSignal === undefined)
|
|
4011
|
+
emissionFailure ??= { error };
|
|
2132
4012
|
else
|
|
2133
|
-
|
|
4013
|
+
controlPlaneError ??= error;
|
|
4014
|
+
}
|
|
4015
|
+
function consumeActorSettlementAborts(forSnapshot = false) {
|
|
4016
|
+
const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
|
|
4017
|
+
actorSettlementAborts = undefined;
|
|
4018
|
+
actorSettlementErrorAborts = undefined;
|
|
4019
|
+
if (forSnapshot && aborts !== undefined) {
|
|
4020
|
+
// XState can report an errored root through both its inspection
|
|
4021
|
+
// snapshot and subscriber. Keep the same provenance through that
|
|
4022
|
+
// synchronous notification only; an ordinary transition must not
|
|
4023
|
+
// lend it to a later unrelated actor error.
|
|
4024
|
+
actorSettlementErrorAborts = aborts;
|
|
4025
|
+
queueMicrotask(() => {
|
|
4026
|
+
if (actorSettlementErrorAborts === aborts) {
|
|
4027
|
+
actorSettlementErrorAborts = undefined;
|
|
4028
|
+
}
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
return aborts;
|
|
2134
4032
|
}
|
|
2135
4033
|
// PBRT-6: the single seam that stops this runtime's actor. Stopping a
|
|
2136
4034
|
// still-running actor fires one more `@xstate.snapshot` for the
|
|
@@ -2144,10 +4042,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2144
4042
|
if (!actor)
|
|
2145
4043
|
return;
|
|
2146
4044
|
suppressInspectionEmissions = true;
|
|
4045
|
+
acceptedOutcomeConsumer.reset();
|
|
2147
4046
|
actor.stop();
|
|
2148
4047
|
}
|
|
2149
4048
|
function buildActor(ports, machineSnapshot) {
|
|
2150
4049
|
priorState = undefined;
|
|
4050
|
+
acceptedOutcomeConsumer.reset();
|
|
2151
4051
|
const actors = {};
|
|
2152
4052
|
if (declaredActors.has('player'))
|
|
2153
4053
|
actors.player = playerActor(ports);
|
|
@@ -2170,12 +4070,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2170
4070
|
? {}
|
|
2171
4071
|
: { snapshot: machineSnapshot }),
|
|
2172
4072
|
inspect: (inspectionEvent) => {
|
|
2173
|
-
if (inspectionEvent.type !== '@xstate.snapshot')
|
|
2174
|
-
return;
|
|
2175
4073
|
if (inspectionEvent.actorRef !== builtActor)
|
|
2176
4074
|
return;
|
|
2177
4075
|
if (suppressInspectionEmissions)
|
|
2178
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')
|
|
4087
|
+
return;
|
|
4088
|
+
const settlementAborts = consumeActorSettlementAborts(true);
|
|
2179
4089
|
try {
|
|
2180
4090
|
const snap = inspectionEvent.snapshot;
|
|
2181
4091
|
const state = normalizePlaybookSnapshot(snap);
|
|
@@ -2183,18 +4093,70 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2183
4093
|
throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
|
|
2184
4094
|
}
|
|
2185
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
|
+
}
|
|
2186
4115
|
const context = (snap.context ??
|
|
2187
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
|
+
}
|
|
2188
4127
|
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
2189
|
-
const
|
|
2190
|
-
|
|
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
|
+
}
|
|
2191
4142
|
priorState = state;
|
|
2192
4143
|
}
|
|
2193
4144
|
catch (error) {
|
|
2194
|
-
|
|
4145
|
+
acceptedOutcomeConsumer.reset();
|
|
4146
|
+
latchRuntimeError(error, settlementAborts);
|
|
2195
4147
|
}
|
|
2196
4148
|
},
|
|
2197
4149
|
});
|
|
4150
|
+
// A synchronously-errored actor is already quiescent, so the turn's
|
|
4151
|
+
// quiescence wait never subscribes and XState would report the error
|
|
4152
|
+
// as unhandled after the boundary returns. Observe it through the
|
|
4153
|
+
// classifying latch: mid-boundary it is the control-plane error
|
|
4154
|
+
// unless it is the abort reason itself; at startup it rides the
|
|
4155
|
+
// emission channel so `init`'s own drain rejects with it and the
|
|
4156
|
+
// failed-start cleanup runs (slc/link.md §Session lifecycle).
|
|
4157
|
+
builtActor.subscribe({
|
|
4158
|
+
error: (error) => latchRuntimeError(error, consumeActorSettlementAborts()),
|
|
4159
|
+
});
|
|
2198
4160
|
return builtActor;
|
|
2199
4161
|
}
|
|
2200
4162
|
function runResultFor(outcome, error) {
|
|
@@ -2202,6 +4164,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2202
4164
|
if (outcome === 'quiescent' || outcome === 'no-action') {
|
|
2203
4165
|
return { outcome, state };
|
|
2204
4166
|
}
|
|
4167
|
+
if (outcome === 'unresolved-effect') {
|
|
4168
|
+
return { outcome, state };
|
|
4169
|
+
}
|
|
2205
4170
|
if (outcome === 'suspended') {
|
|
2206
4171
|
const pendingCall = nestedBridge.getPendingCall();
|
|
2207
4172
|
if (!pendingCall) {
|
|
@@ -2211,14 +4176,19 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2211
4176
|
}
|
|
2212
4177
|
if (outcome === 'terminal') {
|
|
2213
4178
|
const output = actor?.getSnapshot()?.output;
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
4179
|
+
const stateDescription = !hasUnresolvedReconciliation()
|
|
4180
|
+
? stateDescriptionFor(state)
|
|
4181
|
+
: undefined;
|
|
4182
|
+
return {
|
|
4183
|
+
outcome,
|
|
4184
|
+
state,
|
|
4185
|
+
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
4186
|
+
...(output === undefined
|
|
4187
|
+
? {}
|
|
4188
|
+
: {
|
|
4189
|
+
output: snapshotJsonValue(output, 'terminal playbook output'),
|
|
4190
|
+
}),
|
|
4191
|
+
};
|
|
2222
4192
|
}
|
|
2223
4193
|
const failure = error ??
|
|
2224
4194
|
(outcome === 'failed'
|
|
@@ -2236,15 +4206,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2236
4206
|
function settledOutcome(signal) {
|
|
2237
4207
|
if (nestedBridge.getPendingCall())
|
|
2238
4208
|
return 'suspended';
|
|
2239
|
-
if (signal.aborted)
|
|
2240
|
-
return 'aborted';
|
|
2241
4209
|
const state = currentState();
|
|
2242
4210
|
if (state.status === 'error') {
|
|
4211
|
+
// An errored actor outranks a coincident abort unless the actor's
|
|
4212
|
+
// error is the abort reason itself (slc/link.md §Abort).
|
|
2243
4213
|
const actorError = actor?.getSnapshot()?.error;
|
|
4214
|
+
if (actorError !== undefined && isAbortFailure(actorError, signal)) {
|
|
4215
|
+
return 'aborted';
|
|
4216
|
+
}
|
|
2244
4217
|
throw actorError ?? new Error(`${label} actor entered error status`);
|
|
2245
4218
|
}
|
|
4219
|
+
// Terminal completion outranks a coincident abort: the work finished,
|
|
4220
|
+
// and reporting `aborted` would hide a terminal machine behind a
|
|
4221
|
+
// settlement a later turn silently restarts (slc/link.md §Abort).
|
|
2246
4222
|
if (state.status === 'done')
|
|
2247
4223
|
return 'terminal';
|
|
4224
|
+
if (signal.aborted)
|
|
4225
|
+
return 'aborted';
|
|
2248
4226
|
if (state.stateId === 'failed')
|
|
2249
4227
|
return 'failed';
|
|
2250
4228
|
return 'quiescent';
|
|
@@ -2255,12 +4233,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2255
4233
|
...stateIdentity(result.state.stateId),
|
|
2256
4234
|
};
|
|
2257
4235
|
}
|
|
2258
|
-
// Shared failed-start cleanup for init and
|
|
2259
|
-
// 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
|
|
2260
4238
|
// session.disposed boundary, and unbind every closure field so dispose
|
|
2261
|
-
// stays callable. The caller rethrows its original failure. A
|
|
2262
|
-
// failure skips the disposal trace — the parked
|
|
2263
|
-
// 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
|
|
2264
4242
|
// authoritative (DR-014 §2).
|
|
2265
4243
|
async function cleanupFailedStart(cause, options) {
|
|
2266
4244
|
let finalState;
|
|
@@ -2308,6 +4286,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2308
4286
|
privateResumeTokens.clear();
|
|
2309
4287
|
activePlayerKeys.clear();
|
|
2310
4288
|
playbookCallTurnIds.clear();
|
|
4289
|
+
playbookCallEffectPrefixes.clear();
|
|
2311
4290
|
activeEmissionCalls.clear();
|
|
2312
4291
|
emissionQueue.clear();
|
|
2313
4292
|
judgeQueue.clear();
|
|
@@ -2317,10 +4296,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2317
4296
|
savedPorts = undefined;
|
|
2318
4297
|
runtimePorts = undefined;
|
|
2319
4298
|
activeSignal = undefined;
|
|
4299
|
+
activeAborts = undefined;
|
|
4300
|
+
actorSettlementAborts = undefined;
|
|
4301
|
+
actorSettlementErrorAborts = undefined;
|
|
4302
|
+
activeAbortEmission = undefined;
|
|
2320
4303
|
activeTurnId = undefined;
|
|
4304
|
+
activeGovernedBoundarySeen = false;
|
|
4305
|
+
activeGovernedAttemptId = undefined;
|
|
4306
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
4307
|
+
failedGovernedAttemptUnknown = false;
|
|
4308
|
+
failedEffectBoundaryPrefix = undefined;
|
|
4309
|
+
failedGovernedAttemptId = undefined;
|
|
2321
4310
|
controlPlaneError = undefined;
|
|
2322
4311
|
emissionFailure = undefined;
|
|
2323
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 = [];
|
|
2324
4328
|
lastBossEvent = undefined;
|
|
2325
4329
|
suppressInspectionEmissions = false;
|
|
2326
4330
|
initialized = false;
|
|
@@ -2338,28 +4342,58 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2338
4342
|
can.call(snapshot, event) ===
|
|
2339
4343
|
true);
|
|
2340
4344
|
}
|
|
4345
|
+
// DR-034: where the artifact names the FSM context member its entry
|
|
4346
|
+
// action copies the exact Boss text into, that member of the live
|
|
4347
|
+
// snapshot is the retry payload's source. The persisted machine snapshot
|
|
4348
|
+
// carries it, so the candidate derives identically in the process that
|
|
4349
|
+
// exported the snapshot and in one that restored it, and a failure
|
|
4350
|
+
// reached after a Boss reply — whose recorded event the failure state
|
|
4351
|
+
// refuses — is recoverable too. Naming the member is the artifact's
|
|
4352
|
+
// statement that it holds the entry text: a same-named member is never
|
|
4353
|
+
// assumed, since inferring one would turn any matching context member
|
|
4354
|
+
// into a replay payload without its author saying so.
|
|
4355
|
+
// Declared and absent or empty excludes the candidate rather than
|
|
4356
|
+
// falling back to the record, which would make the action depend on the
|
|
4357
|
+
// process again — the very thing this source exists to end.
|
|
4358
|
+
function retryEventFrom(snapshot) {
|
|
4359
|
+
const entryEvent = spec.entryEvent;
|
|
4360
|
+
if (entryEvent?.contextField === undefined)
|
|
4361
|
+
return lastBossEvent;
|
|
4362
|
+
const context = snapshot?.context;
|
|
4363
|
+
const text = isPlainObject(context)
|
|
4364
|
+
? context[entryEvent.contextField]
|
|
4365
|
+
: undefined;
|
|
4366
|
+
if (typeof text !== 'string' || text.trim() === '')
|
|
4367
|
+
return undefined;
|
|
4368
|
+
return { type: entryEvent.type, [entryEvent.textField]: text };
|
|
4369
|
+
}
|
|
2341
4370
|
// The failure-state retry entry replays the recorded last classified
|
|
2342
|
-
// event with its recorded payload
|
|
2343
|
-
//
|
|
2344
|
-
//
|
|
4371
|
+
// event with its recorded payload, or the entry event the declared
|
|
4372
|
+
// context member above sources. A candidate whose event the live
|
|
4373
|
+
// snapshot does not accept — or whose payload the runtime can source
|
|
4374
|
+
// from neither — is excluded rather than completed with invented text.
|
|
2345
4375
|
function retryActionFor(snapshot, stateId) {
|
|
2346
|
-
if (stateId !== 'failed'
|
|
4376
|
+
if (stateId !== 'failed')
|
|
2347
4377
|
return undefined;
|
|
2348
|
-
|
|
2349
|
-
|
|
4378
|
+
if (!failedAttemptAllowsReplay())
|
|
4379
|
+
return undefined;
|
|
4380
|
+
const retryEvent = retryEventFrom(snapshot);
|
|
4381
|
+
if (retryEvent === undefined)
|
|
4382
|
+
return undefined;
|
|
4383
|
+
if (!snapshotCan(snapshot, retryEvent))
|
|
2350
4384
|
return undefined;
|
|
2351
4385
|
// A recorded explicit-state-jump event names the exact state its
|
|
2352
4386
|
// replay re-enters: the root BOSS_INTERRUPT shape is a guarded
|
|
2353
4387
|
// multi-arm list keyed on `targetId`, so the first configured arm
|
|
2354
4388
|
// may label a different state than the one the recorded event
|
|
2355
4389
|
// actually resumes.
|
|
2356
|
-
const recordedTargetId =
|
|
2357
|
-
?
|
|
4390
|
+
const recordedTargetId = retryEvent.type === JUMP_EVENT_TYPE
|
|
4391
|
+
? retryEvent.targetId
|
|
2358
4392
|
: undefined;
|
|
2359
4393
|
const target = typeof recordedTargetId === 'string' &&
|
|
2360
4394
|
recordedTargetId.trim().length > 0
|
|
2361
4395
|
? recordedTargetId
|
|
2362
|
-
: firstTransitionTarget(machine, stateId,
|
|
4396
|
+
: firstTransitionTarget(machine, stateId, retryEvent.type);
|
|
2363
4397
|
// PBRT-52: a label is written from a source state description, never
|
|
2364
4398
|
// from an identifier. Falling back to the target id — or, with no
|
|
2365
4399
|
// resolvable target, to the FSM event type — makes the label *be* the
|
|
@@ -2373,10 +4407,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2373
4407
|
return undefined;
|
|
2374
4408
|
return {
|
|
2375
4409
|
action: {
|
|
2376
|
-
id: `retry:${
|
|
4410
|
+
id: `retry:${retryEvent.type}`,
|
|
2377
4411
|
label: `Retry: ${description}`,
|
|
2378
4412
|
},
|
|
2379
|
-
event:
|
|
4413
|
+
event: retryEvent,
|
|
2380
4414
|
};
|
|
2381
4415
|
}
|
|
2382
4416
|
function deriveControlActions(snapshot) {
|
|
@@ -2399,6 +4433,31 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2399
4433
|
return [];
|
|
2400
4434
|
}
|
|
2401
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
|
+
}
|
|
2402
4461
|
const retry = retryActionFor(snapshot, state.stateId);
|
|
2403
4462
|
if (retry !== undefined)
|
|
2404
4463
|
derived.push(retry);
|
|
@@ -2446,45 +4505,555 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2446
4505
|
try {
|
|
2447
4506
|
projected[key] = snapshotJsonValue(value instanceof Error ? normalizeError(value) : value, `control context ${key}`);
|
|
2448
4507
|
}
|
|
2449
|
-
catch {
|
|
2450
|
-
// Declared but not JSON-safe — dropped.
|
|
4508
|
+
catch {
|
|
4509
|
+
// Declared but not JSON-safe — dropped.
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
return Object.keys(projected).length === 0 ? undefined : projected;
|
|
4513
|
+
}
|
|
4514
|
+
// PBRT-52: the view's Boss-facing state description — the meaning of the
|
|
4515
|
+
// state the runtime is in, written by the artifact's own source, from the
|
|
4516
|
+
// same descriptions its action labels are written from. A control view is
|
|
4517
|
+
// the only grounding a controller host has for a status answer, and an
|
|
4518
|
+
// internal state id is not Boss-appropriate text
|
|
4519
|
+
// (CAPPLAY-5), so the runtime publishes the meaning
|
|
4520
|
+
// rather than leaving the host to substitute the identifier for it. A
|
|
4521
|
+
// state whose source declares no description publishes none: an id is
|
|
4522
|
+
// never promoted into a description by default.
|
|
4523
|
+
function stateDescriptionFor(state) {
|
|
4524
|
+
const keys = [
|
|
4525
|
+
...(state.stateId === undefined ? [] : [state.stateId]),
|
|
4526
|
+
...(typeof state.value === 'string' ? [state.value] : []),
|
|
4527
|
+
...state.activeStateIds,
|
|
4528
|
+
];
|
|
4529
|
+
for (const key of keys) {
|
|
4530
|
+
const description = stateDescriptions.get(key);
|
|
4531
|
+
if (description !== undefined)
|
|
4532
|
+
return description;
|
|
4533
|
+
}
|
|
4534
|
+
return undefined;
|
|
4535
|
+
}
|
|
4536
|
+
function receiptTracePayload(receipt) {
|
|
4537
|
+
return {
|
|
4538
|
+
disposition: receipt.disposition,
|
|
4539
|
+
...(receipt.disposition === 'rejected'
|
|
4540
|
+
? { reason: receipt.reason }
|
|
4541
|
+
: {}),
|
|
4542
|
+
...(receipt.disposition === 'failed' ? { error: receipt.error } : {}),
|
|
4543
|
+
...(receipt.disposition === 'executed' ? { run: receipt.run } : {}),
|
|
4544
|
+
};
|
|
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);
|
|
2451
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;
|
|
2452
4987
|
}
|
|
2453
|
-
return Object.keys(projected).length === 0 ? undefined : projected;
|
|
2454
4988
|
}
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
function
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
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`);
|
|
2474
5016
|
}
|
|
2475
|
-
return undefined;
|
|
2476
5017
|
}
|
|
2477
|
-
function
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
};
|
|
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;
|
|
2486
5052
|
}
|
|
2487
5053
|
const runtime = {
|
|
5054
|
+
...(retainedGenerationMetadata === undefined
|
|
5055
|
+
? {}
|
|
5056
|
+
: { retainedGenerationMetadata }),
|
|
2488
5057
|
async init(nextSession) {
|
|
2489
5058
|
if (initialized || disposed || disposalPromise !== undefined) {
|
|
2490
5059
|
throw new Error('createPlaybookRuntime.init: already initialized');
|
|
@@ -2498,6 +5067,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2498
5067
|
initInFlight = initialization;
|
|
2499
5068
|
const initTask = (async () => {
|
|
2500
5069
|
session = boundSession;
|
|
5070
|
+
syncDeferredReconciliationOverlay();
|
|
5071
|
+
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
2501
5072
|
savedPorts = boundSession.ports;
|
|
2502
5073
|
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
2503
5074
|
suppressInspectionEmissions = false;
|
|
@@ -2530,6 +5101,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2530
5101
|
}
|
|
2531
5102
|
if (activeSignal !== undefined)
|
|
2532
5103
|
return undefined;
|
|
5104
|
+
if (deferredSettlementClosure !== undefined)
|
|
5105
|
+
return undefined;
|
|
2533
5106
|
const pendingCall = nestedBridge.getPendingCall();
|
|
2534
5107
|
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
2535
5108
|
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
@@ -2553,6 +5126,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2553
5126
|
suspendedCall = {
|
|
2554
5127
|
...bridgeSuspendedCall,
|
|
2555
5128
|
...(turnId === undefined ? {} : { turnId }),
|
|
5129
|
+
...(hasGovernedPlayerStates
|
|
5130
|
+
? {
|
|
5131
|
+
effectBoundaryPrefixSequence: playbookCallEffectPrefixes.get(bridgeSuspendedCall.callId) ?? null,
|
|
5132
|
+
}
|
|
5133
|
+
: {}),
|
|
2556
5134
|
};
|
|
2557
5135
|
}
|
|
2558
5136
|
const state = currentState();
|
|
@@ -2561,9 +5139,23 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2561
5139
|
const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
2562
5140
|
const context = actor.getSnapshot()
|
|
2563
5141
|
.context;
|
|
2564
|
-
|
|
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;
|
|
2565
5157
|
return {
|
|
2566
|
-
schemaVersion:
|
|
5158
|
+
schemaVersion: 4,
|
|
2567
5159
|
playbookId: session.playbookId,
|
|
2568
5160
|
machine: machineSnapshot,
|
|
2569
5161
|
roleResumeTokens: snapshotRoleResumeTokens(),
|
|
@@ -2588,6 +5180,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2588
5180
|
sourceItem: pending.sourceItem,
|
|
2589
5181
|
},
|
|
2590
5182
|
],
|
|
5183
|
+
effectLedger: effectLedgerMirror,
|
|
5184
|
+
...(retainedEffectSourceSessionId === undefined
|
|
5185
|
+
? {}
|
|
5186
|
+
: { retainedEffectSourceSessionId }),
|
|
5187
|
+
...(retainedEffectReconciliation === undefined
|
|
5188
|
+
? {}
|
|
5189
|
+
: { retainedEffectReconciliation }),
|
|
5190
|
+
...(failedEffectAttempt === undefined
|
|
5191
|
+
? {}
|
|
5192
|
+
: { failedEffectAttempt }),
|
|
2591
5193
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2592
5194
|
};
|
|
2593
5195
|
},
|
|
@@ -2597,99 +5199,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2597
5199
|
// the session already started; the next public boundary continues
|
|
2598
5200
|
// the contiguous trace sequence.
|
|
2599
5201
|
async restore(nextSession, snapshot) {
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
const suspendedCall = boundSnapshot.suspendedCall;
|
|
2610
|
-
let priorExternalPlayerTokens;
|
|
2611
|
-
let externalStoreRestoreAttempted = false;
|
|
2612
|
-
initialized = true;
|
|
2613
|
-
let finishInitialization;
|
|
2614
|
-
const initialization = new Promise((resolve) => {
|
|
2615
|
-
finishInitialization = resolve;
|
|
2616
|
-
});
|
|
2617
|
-
initInFlight = initialization;
|
|
2618
|
-
const initTask = (async () => {
|
|
2619
|
-
session = boundSession;
|
|
2620
|
-
savedPorts = boundSession.ports;
|
|
2621
|
-
runtimePorts = createRuntimePorts(boundSession.ports);
|
|
2622
|
-
traceSequence = boundSnapshot.sequences.trace;
|
|
2623
|
-
turnSequence = boundSnapshot.sequences.turn;
|
|
2624
|
-
judgeCallSequence = boundSnapshot.sequences.judgeCall;
|
|
2625
|
-
playerCallSequence = boundSnapshot.sequences.playerCall;
|
|
2626
|
-
playbookCallSequence = boundSnapshot.sequences.playbookCall;
|
|
2627
|
-
captainCallSequence = boundSnapshot.sequences.captainCall ?? 0;
|
|
2628
|
-
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
2629
|
-
// apply boundary consumed trace numbers, so the persisted trace
|
|
2630
|
-
// counter is a collision-safe id floor here too, keeping
|
|
2631
|
-
// `apply-<n>` call ids unique across restore.
|
|
2632
|
-
applyCallSequence = boundSnapshot.sequences.trace;
|
|
2633
|
-
if (boundSession.playerSessions) {
|
|
2634
|
-
priorExternalPlayerTokens = snapshotRoleResumeTokens();
|
|
2635
|
-
externalStoreRestoreAttempted = true;
|
|
2636
|
-
}
|
|
2637
|
-
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
2638
|
-
nestedBridge.prepareRestore(suspendedCall);
|
|
2639
|
-
if (suspendedCall !== undefined) {
|
|
2640
|
-
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
2641
|
-
}
|
|
2642
|
-
suppressInspectionEmissions = true;
|
|
2643
|
-
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
2644
|
-
actor.start();
|
|
2645
|
-
if (controlPlaneError !== undefined)
|
|
2646
|
-
throw controlPlaneError;
|
|
2647
|
-
const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
|
|
2648
|
-
? {}
|
|
2649
|
-
: {
|
|
2650
|
-
pendingCall: {
|
|
2651
|
-
callId: suspendedCall.callId,
|
|
2652
|
-
playbookId: suspendedCall.playbookId,
|
|
2653
|
-
childSessionId: suspendedCall.childSessionId,
|
|
2654
|
-
},
|
|
2655
|
-
});
|
|
2656
|
-
if (restoredState.status !== 'active') {
|
|
2657
|
-
throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
|
|
2658
|
-
}
|
|
2659
|
-
if (stableJson(restoredState, 'restored runtime state') !==
|
|
2660
|
-
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
2661
|
-
throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
|
|
2662
|
-
}
|
|
2663
|
-
priorState = restoredState;
|
|
2664
|
-
await drainEmissions();
|
|
2665
|
-
suppressInspectionEmissions = false;
|
|
2666
|
-
// Final fallible step: after this publication the authoritative
|
|
2667
|
-
// child has rejoined ordinary resume/abort ownership, so no later
|
|
2668
|
-
// restore validation may trigger failed-start rollback.
|
|
2669
|
-
nestedBridge.confirmRestore();
|
|
2670
|
-
})();
|
|
2671
|
-
try {
|
|
2672
|
-
await initTask;
|
|
2673
|
-
}
|
|
2674
|
-
catch (error) {
|
|
2675
|
-
let failure = error;
|
|
2676
|
-
if (externalStoreRestoreAttempted &&
|
|
2677
|
-
priorExternalPlayerTokens !== undefined) {
|
|
2678
|
-
try {
|
|
2679
|
-
boundSession.playerSessions.restore(priorExternalPlayerTokens);
|
|
2680
|
-
}
|
|
2681
|
-
catch (rollbackError) {
|
|
2682
|
-
failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
2685
|
-
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
2686
|
-
throw failure;
|
|
2687
|
-
}
|
|
2688
|
-
finally {
|
|
2689
|
-
finishInitialization();
|
|
2690
|
-
if (initInFlight === initialization)
|
|
2691
|
-
initInFlight = undefined;
|
|
2692
|
-
}
|
|
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);
|
|
2693
5211
|
},
|
|
2694
5212
|
// DR-029 / PBRT-52: side-effect-free control view over the live
|
|
2695
5213
|
// snapshot, valid at parked quiescence outside an active boundary.
|
|
@@ -2705,14 +5223,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2705
5223
|
if (activeSignal !== undefined) {
|
|
2706
5224
|
throw new Error('createPlaybookRuntime.describe: another runtime turn is active');
|
|
2707
5225
|
}
|
|
5226
|
+
assertDeferredSettlementOpen('describe');
|
|
5227
|
+
refreshRetainedEffectFenceFromHost();
|
|
2708
5228
|
const snapshot = actor.getSnapshot();
|
|
2709
5229
|
const state = currentState();
|
|
2710
5230
|
const context = (snapshot.context ??
|
|
2711
5231
|
{});
|
|
2712
|
-
const pending =
|
|
5232
|
+
const pending = !hasUnresolvedReconciliation()
|
|
5233
|
+
? pendingBossQuestionForState(state, context)
|
|
5234
|
+
: undefined;
|
|
2713
5235
|
const lastError = normalizeErrorFull(context.lastError);
|
|
2714
5236
|
const projectedContext = projectControlContext(context);
|
|
2715
|
-
const stateDescription =
|
|
5237
|
+
const stateDescription = !hasUnresolvedReconciliation()
|
|
5238
|
+
? stateDescriptionFor(state)
|
|
5239
|
+
: undefined;
|
|
2716
5240
|
return deepFreeze({
|
|
2717
5241
|
state,
|
|
2718
5242
|
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
@@ -2745,6 +5269,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2745
5269
|
if (input === null || typeof input !== 'object') {
|
|
2746
5270
|
throw new TypeError('createPlaybookRuntime.apply: input must be an object');
|
|
2747
5271
|
}
|
|
5272
|
+
assertDeferredSettlementOpen('apply');
|
|
2748
5273
|
const { actionId, key, signal } = input;
|
|
2749
5274
|
if (typeof actionId !== 'string' || actionId.length === 0) {
|
|
2750
5275
|
throw new TypeError('createPlaybookRuntime.apply: actionId must be a non-empty string');
|
|
@@ -2772,11 +5297,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2772
5297
|
// An abort before acceptance ends the call with no receipt
|
|
2773
5298
|
// recorded, like every other pre-acceptance failure.
|
|
2774
5299
|
signal.throwIfAborted();
|
|
5300
|
+
refreshRetainedEffectFenceFromHost();
|
|
2775
5301
|
const turnId = ++turnSequence;
|
|
2776
5302
|
const callId = `apply-${++applyCallSequence}`;
|
|
2777
5303
|
const position = { turnId, callId };
|
|
2778
5304
|
activeTurnId = turnId;
|
|
5305
|
+
beginAutomaticReplayBoundary();
|
|
2779
5306
|
activeSignal = signal;
|
|
5307
|
+
activeAborts = abortReasonClassifier(signal);
|
|
5308
|
+
activeAbortEmission = undefined;
|
|
2780
5309
|
controlPlaneError = undefined;
|
|
2781
5310
|
// Every receipt variant is normalized and frozen where it is built,
|
|
2782
5311
|
// inside the guarded region, so the recording step below cannot
|
|
@@ -2822,9 +5351,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2822
5351
|
// final for their key. Past publication such a failure is therefore
|
|
2823
5352
|
// re-latched onto the emission channel, surfacing from the next
|
|
2824
5353
|
// public boundary's drain, and `apply` still does not throw past
|
|
2825
|
-
// acceptance (PBRT-52).
|
|
5354
|
+
// acceptance (PBRT-52). A delivery rejection causally identical to
|
|
5355
|
+
// this call's own abort reason evidences the cancellation and is
|
|
5356
|
+
// dropped — never carried to a later unrelated boundary
|
|
5357
|
+
// (slc/link.md §Abort).
|
|
2826
5358
|
const latchDeliveryFailure = (error) => {
|
|
2827
|
-
|
|
5359
|
+
if (isAbortFailure(error, signal))
|
|
5360
|
+
return;
|
|
5361
|
+
emissionFailure ??= { error };
|
|
2828
5362
|
};
|
|
2829
5363
|
try {
|
|
2830
5364
|
try {
|
|
@@ -2844,7 +5378,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2844
5378
|
key,
|
|
2845
5379
|
...receiptTracePayload({ disposition: 'rejected', reason }),
|
|
2846
5380
|
});
|
|
2847
|
-
await emitCallStarted('apply.started', 'apply.finished', identity, position, preAcceptanceFinish('apply.started trace sink rejected'));
|
|
5381
|
+
await emitCallStarted('apply.started', 'apply.finished', identity, position, signal, preAcceptanceFinish('apply.started trace sink rejected'));
|
|
2848
5382
|
// An abort may land while the awaited started emission drains
|
|
2849
5383
|
// (e.g. fired from the trace sink itself); the action must
|
|
2850
5384
|
// never execute after abort. Settle the already-started pair
|
|
@@ -2880,13 +5414,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2880
5414
|
// the key, so the action can never execute twice.
|
|
2881
5415
|
accepted = true;
|
|
2882
5416
|
try {
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
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
|
+
}
|
|
2887
5444
|
if (controlPlaneError !== undefined)
|
|
2888
5445
|
throw controlPlaneError;
|
|
2889
|
-
const run = runResultFor(settledOutcome(signal));
|
|
2890
5446
|
receipt = settledReceipt(run.outcome === 'failed' || run.outcome === 'aborted'
|
|
2891
5447
|
? {
|
|
2892
5448
|
disposition: 'failed',
|
|
@@ -2923,6 +5479,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2923
5479
|
catch (error) {
|
|
2924
5480
|
settlementError = error;
|
|
2925
5481
|
}
|
|
5482
|
+
// Exact cancellation is not a control-plane latch, but after apply
|
|
5483
|
+
// acceptance and before publication it is still settlement evidence
|
|
5484
|
+
// and therefore folds into the owed failed receipt (DR-036 §4).
|
|
5485
|
+
settlementError ??= activeAbortEmission;
|
|
2926
5486
|
// Fold before the finish emission, the last point at which the
|
|
2927
5487
|
// traced disposition and the returned one can still be made the
|
|
2928
5488
|
// same value.
|
|
@@ -2964,7 +5524,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2964
5524
|
// wedge every later public boundary behind "another runtime turn
|
|
2965
5525
|
// is active".
|
|
2966
5526
|
activeSignal = undefined;
|
|
5527
|
+
activeAborts = undefined;
|
|
5528
|
+
activeAbortEmission = undefined;
|
|
2967
5529
|
activeTurnId = undefined;
|
|
5530
|
+
activeGovernedBoundarySeen = false;
|
|
5531
|
+
activeGovernedAttemptId = undefined;
|
|
5532
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
2968
5533
|
controlPlaneError = undefined;
|
|
2969
5534
|
}
|
|
2970
5535
|
// Past acceptance every settlement failure has been folded into the
|
|
@@ -2986,6 +5551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2986
5551
|
}
|
|
2987
5552
|
return receipt;
|
|
2988
5553
|
},
|
|
5554
|
+
unresolvedEffectEnvelopes: unresolvedEffectEnvelopeIdentities,
|
|
2989
5555
|
async handleBossInput({ text, signal, }) {
|
|
2990
5556
|
if (!actor || !savedPorts) {
|
|
2991
5557
|
throw new Error('createPlaybookRuntime.handleBossInput: init must be called first');
|
|
@@ -2996,122 +5562,247 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2996
5562
|
if (activeSignal !== undefined) {
|
|
2997
5563
|
throw new Error('createPlaybookRuntime.handleBossInput: another runtime turn is active');
|
|
2998
5564
|
}
|
|
5565
|
+
assertDeferredSettlementOpen('handleBossInput');
|
|
5566
|
+
refreshRetainedEffectFenceFromHost();
|
|
2999
5567
|
const turnId = ++turnSequence;
|
|
3000
5568
|
activeTurnId = turnId;
|
|
5569
|
+
beginAutomaticReplayBoundary();
|
|
3001
5570
|
activeSignal = signal;
|
|
5571
|
+
activeAborts = abortReasonClassifier(signal);
|
|
5572
|
+
activeAbortEmission = undefined;
|
|
3002
5573
|
controlPlaneError = undefined;
|
|
3003
5574
|
let result;
|
|
3004
5575
|
let operationError;
|
|
5576
|
+
// The boundary sentinel releases on every exit: a settlement defect
|
|
5577
|
+
// past the drain — a snapshot normalization throw inside
|
|
5578
|
+
// `runResultFor` included — must never wedge every later public
|
|
5579
|
+
// boundary and `dispose` itself behind "another runtime turn is
|
|
5580
|
+
// active". Mirrors the apply boundary's finally.
|
|
3005
5581
|
try {
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
5582
|
+
try {
|
|
5583
|
+
await emitTrace('boss.input.received', { text }, { turnId });
|
|
5584
|
+
// Record the attempted input, then refuse a boundary that entered
|
|
5585
|
+
// aborted before deterministic mapping or the classifier can
|
|
5586
|
+
// perform host-visible work (DR-036 §5).
|
|
5587
|
+
signal.throwIfAborted();
|
|
5588
|
+
// 1. Map the Boss text to an FSM event: deterministic exact entry
|
|
5589
|
+
// where applicable (slc/link.md §Boss-event mapping), judge
|
|
5590
|
+
// classification otherwise.
|
|
5591
|
+
let event;
|
|
5592
|
+
let classifiedSnapshot;
|
|
5593
|
+
let deferredPending;
|
|
5594
|
+
let deferredOperation;
|
|
5595
|
+
let pendingRequiresDeferredBinding = false;
|
|
5596
|
+
const trimmed = text.trim();
|
|
5597
|
+
if (trimmed !== '') {
|
|
5598
|
+
const snapshot = actor.getSnapshot();
|
|
5599
|
+
classifiedSnapshot = snapshot;
|
|
5600
|
+
const terminal = snapshot.status === 'done';
|
|
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);
|
|
5612
|
+
// PBRT-1 / slc/link.md §Boss-event mapping: the idle entry, the
|
|
5613
|
+
// recoverable failure state, and the reconstructed terminal all
|
|
5614
|
+
// accept exactly one ordinary textual entry event, so delivered
|
|
5615
|
+
// text enters deterministically — no judge call to spend and no
|
|
5616
|
+
// classifier whim to settle a restart as no action. Every other
|
|
5617
|
+
// parked state — a reply wait or an authored mid-workflow
|
|
5618
|
+
// checkpoint — classifies under its own Boss-event contracts.
|
|
5619
|
+
if (hasUnresolvedReconciliation()) {
|
|
5620
|
+
event = undefined;
|
|
5621
|
+
}
|
|
5622
|
+
else if (stateId === 'failed' &&
|
|
5623
|
+
!failedAttemptAllowsReplay()) {
|
|
5624
|
+
event = undefined;
|
|
5625
|
+
}
|
|
5626
|
+
else if (spec.entryEvent !== undefined &&
|
|
5627
|
+
(stateId === 'ready' || stateId === 'failed' || terminal)) {
|
|
5628
|
+
event = {
|
|
5629
|
+
type: spec.entryEvent.type,
|
|
5630
|
+
[spec.entryEvent.textField]: text,
|
|
5631
|
+
};
|
|
5632
|
+
}
|
|
5633
|
+
else {
|
|
5634
|
+
event = await classifyBossText(text, runtimePorts, signal, snapshot, boundary, boundOptions);
|
|
5635
|
+
}
|
|
5636
|
+
signal.throwIfAborted();
|
|
3022
5637
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
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`);
|
|
3025
5645
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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
|
+
}
|
|
3039
5685
|
}
|
|
3040
|
-
|
|
3041
|
-
//
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
5686
|
+
// Empty input, no-action classifier output, or invalid classifier
|
|
5687
|
+
// output — nothing to send.
|
|
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) {
|
|
5693
|
+
result = runResultFor('no-action');
|
|
5694
|
+
}
|
|
5695
|
+
else {
|
|
5696
|
+
// 2. Optional Captain-pane classification line: the bare FSM
|
|
5697
|
+
// event type, emitted before the FSM advances.
|
|
5698
|
+
const statusLine = classificationStatus(event);
|
|
5699
|
+
if (statusLine !== undefined) {
|
|
5700
|
+
await runtimePorts.emitStatus(statusLine);
|
|
5701
|
+
}
|
|
5702
|
+
signal.throwIfAborted();
|
|
5703
|
+
// 3. A final actor cannot accept new events; reconstruct only
|
|
5704
|
+
// after classification produced a real event.
|
|
5705
|
+
if (actor.getSnapshot().status === 'done') {
|
|
5706
|
+
stopActor();
|
|
5707
|
+
actor = buildActor(runtimePorts);
|
|
5708
|
+
// The replacement actor's snapshots are real state entries.
|
|
5709
|
+
suppressInspectionEmissions = false;
|
|
5710
|
+
actor.start();
|
|
5711
|
+
}
|
|
5712
|
+
// DR-029: keep the classified event with its recorded payload
|
|
5713
|
+
// as the retry-replay source. Recording is sanitizing, not
|
|
5714
|
+
// load-bearing: an override classifier's non-JSON-safe event is
|
|
5715
|
+
// simply not recorded, and the turn proceeds unchanged.
|
|
5716
|
+
try {
|
|
5717
|
+
lastBossEvent = snapshotJsonValue(event, 'recorded Boss event');
|
|
5718
|
+
}
|
|
5719
|
+
catch {
|
|
5720
|
+
lastBossEvent = undefined;
|
|
5721
|
+
}
|
|
5722
|
+
actor.send(event);
|
|
5723
|
+
await waitForPlaybookQuiescence(actor, {
|
|
5724
|
+
pendingCalls: nestedBridge,
|
|
5725
|
+
});
|
|
5726
|
+
if (controlPlaneError !== undefined)
|
|
5727
|
+
throw controlPlaneError;
|
|
5728
|
+
result = runResultFor(settledOutcome(signal));
|
|
3049
5729
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
5730
|
+
}
|
|
5731
|
+
catch (error) {
|
|
5732
|
+
operationError = error;
|
|
5733
|
+
}
|
|
5734
|
+
let drainError;
|
|
5735
|
+
try {
|
|
5736
|
+
await drainEmissions();
|
|
5737
|
+
}
|
|
5738
|
+
catch (error) {
|
|
5739
|
+
drainError = error;
|
|
5740
|
+
}
|
|
5741
|
+
const latchedControlError = controlPlaneError;
|
|
5742
|
+
// A drain rejection that is the exact abort reason evidences the
|
|
5743
|
+
// cancellation, not a control-plane failure (slc/link.md §Abort).
|
|
5744
|
+
const drainAbort = drainError !== undefined && isAbortFailure(drainError, signal);
|
|
5745
|
+
const effectiveDrainError = drainAbort ? undefined : drainError;
|
|
5746
|
+
const primaryError = latchedControlError ?? effectiveDrainError ?? operationError;
|
|
5747
|
+
const abortError = latchedControlError === undefined &&
|
|
5748
|
+
effectiveDrainError === undefined &&
|
|
5749
|
+
((operationError !== undefined &&
|
|
5750
|
+
isAbortFailure(operationError, signal)) ||
|
|
5751
|
+
(drainAbort && operationError === undefined));
|
|
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);
|
|
5762
|
+
let settlementEmissionError;
|
|
5763
|
+
if (settlementResult !== undefined) {
|
|
3054
5764
|
try {
|
|
3055
|
-
|
|
5765
|
+
await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
|
|
3056
5766
|
}
|
|
3057
|
-
catch {
|
|
3058
|
-
|
|
5767
|
+
catch (error) {
|
|
5768
|
+
settlementEmissionError = error;
|
|
3059
5769
|
}
|
|
3060
|
-
actor.send(event);
|
|
3061
|
-
await waitForPlaybookQuiescence(actor, {
|
|
3062
|
-
pendingCalls: nestedBridge,
|
|
3063
|
-
});
|
|
3064
|
-
if (controlPlaneError !== undefined)
|
|
3065
|
-
throw controlPlaneError;
|
|
3066
|
-
result = runResultFor(settledOutcome(signal));
|
|
3067
5770
|
}
|
|
5771
|
+
try {
|
|
5772
|
+
await drainEmissions();
|
|
5773
|
+
}
|
|
5774
|
+
catch (error) {
|
|
5775
|
+
settlementEmissionError ??= error;
|
|
5776
|
+
}
|
|
5777
|
+
if (settlementEmissionError !== undefined &&
|
|
5778
|
+
isAbortFailure(settlementEmissionError, signal)) {
|
|
5779
|
+
settlementEmissionError = undefined;
|
|
5780
|
+
}
|
|
5781
|
+
const failure = controlPlaneError ??
|
|
5782
|
+
latchedControlError ??
|
|
5783
|
+
effectiveDrainError ??
|
|
5784
|
+
(abortError
|
|
5785
|
+
? (settlementEmissionError ?? operationError)
|
|
5786
|
+
: (operationError ?? settlementEmissionError));
|
|
5787
|
+
if (failure !== undefined &&
|
|
5788
|
+
!(abortError && settlementEmissionError === undefined)) {
|
|
5789
|
+
throw failure;
|
|
5790
|
+
}
|
|
5791
|
+
if (settlementResult === undefined) {
|
|
5792
|
+
throw deferredSettlementClosure;
|
|
5793
|
+
}
|
|
5794
|
+
return settlementResult;
|
|
3068
5795
|
}
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
}
|
|
3079
|
-
const latchedControlError = controlPlaneError;
|
|
3080
|
-
const primaryError = latchedControlError ?? drainError ?? operationError;
|
|
3081
|
-
const abortError = latchedControlError === undefined &&
|
|
3082
|
-
drainError === undefined &&
|
|
3083
|
-
operationError !== undefined &&
|
|
3084
|
-
isAbortFailure(operationError, signal);
|
|
3085
|
-
const settlementResult = primaryError === undefined
|
|
3086
|
-
? (result ?? runResultFor('no-action'))
|
|
3087
|
-
: runResultFor(abortError ? 'aborted' : 'failed', primaryError);
|
|
3088
|
-
let settlementEmissionError;
|
|
3089
|
-
try {
|
|
3090
|
-
await emitTrace('boss.input.settled', settlementTracePayload(settlementResult), { turnId });
|
|
3091
|
-
}
|
|
3092
|
-
catch (error) {
|
|
3093
|
-
settlementEmissionError = error;
|
|
3094
|
-
}
|
|
3095
|
-
try {
|
|
3096
|
-
await drainEmissions();
|
|
3097
|
-
}
|
|
3098
|
-
catch (error) {
|
|
3099
|
-
settlementEmissionError ??= error;
|
|
3100
|
-
}
|
|
3101
|
-
const failure = controlPlaneError ??
|
|
3102
|
-
latchedControlError ??
|
|
3103
|
-
drainError ??
|
|
3104
|
-
(abortError
|
|
3105
|
-
? (settlementEmissionError ?? operationError)
|
|
3106
|
-
: (operationError ?? settlementEmissionError));
|
|
3107
|
-
activeSignal = undefined;
|
|
3108
|
-
activeTurnId = undefined;
|
|
3109
|
-
controlPlaneError = undefined;
|
|
3110
|
-
if (failure !== undefined &&
|
|
3111
|
-
!(abortError && settlementEmissionError === undefined)) {
|
|
3112
|
-
throw failure;
|
|
5796
|
+
finally {
|
|
5797
|
+
activeSignal = undefined;
|
|
5798
|
+
activeAborts = undefined;
|
|
5799
|
+
activeAbortEmission = undefined;
|
|
5800
|
+
activeTurnId = undefined;
|
|
5801
|
+
activeGovernedBoundarySeen = false;
|
|
5802
|
+
activeGovernedAttemptId = undefined;
|
|
5803
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
5804
|
+
controlPlaneError = undefined;
|
|
3113
5805
|
}
|
|
3114
|
-
return settlementResult;
|
|
3115
5806
|
},
|
|
3116
5807
|
async resumePlaybookCall(input) {
|
|
3117
5808
|
if (!actor || !savedPorts) {
|
|
@@ -3123,43 +5814,99 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3123
5814
|
if (activeSignal !== undefined) {
|
|
3124
5815
|
throw new Error('createPlaybookRuntime.resumePlaybookCall: another runtime turn is active');
|
|
3125
5816
|
}
|
|
5817
|
+
refreshRetainedEffectFenceFromHost();
|
|
5818
|
+
if (hasUnresolvedReconciliation()) {
|
|
5819
|
+
return runResultFor('no-action');
|
|
5820
|
+
}
|
|
3126
5821
|
activeTurnId = playbookCallTurnIds.get(input.callId);
|
|
5822
|
+
bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
|
|
5823
|
+
? playbookCallEffectPrefixes.get(input.callId)
|
|
5824
|
+
: hasGovernedPlayerStates
|
|
5825
|
+
? 0
|
|
5826
|
+
: undefined);
|
|
3127
5827
|
activeSignal = input.signal;
|
|
5828
|
+
activeAborts = abortReasonClassifier(input.signal);
|
|
5829
|
+
activeAbortEmission = undefined;
|
|
3128
5830
|
controlPlaneError = undefined;
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
try {
|
|
3132
|
-
await nestedBridge.resume(input);
|
|
3133
|
-
}
|
|
3134
|
-
catch (error) {
|
|
3135
|
-
operationError = error;
|
|
3136
|
-
}
|
|
3137
|
-
try {
|
|
3138
|
-
await waitForPlaybookQuiescence(actor, {
|
|
3139
|
-
pendingCalls: nestedBridge,
|
|
3140
|
-
});
|
|
3141
|
-
result = runResultFor(settledOutcome(input.signal));
|
|
3142
|
-
}
|
|
3143
|
-
catch (error) {
|
|
3144
|
-
operationError ??= error;
|
|
3145
|
-
}
|
|
3146
|
-
let drainError;
|
|
5831
|
+
// The boundary sentinel releases on every exit, mirroring
|
|
5832
|
+
// `handleBossInput` and the apply boundary.
|
|
3147
5833
|
try {
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
5834
|
+
let result;
|
|
5835
|
+
let operationError;
|
|
5836
|
+
try {
|
|
5837
|
+
await nestedBridge.resume(input);
|
|
5838
|
+
}
|
|
5839
|
+
catch (error) {
|
|
5840
|
+
operationError = error;
|
|
5841
|
+
}
|
|
5842
|
+
try {
|
|
5843
|
+
await waitForPlaybookQuiescence(actor, {
|
|
5844
|
+
pendingCalls: nestedBridge,
|
|
5845
|
+
});
|
|
5846
|
+
result = runResultFor(settledOutcome(input.signal));
|
|
5847
|
+
}
|
|
5848
|
+
catch (error) {
|
|
5849
|
+
operationError ??= error;
|
|
5850
|
+
}
|
|
5851
|
+
// A resume refused because its signal was already aborted
|
|
5852
|
+
// delivers nothing: the pending call survives for a later
|
|
5853
|
+
// resume, and the boundary settles `aborted` rather than
|
|
5854
|
+
// advertising `suspended` (slc/link.md §Nested playbook bridge).
|
|
5855
|
+
if (operationError !== undefined &&
|
|
5856
|
+
isAbortFailure(operationError, input.signal) &&
|
|
5857
|
+
nestedBridge.getPendingCall()?.callId === input.callId) {
|
|
5858
|
+
result = {
|
|
5859
|
+
outcome: 'aborted',
|
|
5860
|
+
state: currentState(),
|
|
5861
|
+
error: normalizeError(input.signal.reason),
|
|
5862
|
+
};
|
|
5863
|
+
}
|
|
5864
|
+
let drainError;
|
|
5865
|
+
try {
|
|
5866
|
+
await drainEmissions();
|
|
5867
|
+
}
|
|
5868
|
+
catch (error) {
|
|
5869
|
+
drainError = error;
|
|
5870
|
+
}
|
|
5871
|
+
const aborts = activeAborts ?? abortReasonClassifier(input.signal);
|
|
5872
|
+
// A control-plane latch has already classified its failure as
|
|
5873
|
+
// distinct under the owning operation. Never reinterpret it
|
|
5874
|
+
// against this later resume signal (DR-036 decision 2).
|
|
5875
|
+
const controlFailure = controlPlaneError;
|
|
5876
|
+
const drainAbort = controlFailure === undefined &&
|
|
5877
|
+
drainError !== undefined &&
|
|
5878
|
+
aborts.isAbortReason(drainError);
|
|
5879
|
+
const operationAbort = controlFailure === undefined &&
|
|
5880
|
+
operationError !== undefined &&
|
|
5881
|
+
aborts.isAbortReason(operationError);
|
|
5882
|
+
const abortEvidence = activeAbortEmission ??
|
|
5883
|
+
(drainAbort ? drainError : undefined) ??
|
|
5884
|
+
(operationAbort ? operationError : undefined);
|
|
5885
|
+
const failure = controlFailure ??
|
|
5886
|
+
(drainAbort ? undefined : drainError) ??
|
|
5887
|
+
(operationAbort ? undefined : operationError);
|
|
5888
|
+
if (failure !== undefined)
|
|
5889
|
+
throw failure;
|
|
5890
|
+
if (abortEvidence !== undefined &&
|
|
5891
|
+
result?.outcome !== 'terminal' &&
|
|
5892
|
+
result?.outcome !== 'suspended') {
|
|
5893
|
+
result = runResultFor('aborted', abortEvidence);
|
|
5894
|
+
}
|
|
5895
|
+
if (result === undefined) {
|
|
5896
|
+
throw new Error('playbook resume produced no runtime result');
|
|
5897
|
+
}
|
|
5898
|
+
return result;
|
|
3152
5899
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
5900
|
+
finally {
|
|
5901
|
+
activeSignal = undefined;
|
|
5902
|
+
activeAborts = undefined;
|
|
5903
|
+
activeAbortEmission = undefined;
|
|
5904
|
+
activeTurnId = undefined;
|
|
5905
|
+
activeGovernedBoundarySeen = false;
|
|
5906
|
+
activeGovernedAttemptId = undefined;
|
|
5907
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
5908
|
+
controlPlaneError = undefined;
|
|
3161
5909
|
}
|
|
3162
|
-
return result;
|
|
3163
5910
|
},
|
|
3164
5911
|
dispose() {
|
|
3165
5912
|
if (disposalPromise !== undefined)
|
|
@@ -3222,16 +5969,42 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3222
5969
|
}
|
|
3223
5970
|
activePlayerKeys.clear();
|
|
3224
5971
|
playbookCallTurnIds.clear();
|
|
5972
|
+
playbookCallEffectPrefixes.clear();
|
|
3225
5973
|
activeEmissionCalls.clear();
|
|
3226
5974
|
emissionQueue.clear();
|
|
3227
5975
|
judgeQueue.clear();
|
|
3228
5976
|
appliedReceipts.clear();
|
|
3229
5977
|
actor = undefined;
|
|
3230
5978
|
activeSignal = undefined;
|
|
5979
|
+
activeAborts = undefined;
|
|
5980
|
+
actorSettlementAborts = undefined;
|
|
5981
|
+
actorSettlementErrorAborts = undefined;
|
|
5982
|
+
activeAbortEmission = undefined;
|
|
3231
5983
|
activeTurnId = undefined;
|
|
5984
|
+
activeGovernedBoundarySeen = false;
|
|
5985
|
+
activeGovernedAttemptId = undefined;
|
|
5986
|
+
activeEffectLedgerPrefixSequence = undefined;
|
|
5987
|
+
failedGovernedAttemptUnknown = false;
|
|
5988
|
+
failedEffectBoundaryPrefix = undefined;
|
|
5989
|
+
failedGovernedAttemptId = undefined;
|
|
3232
5990
|
controlPlaneError = undefined;
|
|
3233
5991
|
emissionFailure = undefined;
|
|
3234
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 = [];
|
|
3235
6008
|
savedPorts = undefined;
|
|
3236
6009
|
runtimePorts = undefined;
|
|
3237
6010
|
session = undefined;
|
|
@@ -3261,4 +6034,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3261
6034
|
};
|
|
3262
6035
|
return runtime;
|
|
3263
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;
|
|
3264
6044
|
}
|