@sublang/playbook 15.0.0 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/embedding.md +9 -7
- package/package.json +3 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
- package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
- package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
- package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
- package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
- package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
- package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
- package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
- package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
- package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
- package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
- package/reference/sdlc/code.playbook/code.fsm.js +609 -649
- package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
- package/reference/sdlc/code.playbook/code.gears.md +39 -29
- package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
- package/reference/sdlc/code.playbook/code.playbook.js +166 -147
- package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
- package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
- package/reference/sdlc/code.playbook/code.registry.ts +6 -2
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
- package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
- package/reference/sdlc/decide.md +1 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
- package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
- package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
- package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
- package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
- package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
- package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
- package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
- package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
- package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
- package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
- package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
- package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
- package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
- package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
- package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
- package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
- package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
- package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
- package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
- package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
- package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
- package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
- package/reference/sdlc/review.md +3 -3
- package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
- package/reference/sdlc/review.playbook/review.fsm.js +479 -550
- package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
- package/reference/sdlc/review.playbook/review.gears.md +32 -29
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
- package/reference/sdlc/review.playbook/review.playbook.js +180 -155
- package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
- package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
- package/reference/sdlc/review.playbook/review.registry.js +3 -3
- package/reference/sdlc/review.playbook/review.registry.ts +9 -5
- package/slc/gears2fsm.md +72 -3
- package/slc/link.md +50 -27
- package/slc/materialize-link.mjs +51 -13
- package/slc/optimize.md +4 -1
- package/slc/prefix-prompts.mjs +191 -0
- package/slc/prefix.md +203 -0
- package/slc/slc.pin-inputs.json +11 -1
- package/slc/text2gears.md +71 -11
- package/src/xstate-playbook-runtime.d.ts +82 -9
- package/src/xstate-playbook-runtime.js +1224 -283
- package/src/xstate-playbook-runtime.ts +1671 -338
- package/src/xstate-runtime.d.ts +3 -1
- package/src/xstate-runtime.js +9 -2
- package/src/xstate-runtime.ts +12 -2
|
@@ -70,11 +70,11 @@ const GOVERNED_JUDGE_FAILURE_REASONS = [
|
|
|
70
70
|
'corrective semantic candidate is invalid',
|
|
71
71
|
'semantic correction budget is unavailable',
|
|
72
72
|
];
|
|
73
|
-
const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
|
|
73
|
+
export const ABORTED_FAILURE_CAUSE = assertPlaybookFailureCause({
|
|
74
74
|
code: 'aborted',
|
|
75
75
|
evidence: {},
|
|
76
76
|
});
|
|
77
|
-
function runtimeDefectCause(reason) {
|
|
77
|
+
export function runtimeDefectCause(reason) {
|
|
78
78
|
return assertPlaybookFailureCause({
|
|
79
79
|
code: 'runtime-defect',
|
|
80
80
|
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
@@ -98,7 +98,7 @@ function failureErrorCode(error) {
|
|
|
98
98
|
return undefined;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
function playerFailureCause(input) {
|
|
101
|
+
export function playerFailureCause(input) {
|
|
102
102
|
return assertPlaybookFailureCause({
|
|
103
103
|
code: 'player-failed',
|
|
104
104
|
evidence: {
|
|
@@ -114,6 +114,67 @@ function playerFailureCause(input) {
|
|
|
114
114
|
},
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
119
|
+
* mismatch supplies its own receipt-read cause; the reasons a runtime owns map
|
|
120
|
+
* to adjudication, abort, and runtime-defect codes. Exported so linked
|
|
121
|
+
* machinery of an artifact's own decides the same reasons.
|
|
122
|
+
*/
|
|
123
|
+
export function governedSettlementCause(reason, error, aborted, supplied) {
|
|
124
|
+
if (supplied !== undefined)
|
|
125
|
+
return supplied;
|
|
126
|
+
if (aborted || reason.includes('aborted'))
|
|
127
|
+
return ABORTED_FAILURE_CAUSE;
|
|
128
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
129
|
+
const transport = failureErrorEvidence(error);
|
|
130
|
+
return assertPlaybookFailureCause({
|
|
131
|
+
code: 'judge-failed',
|
|
132
|
+
evidence: {
|
|
133
|
+
reason,
|
|
134
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return runtimeDefectCause(reason);
|
|
139
|
+
}
|
|
140
|
+
const FAILURE_CAUSE_RETENTION_LIMIT = 8;
|
|
141
|
+
export function createFailureCauseRetention() {
|
|
142
|
+
const causes = new Map();
|
|
143
|
+
return {
|
|
144
|
+
retain(error, cause) {
|
|
145
|
+
if (error === undefined || error === null)
|
|
146
|
+
return;
|
|
147
|
+
causes.delete(error);
|
|
148
|
+
causes.set(error, cause);
|
|
149
|
+
while (causes.size > FAILURE_CAUSE_RETENTION_LIMIT) {
|
|
150
|
+
causes.delete(causes.keys().next().value);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
causeOf(error) {
|
|
154
|
+
if (error === undefined || error === null)
|
|
155
|
+
return undefined;
|
|
156
|
+
return normalizeError(error).cause ?? causes.get(error);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* DR-063 §2: the value an invoked actor's rejection carried into the machine,
|
|
162
|
+
* read from the XState error event that delivered it, or `undefined` for any
|
|
163
|
+
* other event.
|
|
164
|
+
*/
|
|
165
|
+
function actorErrorOf(event) {
|
|
166
|
+
if (typeof event !== 'object' || event === null)
|
|
167
|
+
return undefined;
|
|
168
|
+
try {
|
|
169
|
+
const { type, error } = event;
|
|
170
|
+
return typeof type === 'string' && type.startsWith('xstate.error.actor.')
|
|
171
|
+
? error
|
|
172
|
+
: undefined;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
117
178
|
// ---------------------------------------------------------------------------
|
|
118
179
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
119
180
|
// missing, empty, or whitespace-only under one empty predicate, and that
|
|
@@ -199,7 +260,7 @@ function configuredOptionsFromFactoryInput(value, label) {
|
|
|
199
260
|
effectLedger: effectLedger,
|
|
200
261
|
};
|
|
201
262
|
}
|
|
202
|
-
function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
|
|
263
|
+
function repositoryCapabilityFromHostCapabilities(hostCapabilities, label, requireCohort = false) {
|
|
203
264
|
const descriptor = hostCapabilities === undefined
|
|
204
265
|
? undefined
|
|
205
266
|
: Object.getOwnPropertyDescriptor(hostCapabilities, 'repository');
|
|
@@ -213,8 +274,13 @@ function repositoryCapabilityFromHostCapabilities(hostCapabilities, label) {
|
|
|
213
274
|
Array.isArray(repository) ||
|
|
214
275
|
typeof repository.runExclusive !==
|
|
215
276
|
'function' ||
|
|
216
|
-
typeof repository.runDeferred !==
|
|
217
|
-
|
|
277
|
+
typeof repository.runDeferred !==
|
|
278
|
+
'function' ||
|
|
279
|
+
(requireCohort &&
|
|
280
|
+
typeof repository.runCohort !== 'function')) {
|
|
281
|
+
throw new TypeError(requireCohort
|
|
282
|
+
? `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive, runCohort, and runDeferred`
|
|
283
|
+
: `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`);
|
|
218
284
|
}
|
|
219
285
|
return repository;
|
|
220
286
|
}
|
|
@@ -405,7 +471,7 @@ export function normalizeErrorFull(err) {
|
|
|
405
471
|
// slc/link.md §Abort: cancellation is causal identity with the applicable
|
|
406
472
|
// signal's reason — never an `AbortError` name, never bare signal state. A
|
|
407
473
|
// distinct failure observed while the signal is aborted stays a non-abort
|
|
408
|
-
// control error and takes precedence
|
|
474
|
+
// control error and takes precedence.
|
|
409
475
|
function isAbortFailure(error, signal) {
|
|
410
476
|
return signal.aborted && Object.is(error, signal.reason);
|
|
411
477
|
}
|
|
@@ -436,7 +502,32 @@ function pendingBossQuestionForState(state, context) {
|
|
|
436
502
|
}
|
|
437
503
|
/** Read the FSM context's single pending Boss question, when well-formed. */
|
|
438
504
|
export function pendingBossQuestionFromContext(context) {
|
|
439
|
-
|
|
505
|
+
return pendingBossQuestionRecord(context.pendingBossQuestion);
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* DR-067 §1: the keyed form of gears2fsm's Boss-reply suspension. Each
|
|
509
|
+
* well-formed `context.pendingBossQuestions[stateId]` record whose own
|
|
510
|
+
* `questionId` equals its key counts only while a wait state that resumes
|
|
511
|
+
* its `resumeStateId` is active, so an answered question the context
|
|
512
|
+
* retains for the resumed prompt never reads as pending. Ordered by
|
|
513
|
+
* question id.
|
|
514
|
+
*/
|
|
515
|
+
function keyedPendingBossQuestions(context, resumesFromActiveWait) {
|
|
516
|
+
const keyed = context.pendingBossQuestions;
|
|
517
|
+
if (!isPlainObject(keyed))
|
|
518
|
+
return [];
|
|
519
|
+
const pending = [];
|
|
520
|
+
for (const [key, value] of Object.entries(keyed)) {
|
|
521
|
+
const record = pendingBossQuestionRecord(value);
|
|
522
|
+
if (record === undefined || record.questionId !== key)
|
|
523
|
+
continue;
|
|
524
|
+
if (!resumesFromActiveWait(record.resumeStateId))
|
|
525
|
+
continue;
|
|
526
|
+
pending.push(record);
|
|
527
|
+
}
|
|
528
|
+
return pending.sort((left, right) => left.questionId.localeCompare(right.questionId));
|
|
529
|
+
}
|
|
530
|
+
function pendingBossQuestionRecord(pending) {
|
|
440
531
|
if (pending === undefined ||
|
|
441
532
|
pending === null ||
|
|
442
533
|
typeof pending !== 'object') {
|
|
@@ -578,7 +669,7 @@ function preExistingChangesBlock(baseline) {
|
|
|
578
669
|
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
579
670
|
* nothing.
|
|
580
671
|
*/
|
|
581
|
-
function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
|
|
672
|
+
export function effectAuthorizedPreExistingBlock(effectBoundary, baseline) {
|
|
582
673
|
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
583
674
|
? preExistingChangesBlock(baseline)
|
|
584
675
|
: undefined;
|
|
@@ -808,8 +899,8 @@ export function defaultBuildJudgePrompt(input, finalText) {
|
|
|
808
899
|
* so the judge omits it. Rendering the clause verbatim asked the judge for
|
|
809
900
|
* `question`, `planningResult`, or `evaluatedRevision`, which the reconciler
|
|
810
901
|
* rejects as a structural error, spending the single correction on a
|
|
811
|
-
* self-inflicted defect. Exported so
|
|
812
|
-
*
|
|
902
|
+
* self-inflicted defect. Exported so linked machinery of an artifact's own
|
|
903
|
+
* renders the identical contract instead of restating it.
|
|
813
904
|
*/
|
|
814
905
|
export function renderGovernedOutcomeContract(guard, description, outcome) {
|
|
815
906
|
const { meaning, clause } = splitOutputClause(description);
|
|
@@ -997,7 +1088,7 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
|
|
|
997
1088
|
}
|
|
998
1089
|
if (result.status !== 'ok') {
|
|
999
1090
|
const failure = new Error(result.error ?? `captainBridge: callPlayer status "${result.status}"`);
|
|
1000
|
-
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
1091
|
+
throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
|
|
1001
1092
|
}
|
|
1002
1093
|
const finalText = result.finalText ?? '';
|
|
1003
1094
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -1177,20 +1268,293 @@ function transitionTargets(transition) {
|
|
|
1177
1268
|
}
|
|
1178
1269
|
return targets;
|
|
1179
1270
|
}
|
|
1180
|
-
|
|
1271
|
+
function transitionArmTargets(transition) {
|
|
1272
|
+
const arms = Array.isArray(transition) ? transition : [transition];
|
|
1273
|
+
const targets = [];
|
|
1274
|
+
for (const arm of arms) {
|
|
1275
|
+
if (typeof arm === 'string') {
|
|
1276
|
+
targets.push(arm);
|
|
1277
|
+
}
|
|
1278
|
+
else if (isPlainObject(arm) && typeof arm.target === 'string') {
|
|
1279
|
+
targets.push(arm.target);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return targets;
|
|
1283
|
+
}
|
|
1284
|
+
function playbookStateIdOf(stateDef) {
|
|
1285
|
+
if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
|
|
1286
|
+
return undefined;
|
|
1287
|
+
}
|
|
1288
|
+
const playbook = stateDef.meta.playbook;
|
|
1289
|
+
return isPlainObject(playbook) && typeof playbook.stateId === 'string'
|
|
1290
|
+
? playbook.stateId
|
|
1291
|
+
: undefined;
|
|
1292
|
+
}
|
|
1293
|
+
function stateTagsOf(stateDef) {
|
|
1294
|
+
const tags = stateDef.tags;
|
|
1295
|
+
if (typeof tags === 'string')
|
|
1296
|
+
return [tags];
|
|
1297
|
+
return Array.isArray(tags)
|
|
1298
|
+
? tags.filter((tag) => typeof tag === 'string')
|
|
1299
|
+
: [];
|
|
1300
|
+
}
|
|
1301
|
+
/** Every explicit node `id` in the config, mapped to its playbook state id. */
|
|
1302
|
+
function playbookStateIdsByNodeId(config) {
|
|
1303
|
+
const byNodeId = new Map();
|
|
1304
|
+
const visit = (stateDef) => {
|
|
1305
|
+
if (!isPlainObject(stateDef))
|
|
1306
|
+
return;
|
|
1307
|
+
const stateId = playbookStateIdOf(stateDef);
|
|
1308
|
+
if (typeof stateDef.id === 'string' && stateId !== undefined) {
|
|
1309
|
+
byNodeId.set(stateDef.id, stateId);
|
|
1310
|
+
}
|
|
1311
|
+
if (isPlainObject(stateDef.states)) {
|
|
1312
|
+
for (const child of Object.values(stateDef.states))
|
|
1313
|
+
visit(child);
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1316
|
+
if (isPlainObject(config) && isPlainObject(config.states)) {
|
|
1317
|
+
for (const child of Object.values(config.states))
|
|
1318
|
+
visit(child);
|
|
1319
|
+
}
|
|
1320
|
+
return byNodeId;
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* One transition target written inside a parallel region, resolved to the
|
|
1324
|
+
* playbook state id it names: an `#id` reference through the node ids, and a
|
|
1325
|
+
* bare sibling key through the region's own leaves.
|
|
1326
|
+
*/
|
|
1327
|
+
function resolveRegionTarget(target, regionStates, byNodeId) {
|
|
1328
|
+
if (target.startsWith('#')) {
|
|
1329
|
+
const nodeId = target.slice(1);
|
|
1330
|
+
return byNodeId.get(nodeId) ?? nodeId;
|
|
1331
|
+
}
|
|
1332
|
+
if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
|
|
1333
|
+
return playbookStateIdOf(regionStates[target]) ?? target;
|
|
1334
|
+
}
|
|
1335
|
+
return target;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
|
|
1339
|
+
* the parallel shape gears2fsm compiles (DR-067) — the targets of each
|
|
1340
|
+
* region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
|
|
1341
|
+
*/
|
|
1181
1342
|
export function resumableStateIdsFromMachine(machine) {
|
|
1182
1343
|
const config = machine.config;
|
|
1183
1344
|
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
1184
1345
|
return new Set();
|
|
1185
1346
|
}
|
|
1347
|
+
const resumable = new Set();
|
|
1186
1348
|
const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1349
|
+
if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
|
|
1350
|
+
const bossReply = awaitState.on.BOSS_REPLY;
|
|
1351
|
+
if (bossReply !== undefined) {
|
|
1352
|
+
for (const target of transitionTargets(bossReply))
|
|
1353
|
+
resumable.add(target);
|
|
1354
|
+
}
|
|
1189
1355
|
}
|
|
1190
|
-
const
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1356
|
+
const byNodeId = playbookStateIdsByNodeId(config);
|
|
1357
|
+
for (const stateDef of Object.values(config.states)) {
|
|
1358
|
+
if (!isPlainObject(stateDef) ||
|
|
1359
|
+
stateDef.type !== 'parallel' ||
|
|
1360
|
+
!isPlainObject(stateDef.states)) {
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
for (const region of Object.values(stateDef.states)) {
|
|
1364
|
+
if (!isPlainObject(region) || !isPlainObject(region.states))
|
|
1365
|
+
continue;
|
|
1366
|
+
const regionStates = region.states;
|
|
1367
|
+
for (const leaf of Object.values(regionStates)) {
|
|
1368
|
+
if (!isPlainObject(leaf) || !isPlainObject(leaf.on))
|
|
1369
|
+
continue;
|
|
1370
|
+
const bossReply = leaf.on.BOSS_REPLY;
|
|
1371
|
+
if (bossReply === undefined)
|
|
1372
|
+
continue;
|
|
1373
|
+
for (const target of transitionArmTargets(bossReply)) {
|
|
1374
|
+
resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
return resumable;
|
|
1380
|
+
}
|
|
1381
|
+
function malformedParallelState(label, key, reason) {
|
|
1382
|
+
return new Error(`${label} parallel state ${key} is not the compiled parallel shape: ${reason}`);
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
|
|
1386
|
+
* `type: 'parallel'` states of the one shape gears2fsm compiles — each
|
|
1387
|
+
* region a compound state with exactly one player-invoking `playbook.busy`
|
|
1388
|
+
* working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
|
|
1389
|
+
* arms resume that working leaf, and exactly one final leaf, every leaf
|
|
1390
|
+
* carrying a stable `meta.playbook.stateId` distinct from every other
|
|
1391
|
+
* state's, and the parent's `onDone` the join. Any other compound state
|
|
1392
|
+
* keeps the flat-domain rejection. Returns `undefined` for a flat machine.
|
|
1393
|
+
*/
|
|
1394
|
+
function parallelProfileFromMachine(machine, label) {
|
|
1395
|
+
const config = machine.config;
|
|
1396
|
+
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
1397
|
+
return undefined;
|
|
1398
|
+
}
|
|
1399
|
+
const rootStates = config.states;
|
|
1400
|
+
const parallelKeys = Object.entries(rootStates)
|
|
1401
|
+
.filter(([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel')
|
|
1402
|
+
.map(([key]) => key);
|
|
1403
|
+
for (const stateDef of Object.values(rootStates)) {
|
|
1404
|
+
if (isPlainObject(stateDef) &&
|
|
1405
|
+
stateDef.type !== 'parallel' &&
|
|
1406
|
+
isPlainObject(stateDef.states) &&
|
|
1407
|
+
Object.keys(stateDef.states).length > 0) {
|
|
1408
|
+
throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
if (parallelKeys.length === 0)
|
|
1412
|
+
return undefined;
|
|
1413
|
+
const byNodeId = playbookStateIdsByNodeId(config);
|
|
1414
|
+
const seenStateIds = new Set();
|
|
1415
|
+
for (const stateDef of Object.values(rootStates)) {
|
|
1416
|
+
const stateId = playbookStateIdOf(stateDef);
|
|
1417
|
+
if (stateId !== undefined)
|
|
1418
|
+
seenStateIds.add(stateId);
|
|
1419
|
+
}
|
|
1420
|
+
const states = [];
|
|
1421
|
+
const parallelStateByWorkingStateId = new Map();
|
|
1422
|
+
const waitResumeTargets = new Map();
|
|
1423
|
+
const transitionNodesByStateId = new Map();
|
|
1424
|
+
for (const key of parallelKeys) {
|
|
1425
|
+
const parent = rootStates[key];
|
|
1426
|
+
if (parent.onDone === undefined) {
|
|
1427
|
+
throw malformedParallelState(label, key, 'it declares no onDone join');
|
|
1428
|
+
}
|
|
1429
|
+
if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
|
|
1430
|
+
throw malformedParallelState(label, key, 'it must declare at least two regions');
|
|
1431
|
+
}
|
|
1432
|
+
const regions = [];
|
|
1433
|
+
const roleIds = new Set();
|
|
1434
|
+
for (const [regionKey, region] of Object.entries(parent.states)) {
|
|
1435
|
+
const regionReason = (reason) => malformedParallelState(label, key, `region ${regionKey} ${reason}`);
|
|
1436
|
+
if (!isPlainObject(region) ||
|
|
1437
|
+
region.type === 'parallel' ||
|
|
1438
|
+
region.type === 'final' ||
|
|
1439
|
+
region.type === 'history' ||
|
|
1440
|
+
!isPlainObject(region.states)) {
|
|
1441
|
+
throw regionReason('must be a compound state of leaves');
|
|
1442
|
+
}
|
|
1443
|
+
const regionStates = region.states;
|
|
1444
|
+
let working;
|
|
1445
|
+
let wait;
|
|
1446
|
+
let final;
|
|
1447
|
+
for (const [leafKey, leaf] of Object.entries(regionStates)) {
|
|
1448
|
+
if (!isPlainObject(leaf)) {
|
|
1449
|
+
throw regionReason(`leaf ${leafKey} must be a state object`);
|
|
1450
|
+
}
|
|
1451
|
+
if ((isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
|
|
1452
|
+
leaf.type === 'parallel' ||
|
|
1453
|
+
leaf.type === 'history') {
|
|
1454
|
+
throw regionReason(`leaf ${leafKey} must be an atomic state`);
|
|
1455
|
+
}
|
|
1456
|
+
const leafStateId = playbookStateIdOf(leaf);
|
|
1457
|
+
if (leafStateId === undefined || leafStateId.trim().length === 0) {
|
|
1458
|
+
throw regionReason(`leaf ${leafKey} must carry a string meta.playbook.stateId`);
|
|
1459
|
+
}
|
|
1460
|
+
if (seenStateIds.has(leafStateId)) {
|
|
1461
|
+
throw regionReason(`leaf ${leafKey} reuses playbook state id ${leafStateId}`);
|
|
1462
|
+
}
|
|
1463
|
+
seenStateIds.add(leafStateId);
|
|
1464
|
+
const invoke = leaf.invoke;
|
|
1465
|
+
const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
|
|
1466
|
+
const tags = stateTagsOf(leaf);
|
|
1467
|
+
if (leaf.type === 'final') {
|
|
1468
|
+
if (final !== undefined)
|
|
1469
|
+
throw regionReason('declares two final leaves');
|
|
1470
|
+
final = [leafKey, leaf];
|
|
1471
|
+
}
|
|
1472
|
+
else if (invokes.length === 1 &&
|
|
1473
|
+
isPlainObject(invokes[0]) &&
|
|
1474
|
+
invokes[0].src === 'player' &&
|
|
1475
|
+
tags.includes('playbook.busy')) {
|
|
1476
|
+
if (working !== undefined) {
|
|
1477
|
+
throw regionReason('declares two working leaves');
|
|
1478
|
+
}
|
|
1479
|
+
working = [leafKey, leaf];
|
|
1480
|
+
}
|
|
1481
|
+
else if (invokes.length === 0 &&
|
|
1482
|
+
tags.includes('playbook.parked') &&
|
|
1483
|
+
isPlainObject(leaf.on) &&
|
|
1484
|
+
leaf.on.BOSS_REPLY !== undefined) {
|
|
1485
|
+
if (wait !== undefined)
|
|
1486
|
+
throw regionReason('declares two wait leaves');
|
|
1487
|
+
wait = [leafKey, leaf];
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
throw regionReason(`leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (working === undefined || wait === undefined || final === undefined) {
|
|
1494
|
+
throw regionReason('must hold exactly one working leaf, one wait leaf, and one final leaf');
|
|
1495
|
+
}
|
|
1496
|
+
if (region.initial !== working[0]) {
|
|
1497
|
+
throw regionReason('must start in its working leaf');
|
|
1498
|
+
}
|
|
1499
|
+
const workingStateId = playbookStateIdOf(working[1]);
|
|
1500
|
+
const waitStateId = playbookStateIdOf(wait[1]);
|
|
1501
|
+
const finalStateId = playbookStateIdOf(final[1]);
|
|
1502
|
+
const replyTargets = transitionArmTargets(wait[1].on.BOSS_REPLY).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
|
|
1503
|
+
const regionStateIds = new Set([waitStateId, finalStateId]);
|
|
1504
|
+
if (!replyTargets.includes(workingStateId) ||
|
|
1505
|
+
replyTargets.some((target) => regionStateIds.has(target))) {
|
|
1506
|
+
throw regionReason('wait leaf BOSS_REPLY arms must resume its working leaf');
|
|
1507
|
+
}
|
|
1508
|
+
const workingMeta = working[1].meta
|
|
1509
|
+
.playbook;
|
|
1510
|
+
const roleId = workingMeta.role;
|
|
1511
|
+
if (typeof roleId !== 'string' || roleId.trim().length === 0) {
|
|
1512
|
+
throw regionReason('working leaf must declare meta.playbook.role');
|
|
1513
|
+
}
|
|
1514
|
+
if (roleIds.has(roleId)) {
|
|
1515
|
+
throw malformedParallelState(label, key, `role ${roleId} works in more than one region`);
|
|
1516
|
+
}
|
|
1517
|
+
roleIds.add(roleId);
|
|
1518
|
+
regions.push({
|
|
1519
|
+
key: regionKey,
|
|
1520
|
+
roleId,
|
|
1521
|
+
workingStateId,
|
|
1522
|
+
waitStateId,
|
|
1523
|
+
finalStateId,
|
|
1524
|
+
});
|
|
1525
|
+
waitResumeTargets.set(waitStateId, new Set([workingStateId]));
|
|
1526
|
+
for (const [, leaf] of [working, wait, final]) {
|
|
1527
|
+
transitionNodesByStateId.set(playbookStateIdOf(leaf), [region, leaf]);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
const parallelState = {
|
|
1531
|
+
stateId: key,
|
|
1532
|
+
regions: Object.freeze(regions),
|
|
1533
|
+
};
|
|
1534
|
+
states.push(parallelState);
|
|
1535
|
+
for (const region of regions) {
|
|
1536
|
+
parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
|
|
1537
|
+
}
|
|
1538
|
+
transitionNodesByStateId.set(key, [parent]);
|
|
1539
|
+
}
|
|
1540
|
+
for (const [key, stateDef] of Object.entries(rootStates)) {
|
|
1541
|
+
if (!isPlainObject(stateDef) || parallelKeys.includes(key))
|
|
1542
|
+
continue;
|
|
1543
|
+
transitionNodesByStateId.set(key, [stateDef]);
|
|
1544
|
+
}
|
|
1545
|
+
const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
|
|
1546
|
+
if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
|
|
1547
|
+
const bossReply = rootWait.on.BOSS_REPLY;
|
|
1548
|
+
if (bossReply !== undefined) {
|
|
1549
|
+
waitResumeTargets.set(BOSS_REPLY_WAIT_STATE_ID, new Set(transitionTargets(bossReply)));
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return Object.freeze({
|
|
1553
|
+
states: Object.freeze(states),
|
|
1554
|
+
parallelStateByWorkingStateId,
|
|
1555
|
+
waitResumeTargets,
|
|
1556
|
+
transitionNodesByStateId,
|
|
1557
|
+
});
|
|
1194
1558
|
}
|
|
1195
1559
|
// ---------------------------------------------------------------------------
|
|
1196
1560
|
// DR-029 control surface: the FSM's explicit-state-jump event and the
|
|
@@ -1797,7 +2161,48 @@ function eventContractPrompt(contract) {
|
|
|
1797
2161
|
]);
|
|
1798
2162
|
return `{ ${members.join(', ')} }${notes.length === 0 ? '' : ` (${notes.join('; ')})`}`;
|
|
1799
2163
|
}
|
|
1800
|
-
|
|
2164
|
+
/**
|
|
2165
|
+
* DR-067 §1: the questions pending in a parallel-declaring machine — the
|
|
2166
|
+
* keyed records whose resume state an active wait state (a region wait leaf
|
|
2167
|
+
* or the root reply wait) resumes.
|
|
2168
|
+
*/
|
|
2169
|
+
function parallelPendingBossQuestions(profile, activeStateIds, context) {
|
|
2170
|
+
return keyedPendingBossQuestions(context, (resumeStateId) => activeStateIds.some((stateId) => profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true));
|
|
2171
|
+
}
|
|
2172
|
+
/** Event types the root and every active state node of a parallel machine configure. */
|
|
2173
|
+
function parallelConfiguredEventTypes(machine, profile, activeStateIds) {
|
|
2174
|
+
const configured = new Set();
|
|
2175
|
+
const config = machine.config;
|
|
2176
|
+
if (!isPlainObject(config))
|
|
2177
|
+
return configured;
|
|
2178
|
+
if (isPlainObject(config.on)) {
|
|
2179
|
+
for (const type of Object.keys(config.on))
|
|
2180
|
+
configured.add(type);
|
|
2181
|
+
}
|
|
2182
|
+
for (const stateId of activeStateIds) {
|
|
2183
|
+
for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
|
|
2184
|
+
if (!isPlainObject(node.on))
|
|
2185
|
+
continue;
|
|
2186
|
+
for (const type of Object.keys(node.on))
|
|
2187
|
+
configured.add(type);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
return configured;
|
|
2191
|
+
}
|
|
2192
|
+
function activeStateIdsOf(snapshotOrState) {
|
|
2193
|
+
const listed = snapshotOrState
|
|
2194
|
+
?.activeStateIds;
|
|
2195
|
+
if (Array.isArray(listed)) {
|
|
2196
|
+
return listed.filter((stateId) => typeof stateId === 'string');
|
|
2197
|
+
}
|
|
2198
|
+
try {
|
|
2199
|
+
return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
|
|
2200
|
+
}
|
|
2201
|
+
catch {
|
|
2202
|
+
return [];
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function makeDefaultClassifyBossText(machine, entryEvent, bossEvents, parallelProfile) {
|
|
1801
2206
|
const contracts = defaultBossEventSpecs(machine, entryEvent, bossEvents);
|
|
1802
2207
|
return async (text, ports, signal, snapshotOrState, boundary) => {
|
|
1803
2208
|
const trimmed = text.trim();
|
|
@@ -1811,12 +2216,41 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
1811
2216
|
// is answered history, so the prompt must not present it as pending —
|
|
1812
2217
|
// a judge told a question awaits at the failure state is steered toward
|
|
1813
2218
|
// a reply it cannot select or toward no action at all.
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2219
|
+
let pendingQuestions;
|
|
2220
|
+
let configuredTypes;
|
|
2221
|
+
if (parallelProfile === undefined) {
|
|
2222
|
+
const pending = stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
2223
|
+
? pendingBossQuestionFromContext(state.context)
|
|
2224
|
+
: undefined;
|
|
2225
|
+
pendingQuestions = pending === undefined ? [] : [pending];
|
|
2226
|
+
configuredTypes = configuredEventTypesForState(machine, stateId);
|
|
2227
|
+
}
|
|
2228
|
+
else {
|
|
2229
|
+
// DR-067 §1: every question pending in any active wait is offered, and
|
|
2230
|
+
// the events of every active state node apply.
|
|
2231
|
+
const activeStateIds = activeStateIdsOf(snapshotOrState);
|
|
2232
|
+
pendingQuestions = parallelPendingBossQuestions(parallelProfile, activeStateIds, state.context);
|
|
2233
|
+
configuredTypes = parallelConfiguredEventTypes(machine, parallelProfile, activeStateIds);
|
|
2234
|
+
}
|
|
2235
|
+
// Several pending questions: a reply must name the one it answers, and
|
|
2236
|
+
// the classifier never guesses among them (gears2fsm §Parallel groups).
|
|
2237
|
+
const severalPending = pendingQuestions.length > 1;
|
|
2238
|
+
const applicable = [...contracts.values()]
|
|
2239
|
+
.filter((contract) => configuredTypes.has(contract.type) &&
|
|
2240
|
+
(contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0))
|
|
2241
|
+
.map((contract) => contract.type === 'BOSS_REPLY' && severalPending
|
|
2242
|
+
? {
|
|
2243
|
+
type: 'BOSS_REPLY',
|
|
2244
|
+
fields: {
|
|
2245
|
+
questionId: {
|
|
2246
|
+
source: 'judge',
|
|
2247
|
+
required: true,
|
|
2248
|
+
values: pendingQuestions.map(({ questionId }) => questionId),
|
|
2249
|
+
},
|
|
2250
|
+
answer: { source: 'text', required: true },
|
|
2251
|
+
},
|
|
2252
|
+
}
|
|
2253
|
+
: contract);
|
|
1820
2254
|
const lines = [
|
|
1821
2255
|
'Classify the following Boss message into exactly one event.',
|
|
1822
2256
|
'Respond with one exact flat JSON object. Do not add fields that are not shown.',
|
|
@@ -1824,9 +2258,12 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
1824
2258
|
'',
|
|
1825
2259
|
`Current state: ${currentState}`,
|
|
1826
2260
|
];
|
|
1827
|
-
|
|
2261
|
+
for (const pending of pendingQuestions) {
|
|
1828
2262
|
lines.push(`Pending question id: ${pending.questionId}`, `Pending asker: ${askerLabel(pending.asker)}`, `Pending Boss question: ${pending.question}`);
|
|
1829
2263
|
}
|
|
2264
|
+
if (severalPending) {
|
|
2265
|
+
lines.push('Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.');
|
|
2266
|
+
}
|
|
1830
2267
|
lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
|
|
1831
2268
|
for (const contract of applicable) {
|
|
1832
2269
|
lines.push(`- ${eventContractPrompt(contract)}`);
|
|
@@ -1897,16 +2334,19 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
1897
2334
|
event[field] = value;
|
|
1898
2335
|
}
|
|
1899
2336
|
if (eventType === 'BOSS_REPLY') {
|
|
1900
|
-
if (
|
|
2337
|
+
if (pendingQuestions.length === 0) {
|
|
1901
2338
|
await ports.emitStatus('Classifier returned BOSS_REPLY without a pending question');
|
|
1902
2339
|
return undefined;
|
|
1903
2340
|
}
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2341
|
+
if (!severalPending) {
|
|
2342
|
+
const pending = pendingQuestions[0];
|
|
2343
|
+
const questionId = event.questionId;
|
|
2344
|
+
if (questionId !== undefined && questionId !== pending.questionId) {
|
|
2345
|
+
await ports.emitStatus(`Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`);
|
|
2346
|
+
return undefined;
|
|
2347
|
+
}
|
|
2348
|
+
event.questionId = pending.questionId;
|
|
1908
2349
|
}
|
|
1909
|
-
event.questionId = pending.questionId;
|
|
1910
2350
|
}
|
|
1911
2351
|
const can = snapshotOrState?.can;
|
|
1912
2352
|
if (typeof can === 'function' &&
|
|
@@ -1917,33 +2357,6 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
|
|
|
1917
2357
|
return event;
|
|
1918
2358
|
};
|
|
1919
2359
|
}
|
|
1920
|
-
function machineDeclaresParallelState(machine) {
|
|
1921
|
-
const visit = (stateDef) => {
|
|
1922
|
-
if (!isPlainObject(stateDef))
|
|
1923
|
-
return false;
|
|
1924
|
-
if (stateDef.type === 'parallel')
|
|
1925
|
-
return true;
|
|
1926
|
-
if (!isPlainObject(stateDef.states))
|
|
1927
|
-
return false;
|
|
1928
|
-
return Object.values(stateDef.states).some(visit);
|
|
1929
|
-
};
|
|
1930
|
-
return visit(machine.config);
|
|
1931
|
-
}
|
|
1932
|
-
// PBRT-52: the factory's domain is FLAT single-region machines — every
|
|
1933
|
-
// state a direct child of the root, so each snapshot exposes exactly one
|
|
1934
|
-
// playbook state id and every state-keyed lookup (deterministic entries,
|
|
1935
|
-
// retry, reply-wait pendingness, configured events, descriptions) indexes
|
|
1936
|
-
// one unambiguous identity. A compound child would be accepted and then
|
|
1937
|
-
// silently misbehave on all of those gates, so it is rejected up front
|
|
1938
|
-
// exactly like a parallel region.
|
|
1939
|
-
function machineDeclaresNestedState(machine) {
|
|
1940
|
-
const config = machine.config;
|
|
1941
|
-
if (!isPlainObject(config) || !isPlainObject(config.states))
|
|
1942
|
-
return false;
|
|
1943
|
-
return Object.values(config.states).some((stateDef) => isPlainObject(stateDef) &&
|
|
1944
|
-
isPlainObject(stateDef.states) &&
|
|
1945
|
-
Object.keys(stateDef.states).length > 0);
|
|
1946
|
-
}
|
|
1947
2360
|
// PBRT-52: the factory's lookups index states by their root key, and the
|
|
1948
2361
|
// published playbook identity is `meta.playbook.stateId` — the two must
|
|
1949
2362
|
// coincide or a machine can advertise a pending question or retry under an
|
|
@@ -2022,12 +2435,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2022
2435
|
if (Object.prototype.hasOwnProperty.call(specDescriptors, 'resolvePlayerId')) {
|
|
2023
2436
|
throw new TypeError(`${label} artifacts must not derive concrete player bindings`);
|
|
2024
2437
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
if (machineDeclaresNestedState(machine)) {
|
|
2029
|
-
throw new Error(`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`);
|
|
2030
|
-
}
|
|
2438
|
+
// DR-067 §1: a flat machine, or one whose only compound states are root
|
|
2439
|
+
// parallel states of the compiled shape; everything else is rejected.
|
|
2440
|
+
const parallelProfile = parallelProfileFromMachine(machine, label);
|
|
2031
2441
|
assertFlatStateIdentity(machine, label);
|
|
2032
2442
|
assertUnfinishedFinalStateIds(spec.unfinishedFinalStateIds, machine, label);
|
|
2033
2443
|
const retainedGenerationMetadata = spec.unfinishedFinalStateIds === undefined
|
|
@@ -2055,6 +2465,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2055
2465
|
const declaredRoleIds = Object.freeze([
|
|
2056
2466
|
...new Set([...roleStates.values()].map(({ role }) => role)),
|
|
2057
2467
|
]);
|
|
2468
|
+
// DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
|
|
2469
|
+
// each player state, each wait state, and the failure state. A status is
|
|
2470
|
+
// scheduled for each one newly entered, and a status trace names a
|
|
2471
|
+
// `stateId` only while exactly one is active.
|
|
2472
|
+
const bossRelevantStateIds = parallelProfile === undefined
|
|
2473
|
+
? undefined
|
|
2474
|
+
: new Set([
|
|
2475
|
+
...roleStates.keys(),
|
|
2476
|
+
...parallelProfile.waitResumeTargets.keys(),
|
|
2477
|
+
'failed',
|
|
2478
|
+
]);
|
|
2058
2479
|
// PBRT-52: the artifact's own ControlView context projection. Nothing is
|
|
2059
2480
|
// exported by default, so an FSM context member — including one added
|
|
2060
2481
|
// after this artifact was linked — is private until named here. The two
|
|
@@ -2093,7 +2514,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2093
2514
|
// supplied `bossEvents`, and DR-019 §2 requires a conflicting duplicate to
|
|
2094
2515
|
// fail factory construction whether or not this spec overrides the
|
|
2095
2516
|
// classifier that would have consumed the contracts.
|
|
2096
|
-
const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? []);
|
|
2517
|
+
const derivedClassifyBossText = makeDefaultClassifyBossText(machine, spec.entryEvent, spec.bossEvents ?? [], parallelProfile);
|
|
2097
2518
|
const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
|
|
2098
2519
|
const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
|
|
2099
2520
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
@@ -2108,6 +2529,93 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2108
2529
|
const cwd = options?.cwd;
|
|
2109
2530
|
return typeof cwd === 'string' ? cwd : undefined;
|
|
2110
2531
|
});
|
|
2532
|
+
// DR-067 §1: the questions pending in a state — the singular reply-wait
|
|
2533
|
+
// question of a flat machine, or a parallel machine's keyed questions
|
|
2534
|
+
// whose own wait state is active.
|
|
2535
|
+
const pendingBossQuestionsFor = (state, context) => {
|
|
2536
|
+
if (parallelProfile === undefined) {
|
|
2537
|
+
const pending = pendingBossQuestionForState(state, context);
|
|
2538
|
+
return pending === undefined ? [] : [pending];
|
|
2539
|
+
}
|
|
2540
|
+
return parallelPendingBossQuestions(parallelProfile, state.activeStateIds, context);
|
|
2541
|
+
};
|
|
2542
|
+
const singlePendingBossQuestion = (state, context) => {
|
|
2543
|
+
const pending = pendingBossQuestionsFor(state, context);
|
|
2544
|
+
return pending.length === 1 ? pending[0] : undefined;
|
|
2545
|
+
};
|
|
2546
|
+
// DR-067 §1: a status trace names a `stateId` only while exactly one
|
|
2547
|
+
// Boss-relevant state is active; a flat machine's is its one state.
|
|
2548
|
+
const statusTraceStateId = (state) => {
|
|
2549
|
+
if (bossRelevantStateIds === undefined)
|
|
2550
|
+
return state.stateId;
|
|
2551
|
+
const relevant = state.activeStateIds.filter((stateId) => bossRelevantStateIds.has(stateId));
|
|
2552
|
+
return relevant.length === 1 ? relevant[0] : undefined;
|
|
2553
|
+
};
|
|
2554
|
+
// DR-067 §1: a parallel machine schedules statuses for each newly entered
|
|
2555
|
+
// Boss-relevant state — a player state's role line, a wait state's
|
|
2556
|
+
// question lines, and the failure line — diffed against the previous
|
|
2557
|
+
// snapshot's active state ids, so a sibling still running is not
|
|
2558
|
+
// announced again.
|
|
2559
|
+
const parallelStatusesFor = (previousState, state, context) => {
|
|
2560
|
+
const prior = new Set(previousState?.activeStateIds ?? []);
|
|
2561
|
+
const statuses = [];
|
|
2562
|
+
for (const stateId of state.activeStateIds) {
|
|
2563
|
+
if (prior.has(stateId) || !bossRelevantStateIds.has(stateId))
|
|
2564
|
+
continue;
|
|
2565
|
+
const resumeTargets = parallelProfile.waitResumeTargets.get(stateId);
|
|
2566
|
+
if (resumeTargets !== undefined) {
|
|
2567
|
+
const questions = keyedPendingBossQuestions(context, (resumeStateId) => resumeTargets.has(resumeStateId));
|
|
2568
|
+
if (questions.length === 0) {
|
|
2569
|
+
statuses.push({ message: 'Awaiting Boss reply.' });
|
|
2570
|
+
continue;
|
|
2571
|
+
}
|
|
2572
|
+
for (const pending of questions) {
|
|
2573
|
+
statuses.push({
|
|
2574
|
+
message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
|
|
2575
|
+
}, {
|
|
2576
|
+
message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
2577
|
+
`${askerLabel(pending.asker)} · ${pending.sourceItem}`,
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
continue;
|
|
2581
|
+
}
|
|
2582
|
+
if (stateId === 'failed') {
|
|
2583
|
+
const lastError = normalizeErrorCompact(context.lastError);
|
|
2584
|
+
statuses.push({
|
|
2585
|
+
message: '◆ workflow failed; awaiting Boss recovery.',
|
|
2586
|
+
...(lastError === undefined
|
|
2587
|
+
? {}
|
|
2588
|
+
: {
|
|
2589
|
+
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
2590
|
+
}),
|
|
2591
|
+
});
|
|
2592
|
+
continue;
|
|
2593
|
+
}
|
|
2594
|
+
const roleState = roleStates.get(stateId);
|
|
2595
|
+
if (roleState !== undefined) {
|
|
2596
|
+
statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
return statuses;
|
|
2600
|
+
};
|
|
2601
|
+
// DR-067 §1: each parallel state's region roles, in region order, are one
|
|
2602
|
+
// concurrent role set the FSM exports; a host that forwards those sets in
|
|
2603
|
+
// its schema-3 authority is held to them before any agent call.
|
|
2604
|
+
const assertDeclaredCohorts = (hostCapabilities) => {
|
|
2605
|
+
const authority = Object.getOwnPropertyDescriptor(hostCapabilities, 'authority')?.value;
|
|
2606
|
+
if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
const declared = authority.concurrentRoleSets;
|
|
2610
|
+
for (const parallelState of parallelProfile.states) {
|
|
2611
|
+
const roles = parallelState.regions.map(({ roleId }) => roleId);
|
|
2612
|
+
if (!declared.some((candidate) => Array.isArray(candidate) &&
|
|
2613
|
+
candidate.length === roles.length &&
|
|
2614
|
+
candidate.every((roleId, index) => roleId === roles[index]))) {
|
|
2615
|
+
throw new TypeError(`${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2111
2619
|
const createPlaybookRuntime = function createPlaybookRuntime(factoryOptions) {
|
|
2112
2620
|
const construction = configuredOptionsFromFactoryInput(factoryOptions, label);
|
|
2113
2621
|
const configuredOptions = construction.configuredOptions;
|
|
@@ -2123,8 +2631,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2123
2631
|
Object.prototype.hasOwnProperty.call(declarations, acceptedOutcome));
|
|
2124
2632
|
});
|
|
2125
2633
|
const repositoryCapability = hasGovernedPlayerStates
|
|
2126
|
-
? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label)
|
|
2634
|
+
? repositoryCapabilityFromHostCapabilities(construction.hostCapabilities, label, parallelProfile !== undefined)
|
|
2127
2635
|
: undefined;
|
|
2636
|
+
if (parallelProfile !== undefined) {
|
|
2637
|
+
assertDeclaredCohorts(construction.hostCapabilities);
|
|
2638
|
+
}
|
|
2128
2639
|
const currentEffectLedger = () => assertPlaybookEffectLedger(effectLedgerCapability.snapshot(), `${label} current host effect ledger`);
|
|
2129
2640
|
let effectLedgerMirror = currentEffectLedger();
|
|
2130
2641
|
let retainedEffectSourceSessionId;
|
|
@@ -2159,9 +2670,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2159
2670
|
// mutable `activeSignal` alone cannot classify a late invocation reason.
|
|
2160
2671
|
let activeAborts;
|
|
2161
2672
|
// The bridge binds the provenance of a child result immediately before
|
|
2162
|
-
// its promise actor settles. The next root snapshot/error consumes
|
|
2163
|
-
//
|
|
2164
|
-
|
|
2673
|
+
// its promise actor settles. The next root snapshot/error consumes it so
|
|
2674
|
+
// background settlement emissions retain their owner. A flat machine has
|
|
2675
|
+
// one settling actor at a time, so a later binding replaces an unconsumed
|
|
2676
|
+
// one; a parallel machine's settlements queue in order (DR-067 §1).
|
|
2677
|
+
const actorSettlementAborts = [];
|
|
2165
2678
|
let actorSettlementErrorAborts;
|
|
2166
2679
|
// Exact cancellation observed by an emission owned by the active
|
|
2167
2680
|
// boundary. Ordinary runs settle from their signal/state; apply also
|
|
@@ -2186,15 +2699,35 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2186
2699
|
let deferInspectionEmissions = false;
|
|
2187
2700
|
let deferredInspectionEmissions = [];
|
|
2188
2701
|
let controlPlaneError;
|
|
2189
|
-
// DR-063 §2: the cause decided
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2702
|
+
// DR-063 §2: the cause decided for a non-`ok` player result before the
|
|
2703
|
+
// compiled artifact builds the Error that carries it and hands it to
|
|
2704
|
+
// `markPlayerResultFailure`. It is cleared at each Boss turn start and at
|
|
2705
|
+
// each successful result, so it is never stale.
|
|
2706
|
+
let pendingResultFailureCause;
|
|
2707
|
+
// DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
2708
|
+
// marker — a string, a frozen error — and for the record a machine keeps
|
|
2709
|
+
// in place of the thrown value, kept by identity for as long as the
|
|
2710
|
+
// failure stands, so every surface that reads the FSM's own `lastError`
|
|
2711
|
+
// publishes the cause decided for exactly that failure, whatever turn
|
|
2712
|
+
// reads it.
|
|
2713
|
+
const failureCauses = createFailureCauseRetention();
|
|
2714
|
+
// DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
|
|
2715
|
+
// result, keyed by the result itself, so concurrent region calls never
|
|
2716
|
+
// lend one another the single slot above.
|
|
2717
|
+
const resultFailureCauses = new WeakMap();
|
|
2718
|
+
// DR-067 §1: the parallel profile's cohort bookkeeping. Every public
|
|
2719
|
+
// boundary advances the sequence; a parallel state's working leaves run
|
|
2720
|
+
// as one cohort at most once per boundary, and each cohort's semantic
|
|
2721
|
+
// completions — which the host settles concurrently — reconcile one at a
|
|
2722
|
+
// time so their durable correction-budget writes cannot race.
|
|
2723
|
+
let publicBoundarySequence = 0;
|
|
2724
|
+
const consumedCohortBoundaries = new Map();
|
|
2725
|
+
const pendingCohorts = new Map();
|
|
2726
|
+
const cohortCompletionQueue = new PQueue({ concurrency: 1 });
|
|
2727
|
+
// DR-067 §1: every player, judge, and nested call a parallel machine
|
|
2728
|
+
// starts, so a boundary's drain waits for a cancelled sibling whose port
|
|
2729
|
+
// ignores its signal before it settles.
|
|
2730
|
+
const activeBoundaryCalls = new Set();
|
|
2198
2731
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
2199
2732
|
// status emitter. undefined before the first inspect firing.
|
|
2200
2733
|
let priorState;
|
|
@@ -2970,7 +3503,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2970
3503
|
...(descriptor !== undefined
|
|
2971
3504
|
? {
|
|
2972
3505
|
state: descriptor,
|
|
2973
|
-
...stateIdentity(descriptor
|
|
3506
|
+
...stateIdentity(statusTraceStateId(descriptor)),
|
|
2974
3507
|
}
|
|
2975
3508
|
: {}),
|
|
2976
3509
|
}, activeTurnId !== undefined ? { turnId: activeTurnId } : {});
|
|
@@ -3003,9 +3536,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3003
3536
|
// its finish carries the receipt disposition and none of the
|
|
3004
3537
|
// start-only fields, so it passes its own canonical pre-acceptance
|
|
3005
3538
|
// base (slc/link.md §Playbook trace).
|
|
3006
|
-
finishIdentity = identity
|
|
3539
|
+
finishIdentity = identity,
|
|
3540
|
+
// DR-067 §1: the per-call abort classification a parallel machine's
|
|
3541
|
+
// player pairs enqueue under; absent, the active boundary's.
|
|
3542
|
+
aborts) {
|
|
3007
3543
|
try {
|
|
3008
|
-
await emitTrace(startedType, identity, position);
|
|
3544
|
+
await emitTrace(startedType, identity, position, aborts);
|
|
3009
3545
|
}
|
|
3010
3546
|
catch (error) {
|
|
3011
3547
|
if (!isAbortFailure(error, signal))
|
|
@@ -3015,7 +3551,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3015
3551
|
...finishIdentity,
|
|
3016
3552
|
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
3017
3553
|
error: normalizeError(error),
|
|
3018
|
-
}, position);
|
|
3554
|
+
}, position, aborts);
|
|
3019
3555
|
}
|
|
3020
3556
|
catch {
|
|
3021
3557
|
// Preserve the start failure after one best-effort finish attempt.
|
|
@@ -3102,33 +3638,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3102
3638
|
},
|
|
3103
3639
|
});
|
|
3104
3640
|
}
|
|
3105
|
-
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
3106
|
-
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
3107
|
-
// owns map to adjudication, abort, and runtime-defect codes.
|
|
3108
|
-
function governedSettlementCause(reason, error, aborted, supplied) {
|
|
3109
|
-
if (supplied !== undefined)
|
|
3110
|
-
return supplied;
|
|
3111
|
-
if (aborted || reason.includes('aborted'))
|
|
3112
|
-
return ABORTED_FAILURE_CAUSE;
|
|
3113
|
-
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
3114
|
-
const transport = failureErrorEvidence(error);
|
|
3115
|
-
return assertPlaybookFailureCause({
|
|
3116
|
-
code: 'judge-failed',
|
|
3117
|
-
evidence: {
|
|
3118
|
-
reason,
|
|
3119
|
-
...(transport === undefined ? {} : { error: transport }),
|
|
3120
|
-
},
|
|
3121
|
-
});
|
|
3122
|
-
}
|
|
3123
|
-
return runtimeDefectCause(reason);
|
|
3124
|
-
}
|
|
3125
3641
|
function unresolvedGovernedSettlement(reason, error, signal = activeSignal, cause) {
|
|
3126
3642
|
const aborted = error !== undefined && signal?.aborted === true
|
|
3127
3643
|
? Object.is(error, signal.reason)
|
|
3128
3644
|
: false;
|
|
3129
3645
|
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
3130
|
-
rememberFailureCause(decided);
|
|
3131
3646
|
if (aborted) {
|
|
3647
|
+
failureCauses.retain(error, decided);
|
|
3132
3648
|
return {
|
|
3133
3649
|
status: 'unresolved',
|
|
3134
3650
|
error: attachPlaybookFailureCause(error, decided),
|
|
@@ -3137,6 +3653,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3137
3653
|
const failure = error instanceof Error
|
|
3138
3654
|
? error
|
|
3139
3655
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
3656
|
+
failureCauses.retain(failure, decided);
|
|
3140
3657
|
return {
|
|
3141
3658
|
status: 'unresolved',
|
|
3142
3659
|
error: attachPlaybookFailureCause(markFsmResultFailure(failure), decided),
|
|
@@ -3173,11 +3690,32 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3173
3690
|
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
3174
3691
|
correctionBudget: { limit: 1, spent: true },
|
|
3175
3692
|
};
|
|
3693
|
+
// DR-067 §1 / PBRT-69: a cohort's shared receipt becomes visible for
|
|
3694
|
+
// every member in one ledger revision — publishing only this member's
|
|
3695
|
+
// spend would leave the invalid half-complete cohort the ledger admits
|
|
3696
|
+
// nowhere.
|
|
3697
|
+
const cohortMembers = current.cohortId === undefined
|
|
3698
|
+
? [current]
|
|
3699
|
+
: currentLedger.boundaries.filter(({ cohortId }) => cohortId === current.cohortId);
|
|
3700
|
+
const replacements = cohortMembers.map((member) => {
|
|
3701
|
+
if (member.boundaryId === current.boundaryId) {
|
|
3702
|
+
return { expected: current, next };
|
|
3703
|
+
}
|
|
3704
|
+
if (member.physicalReceipt !== undefined &&
|
|
3705
|
+
!isDeepStrictEqual(member.physicalReceipt, receipt)) {
|
|
3706
|
+
throw new TypeError(`${label} correction budget cohort conflicts with its shared repository receipt`);
|
|
3707
|
+
}
|
|
3708
|
+
return {
|
|
3709
|
+
expected: member,
|
|
3710
|
+
next: {
|
|
3711
|
+
...member,
|
|
3712
|
+
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
3713
|
+
physicalReceipt: receipt,
|
|
3714
|
+
},
|
|
3715
|
+
};
|
|
3716
|
+
});
|
|
3176
3717
|
const acknowledged = assertPlaybookEffectLedger(await effectLedgerCapability.writeAhead([
|
|
3177
|
-
{
|
|
3178
|
-
kind: 'replace-boundaries',
|
|
3179
|
-
replacements: [{ expected: current, next }],
|
|
3180
|
-
},
|
|
3718
|
+
{ kind: 'replace-boundaries', replacements },
|
|
3181
3719
|
]), `${label} semantic correction budget acknowledgement`);
|
|
3182
3720
|
effectLedgerMirror = acknowledged;
|
|
3183
3721
|
refreshRetainedEffectReconciliation(acknowledged);
|
|
@@ -3482,7 +4020,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3482
4020
|
governedOutput = undefined;
|
|
3483
4021
|
}
|
|
3484
4022
|
}
|
|
3485
|
-
else if (source
|
|
4023
|
+
else if (source !== 'runDeferred' && value.deferredStatus !== undefined) {
|
|
3486
4024
|
throw new TypeError(`${label} non-deferred settlement returned a deferred binding status`);
|
|
3487
4025
|
}
|
|
3488
4026
|
if (governedSettlement !== undefined) {
|
|
@@ -3617,6 +4155,269 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3617
4155
|
: activeEffectLedgerPrefixSequence;
|
|
3618
4156
|
failedGovernedAttemptId = attemptIds.values().next().value;
|
|
3619
4157
|
}
|
|
4158
|
+
// -----------------------------------------------------------------
|
|
4159
|
+
// DR-067 §1: the parallel profile's repository cohort. The working
|
|
4160
|
+
// leaves of one parallel state entered together run under one
|
|
4161
|
+
// `runCohort` claim, each member an `unchanged` boundary: a member's
|
|
4162
|
+
// result is adjudicated through the one reconciler in serial, a spent
|
|
4163
|
+
// correction rewrites every member's boundary in one acknowledged batch,
|
|
4164
|
+
// members are acknowledged and released in completion order, and a
|
|
4165
|
+
// member's failure cancels its siblings and fails the cohort with the
|
|
4166
|
+
// cause decided first.
|
|
4167
|
+
// -----------------------------------------------------------------
|
|
4168
|
+
function trackBoundaryCall(call) {
|
|
4169
|
+
if (parallelProfile === undefined)
|
|
4170
|
+
return call;
|
|
4171
|
+
activeBoundaryCalls.add(call);
|
|
4172
|
+
const forget = () => {
|
|
4173
|
+
activeBoundaryCalls.delete(call);
|
|
4174
|
+
};
|
|
4175
|
+
void call.then(forget, forget);
|
|
4176
|
+
return call;
|
|
4177
|
+
}
|
|
4178
|
+
async function drainBoundaryCalls() {
|
|
4179
|
+
while (activeBoundaryCalls.size > 0) {
|
|
4180
|
+
await Promise.allSettled([...activeBoundaryCalls]);
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
function cohortParallelStateFor(stateId) {
|
|
4184
|
+
const parallelState = parallelProfile?.parallelStateByWorkingStateId.get(stateId);
|
|
4185
|
+
if (parallelState === undefined)
|
|
4186
|
+
return undefined;
|
|
4187
|
+
if (consumedCohortBoundaries.get(parallelState.stateId) ===
|
|
4188
|
+
publicBoundarySequence) {
|
|
4189
|
+
return undefined;
|
|
4190
|
+
}
|
|
4191
|
+
const pending = pendingCohorts.get(parallelState.stateId);
|
|
4192
|
+
if (pending !== undefined &&
|
|
4193
|
+
pending.boundarySequence === publicBoundarySequence &&
|
|
4194
|
+
!pending.started) {
|
|
4195
|
+
return parallelState;
|
|
4196
|
+
}
|
|
4197
|
+
const active = currentState().activeStateIds;
|
|
4198
|
+
return parallelState.regions.every(({ workingStateId }) => active.includes(workingStateId))
|
|
4199
|
+
? parallelState
|
|
4200
|
+
: undefined;
|
|
4201
|
+
}
|
|
4202
|
+
function startCohort(parallelState, cohort) {
|
|
4203
|
+
cohort.started = true;
|
|
4204
|
+
consumedCohortBoundaries.set(parallelState.stateId, cohort.boundarySequence);
|
|
4205
|
+
if (pendingCohorts.get(parallelState.stateId) === cohort) {
|
|
4206
|
+
pendingCohorts.delete(parallelState.stateId);
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
async function joinCohort(parallelState, member) {
|
|
4210
|
+
let cohort = pendingCohorts.get(parallelState.stateId);
|
|
4211
|
+
if (cohort === undefined ||
|
|
4212
|
+
cohort.boundarySequence !== publicBoundarySequence ||
|
|
4213
|
+
cohort.started) {
|
|
4214
|
+
cohort = {
|
|
4215
|
+
boundarySequence: publicBoundarySequence,
|
|
4216
|
+
members: new Map(),
|
|
4217
|
+
started: false,
|
|
4218
|
+
};
|
|
4219
|
+
pendingCohorts.set(parallelState.stateId, cohort);
|
|
4220
|
+
}
|
|
4221
|
+
const registered = cohort;
|
|
4222
|
+
if (registered.members.has(member.region.key)) {
|
|
4223
|
+
throw new Error(`${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`);
|
|
4224
|
+
}
|
|
4225
|
+
registered.members.set(member.region.key, member);
|
|
4226
|
+
// A member whose invocation stops before every sibling registered —
|
|
4227
|
+
// its region exited, or the boundary aborted — leaves no cohort to
|
|
4228
|
+
// run: each registered member settles with its own cancellation and
|
|
4229
|
+
// the boundary's cohort is spent.
|
|
4230
|
+
const withdraw = () => {
|
|
4231
|
+
if (registered.started)
|
|
4232
|
+
return;
|
|
4233
|
+
startCohort(parallelState, registered);
|
|
4234
|
+
for (const candidate of registered.members.values()) {
|
|
4235
|
+
candidate.settled.reject(candidate.signal.aborted
|
|
4236
|
+
? candidate.signal.reason
|
|
4237
|
+
: member.signal.reason);
|
|
4238
|
+
}
|
|
4239
|
+
};
|
|
4240
|
+
if (member.signal.aborted) {
|
|
4241
|
+
withdraw();
|
|
4242
|
+
}
|
|
4243
|
+
else {
|
|
4244
|
+
member.signal.addEventListener('abort', withdraw, { once: true });
|
|
4245
|
+
}
|
|
4246
|
+
if (!registered.started &&
|
|
4247
|
+
registered.members.size === parallelState.regions.length) {
|
|
4248
|
+
startCohort(parallelState, registered);
|
|
4249
|
+
void settleCohort(parallelState, registered);
|
|
4250
|
+
}
|
|
4251
|
+
return await member.settled.promise;
|
|
4252
|
+
}
|
|
4253
|
+
async function settleCohort(parallelState, cohort) {
|
|
4254
|
+
const members = parallelState.regions.map(({ key }) => cohort.members.get(key));
|
|
4255
|
+
const cancellation = new AbortController();
|
|
4256
|
+
const completionOrder = [];
|
|
4257
|
+
const decided = new Map();
|
|
4258
|
+
let decisionSequence = 0;
|
|
4259
|
+
// DR-063 §2: each member failure is one decision, ordered as it was
|
|
4260
|
+
// decided, with the cause decided for exactly that value at that
|
|
4261
|
+
// moment — so a value a sibling later rethrows cannot rename it.
|
|
4262
|
+
const decide = (member, error, cause = failureCauses.causeOf(error)) => {
|
|
4263
|
+
if (decided.has(member))
|
|
4264
|
+
return;
|
|
4265
|
+
decided.set(member, { error, cause, sequence: ++decisionSequence });
|
|
4266
|
+
};
|
|
4267
|
+
const isCancellation = (error) => cancellation.signal.aborted &&
|
|
4268
|
+
Object.is(error, cancellation.signal.reason) &&
|
|
4269
|
+
!activeAborts?.isAbortReason(error);
|
|
4270
|
+
const operationFor = (member) => async (context) => {
|
|
4271
|
+
const callSignal = combineAbortSignals(member.signal, cancellation.signal);
|
|
4272
|
+
try {
|
|
4273
|
+
if (activePlayerKeys.has(member.playerKey)) {
|
|
4274
|
+
await member.reportCollision(callSignal);
|
|
4275
|
+
}
|
|
4276
|
+
activePlayerKeys.add(member.playerKey);
|
|
4277
|
+
try {
|
|
4278
|
+
const result = await member.run(callSignal, cancellation.signal, effectAuthorizedPreExistingBlock(member.effectBoundary, context?.baseline));
|
|
4279
|
+
if (result.status !== 'ok' && !cancellation.signal.aborted) {
|
|
4280
|
+
// DR-063 §2: the failure built for a non-`ok` member result
|
|
4281
|
+
// carries the cause decided for exactly that result, and its
|
|
4282
|
+
// siblings are cancelled with it.
|
|
4283
|
+
const cause = resultFailureCauses.get(result) ??
|
|
4284
|
+
runtimeDefectCause(`callPlayer status ${JSON.stringify(result.status)}`);
|
|
4285
|
+
const failure = attachPlaybookFailureCause(markFsmResultFailure(new Error(result.error ??
|
|
4286
|
+
`captainBridge: callPlayer status "${result.status}"`)), cause);
|
|
4287
|
+
failureCauses.retain(failure, cause);
|
|
4288
|
+
decide(member, failure, cause);
|
|
4289
|
+
cancellation.abort(failure);
|
|
4290
|
+
}
|
|
4291
|
+
return result;
|
|
4292
|
+
}
|
|
4293
|
+
finally {
|
|
4294
|
+
activePlayerKeys.delete(member.playerKey);
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
catch (error) {
|
|
4298
|
+
if (!isCancellation(error))
|
|
4299
|
+
decide(member, error);
|
|
4300
|
+
if (!cancellation.signal.aborted)
|
|
4301
|
+
cancellation.abort(error);
|
|
4302
|
+
throw error;
|
|
4303
|
+
}
|
|
4304
|
+
finally {
|
|
4305
|
+
completionOrder.push(member);
|
|
4306
|
+
}
|
|
4307
|
+
};
|
|
4308
|
+
const completeEffectBoundary = (completion) => cohortCompletionQueue.add(async () => {
|
|
4309
|
+
const member = members.find(({ roleId }) => roleId === completion.roleId);
|
|
4310
|
+
if (member === undefined ||
|
|
4311
|
+
completion.boundary.boundaryId !== member.effectBoundary.boundaryId) {
|
|
4312
|
+
throw new TypeError(`${label} repository cohort completed an undeclared member`);
|
|
4313
|
+
}
|
|
4314
|
+
const evidence = await completionEvidenceFor(member.input, member.roleId, member.playerId, member.signal, undefined)(completion);
|
|
4315
|
+
const settlement = governedSettlementsByBoundaryId.get(member.effectBoundary.boundaryId);
|
|
4316
|
+
if (settlement?.status === 'unresolved') {
|
|
4317
|
+
decide(member, settlement.error);
|
|
4318
|
+
}
|
|
4319
|
+
return evidence;
|
|
4320
|
+
});
|
|
4321
|
+
const runSignal = activeSignal ?? members[0].signal;
|
|
4322
|
+
const invocationId = randomUUID();
|
|
4323
|
+
let settled;
|
|
4324
|
+
try {
|
|
4325
|
+
for (const member of members)
|
|
4326
|
+
member.signal.throwIfAborted();
|
|
4327
|
+
settled = await repositoryCapability.runCohort({
|
|
4328
|
+
signal: runSignal,
|
|
4329
|
+
invocationId,
|
|
4330
|
+
roleIds: members.map(({ roleId }) => roleId),
|
|
4331
|
+
dispositionsByRole: Object.fromEntries(members.map(({ roleId }) => [roleId, ['unchanged']])),
|
|
4332
|
+
effectBoundaries: Object.fromEntries(members.map(({ roleId, effectBoundary }) => [
|
|
4333
|
+
roleId,
|
|
4334
|
+
effectBoundary,
|
|
4335
|
+
])),
|
|
4336
|
+
operations: Object.fromEntries(members.map((member) => [member.roleId, operationFor(member)])),
|
|
4337
|
+
completeEffectBoundary,
|
|
4338
|
+
});
|
|
4339
|
+
if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
|
|
4340
|
+
throw new TypeError(`${label} repository cohort changed its invocation identity`);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
catch (error) {
|
|
4344
|
+
for (const member of members) {
|
|
4345
|
+
governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
|
|
4346
|
+
governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
|
|
4347
|
+
refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
|
|
4348
|
+
}
|
|
4349
|
+
if (!isAbortFailure(error, runSignal))
|
|
4350
|
+
controlPlaneError ??= error;
|
|
4351
|
+
for (const member of members)
|
|
4352
|
+
member.settled.reject(error);
|
|
4353
|
+
return;
|
|
4354
|
+
}
|
|
4355
|
+
const releaseOrder = [
|
|
4356
|
+
...completionOrder,
|
|
4357
|
+
...members.filter((member) => !completionOrder.includes(member)),
|
|
4358
|
+
];
|
|
4359
|
+
const acknowledged = new Map();
|
|
4360
|
+
const failures = new Map();
|
|
4361
|
+
for (const member of releaseOrder) {
|
|
4362
|
+
const operation = isPlainObject(settled.operations)
|
|
4363
|
+
? settled.operations[member.roleId]
|
|
4364
|
+
: undefined;
|
|
4365
|
+
try {
|
|
4366
|
+
const result = acknowledgeGovernedPlayerResult({
|
|
4367
|
+
operation,
|
|
4368
|
+
receipt: isPlainObject(settled.receipts)
|
|
4369
|
+
? settled.receipts[member.roleId]
|
|
4370
|
+
: undefined,
|
|
4371
|
+
effectLedger: settled.effectLedger,
|
|
4372
|
+
}, member.effectBoundary.boundaryId, 'runCohort');
|
|
4373
|
+
acknowledged.set(member, result);
|
|
4374
|
+
const settlement = governedPlayerSettlements.get(result);
|
|
4375
|
+
if (settlement?.status === 'unresolved') {
|
|
4376
|
+
failures.set(member, settlement.error);
|
|
4377
|
+
}
|
|
4378
|
+
else if (result.status !== 'ok') {
|
|
4379
|
+
failures.set(member, decided.get(member)?.error);
|
|
4380
|
+
}
|
|
4381
|
+
}
|
|
4382
|
+
catch (error) {
|
|
4383
|
+
failures.set(member, error);
|
|
4384
|
+
const operationReason = isPlainObject(operation) &&
|
|
4385
|
+
operation.status === 'rejected' &&
|
|
4386
|
+
Object.is(operation.reason, error);
|
|
4387
|
+
if (!operationReason) {
|
|
4388
|
+
// An acknowledgement the host could not prove is a control-plane
|
|
4389
|
+
// defect, exactly as on the exclusive path.
|
|
4390
|
+
if (!isAbortFailure(error, runSignal))
|
|
4391
|
+
controlPlaneError ??= error;
|
|
4392
|
+
decide(member, error);
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
finally {
|
|
4396
|
+
governedSettlementsByBoundaryId.delete(member.effectBoundary.boundaryId);
|
|
4397
|
+
governedCompletionEvidenceByBoundaryId.delete(member.effectBoundary.boundaryId);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
if (failures.size === 0) {
|
|
4401
|
+
for (const member of releaseOrder) {
|
|
4402
|
+
member.settled.resolve(acknowledged.get(member));
|
|
4403
|
+
}
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
for (const result of acknowledged.values()) {
|
|
4407
|
+
governedPlayerSettlements.delete(result);
|
|
4408
|
+
}
|
|
4409
|
+
const first = [...decided.values()].sort((left, right) => left.sequence - right.sequence)[0];
|
|
4410
|
+
const failure = first?.error ??
|
|
4411
|
+
failures.get(releaseOrder.find((member) => failures.has(member))) ??
|
|
4412
|
+
markFsmResultFailure(new Error(`${label} parallel state ${parallelState.stateId} cohort failed`));
|
|
4413
|
+
const cause = first?.cause ?? failureCauses.causeOf(failure);
|
|
4414
|
+
if (cause !== undefined) {
|
|
4415
|
+
failureCauses.retain(failure, cause);
|
|
4416
|
+
attachPlaybookFailureCause(failure, cause);
|
|
4417
|
+
}
|
|
4418
|
+
for (const member of members)
|
|
4419
|
+
member.settled.reject(failure);
|
|
4420
|
+
}
|
|
3620
4421
|
const boundary = {
|
|
3621
4422
|
async callPlayer(input, roleId, freshPrompt, signal) {
|
|
3622
4423
|
// State-entry telemetry/status must precede the call they describe.
|
|
@@ -3661,126 +4462,173 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3661
4462
|
callId,
|
|
3662
4463
|
};
|
|
3663
4464
|
const playerKey = continuationKey(roleId, playerId);
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4465
|
+
const runTracedPlayerCall = async (resume = selectedResume,
|
|
4466
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
4467
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
4468
|
+
preExistingBlock,
|
|
4469
|
+
// DR-067 §1: a cohort member's call signal also carries its
|
|
4470
|
+
// cohort's sibling cancellation.
|
|
4471
|
+
callSignal = signal, cohortCancellation) => {
|
|
4472
|
+
const identity = callIdentity(resume);
|
|
4473
|
+
// DR-067 §1: a parallel machine classifies aborts per call, so a
|
|
4474
|
+
// trace sink rejecting with a sibling's cancellation is forgiven
|
|
4475
|
+
// for that call alone.
|
|
4476
|
+
const traceAborts = parallelProfile === undefined
|
|
4477
|
+
? undefined
|
|
4478
|
+
: abortReasonClassifier(callSignal, activeAborts);
|
|
4479
|
+
const withPreExisting = (text) => preExistingBlock === undefined
|
|
4480
|
+
? text
|
|
4481
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
4482
|
+
const callPrompt = withPreExisting(resume === false ? freshPrompt : prompt);
|
|
4483
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
4484
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...identity, prompt: callPrompt }, position, callSignal, undefined, traceAborts);
|
|
4485
|
+
let rawResult;
|
|
4486
|
+
try {
|
|
4487
|
+
// An abort may land while the awaited started emission drains
|
|
4488
|
+
// (e.g. fired from the trace sink itself); the host call must
|
|
4489
|
+
// never start after abort, so settle the already-started pair
|
|
4490
|
+
// as `aborted` through the catch below.
|
|
4491
|
+
callSignal.throwIfAborted();
|
|
4492
|
+
rawResult = await requireHostPorts().callPlayer(roleId, callPrompt, callSignal, {
|
|
4493
|
+
resume,
|
|
4494
|
+
...(callPrompt === callFreshPrompt
|
|
4495
|
+
? {}
|
|
4496
|
+
: { freshPrompt: callFreshPrompt }),
|
|
4497
|
+
});
|
|
4498
|
+
// A host promise is not required to honor cancellation. Do not
|
|
4499
|
+
// let a late result mutate continuity or publish a successful
|
|
4500
|
+
// finish.
|
|
4501
|
+
callSignal.throwIfAborted();
|
|
4502
|
+
}
|
|
4503
|
+
catch (error) {
|
|
4504
|
+
if (!isAbortFailure(error, callSignal))
|
|
4505
|
+
controlPlaneError ??= error;
|
|
3688
4506
|
try {
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
...(callPrompt === callFreshPrompt
|
|
3697
|
-
? {}
|
|
3698
|
-
: { freshPrompt: callFreshPrompt }),
|
|
3699
|
-
});
|
|
3700
|
-
// A host promise is not required to honor cancellation. Do not
|
|
3701
|
-
// let a late result mutate continuity or publish a successful
|
|
3702
|
-
// finish.
|
|
3703
|
-
signal.throwIfAborted();
|
|
4507
|
+
await emitTrace('player.call.finished', {
|
|
4508
|
+
...identity,
|
|
4509
|
+
status: isAbortFailure(error, callSignal)
|
|
4510
|
+
? 'aborted'
|
|
4511
|
+
: 'error',
|
|
4512
|
+
error: normalizeError(error),
|
|
4513
|
+
}, position, traceAborts);
|
|
3704
4514
|
}
|
|
3705
|
-
catch
|
|
3706
|
-
|
|
3707
|
-
controlPlaneError ??= error;
|
|
3708
|
-
try {
|
|
3709
|
-
await emitTrace('player.call.finished', {
|
|
3710
|
-
...identity,
|
|
3711
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
3712
|
-
error: normalizeError(error),
|
|
3713
|
-
}, position);
|
|
3714
|
-
}
|
|
3715
|
-
catch {
|
|
3716
|
-
// The original non-abort port rejection remains authoritative.
|
|
3717
|
-
}
|
|
3718
|
-
// A thrown port call carries no authoritative result, so the
|
|
3719
|
-
// prior token remains available for a later explicit resume.
|
|
3720
|
-
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
4515
|
+
catch {
|
|
4516
|
+
// The original non-abort port rejection remains authoritative.
|
|
3721
4517
|
}
|
|
3722
|
-
|
|
4518
|
+
// A thrown port call carries no authoritative result, so the
|
|
4519
|
+
// prior token remains available for a later explicit resume.
|
|
4520
|
+
throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
|
|
4521
|
+
}
|
|
4522
|
+
let result;
|
|
4523
|
+
try {
|
|
4524
|
+
result = validatePlayerResult(rawResult);
|
|
4525
|
+
}
|
|
4526
|
+
catch (error) {
|
|
4527
|
+
if (!isAbortFailure(error, callSignal))
|
|
4528
|
+
controlPlaneError ??= error;
|
|
3723
4529
|
try {
|
|
3724
|
-
|
|
4530
|
+
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
|
|
3725
4531
|
}
|
|
3726
|
-
catch
|
|
3727
|
-
|
|
3728
|
-
controlPlaneError ??= error;
|
|
3729
|
-
try {
|
|
3730
|
-
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
3731
|
-
}
|
|
3732
|
-
catch {
|
|
3733
|
-
// The malformed host result remains authoritative.
|
|
3734
|
-
}
|
|
3735
|
-
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
4532
|
+
catch {
|
|
4533
|
+
// The malformed host result remains authoritative.
|
|
3736
4534
|
}
|
|
4535
|
+
throw decidePlayerCallFailure(error, roleId, playerId, callSignal, cohortCancellation);
|
|
4536
|
+
}
|
|
4537
|
+
try {
|
|
4538
|
+
updatePlayerResume(roleId, playerId, result);
|
|
4539
|
+
}
|
|
4540
|
+
catch (error) {
|
|
4541
|
+
if (!isAbortFailure(error, callSignal))
|
|
4542
|
+
controlPlaneError ??= error;
|
|
3737
4543
|
try {
|
|
3738
|
-
|
|
4544
|
+
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position, traceAborts);
|
|
3739
4545
|
}
|
|
3740
|
-
catch
|
|
3741
|
-
|
|
3742
|
-
controlPlaneError ??= error;
|
|
3743
|
-
try {
|
|
3744
|
-
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
3745
|
-
}
|
|
3746
|
-
catch {
|
|
3747
|
-
// The continuation-store failure remains authoritative.
|
|
3748
|
-
}
|
|
3749
|
-
throw error;
|
|
3750
|
-
}
|
|
3751
|
-
await emitTrace('player.call.finished', {
|
|
3752
|
-
...identity,
|
|
3753
|
-
status: result.status,
|
|
3754
|
-
...(result.finalText !== undefined
|
|
3755
|
-
? { finalText: result.finalText }
|
|
3756
|
-
: {}),
|
|
3757
|
-
...(result.error !== undefined
|
|
3758
|
-
? { error: normalizeError(result.error) }
|
|
3759
|
-
: {}),
|
|
3760
|
-
...(result.resumeToken !== undefined
|
|
3761
|
-
? { resumeToken: result.resumeToken }
|
|
3762
|
-
: {}),
|
|
3763
|
-
}, position);
|
|
3764
|
-
// DR-063 §2: the player bridge builds the failure Error for a
|
|
3765
|
-
// non-`ok` result, so the cause is decided here, where the role,
|
|
3766
|
-
// the player, and the reported error are known, and a successful
|
|
3767
|
-
// result clears it so no later failure inherits it.
|
|
3768
|
-
if (result.status === 'ok') {
|
|
3769
|
-
lastFailureCause = undefined;
|
|
3770
|
-
}
|
|
3771
|
-
else if (result.status === 'aborted') {
|
|
3772
|
-
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
3773
|
-
}
|
|
3774
|
-
else {
|
|
3775
|
-
rememberFailureCause(playerFailureCause({
|
|
3776
|
-
roleId,
|
|
3777
|
-
...(playerId === undefined ? {} : { playerId }),
|
|
3778
|
-
error: result.error ??
|
|
3779
|
-
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
3780
|
-
}));
|
|
4546
|
+
catch {
|
|
4547
|
+
// The continuation-store failure remains authoritative.
|
|
3781
4548
|
}
|
|
3782
|
-
|
|
3783
|
-
}
|
|
4549
|
+
throw error;
|
|
4550
|
+
}
|
|
4551
|
+
await emitTrace('player.call.finished', {
|
|
4552
|
+
...identity,
|
|
4553
|
+
status: result.status,
|
|
4554
|
+
...(result.finalText !== undefined
|
|
4555
|
+
? { finalText: result.finalText }
|
|
4556
|
+
: {}),
|
|
4557
|
+
...(result.error !== undefined
|
|
4558
|
+
? { error: normalizeError(result.error) }
|
|
4559
|
+
: {}),
|
|
4560
|
+
...(result.resumeToken !== undefined
|
|
4561
|
+
? { resumeToken: result.resumeToken }
|
|
4562
|
+
: {}),
|
|
4563
|
+
}, position, traceAborts);
|
|
4564
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
4565
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
4566
|
+
// the player, and the reported error are known, and a successful
|
|
4567
|
+
// result clears it so no later failure inherits it.
|
|
4568
|
+
if (result.status === 'ok') {
|
|
4569
|
+
pendingResultFailureCause = undefined;
|
|
4570
|
+
}
|
|
4571
|
+
else if (result.status === 'aborted') {
|
|
4572
|
+
pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
|
|
4573
|
+
}
|
|
4574
|
+
else {
|
|
4575
|
+
pendingResultFailureCause = playerFailureCause({
|
|
4576
|
+
roleId,
|
|
4577
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
4578
|
+
error: result.error ??
|
|
4579
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
if (pendingResultFailureCause !== undefined) {
|
|
4583
|
+
resultFailureCauses.set(result, pendingResultFailureCause);
|
|
4584
|
+
}
|
|
4585
|
+
return result;
|
|
4586
|
+
};
|
|
4587
|
+
const reportPlayerKeyCollision = async (callSignal) => {
|
|
4588
|
+
const error = new Error(`simultaneous calls to player key ${playerKey} are not allowed`);
|
|
4589
|
+
const traceAborts = parallelProfile === undefined
|
|
4590
|
+
? undefined
|
|
4591
|
+
: abortReasonClassifier(callSignal, activeAborts);
|
|
4592
|
+
await emitCallStarted('player.call.started', 'player.call.finished', { ...callIdentity(selectedResume), prompt }, position, callSignal, undefined, traceAborts);
|
|
4593
|
+
await emitTrace('player.call.finished', {
|
|
4594
|
+
...callIdentity(selectedResume),
|
|
4595
|
+
status: 'error',
|
|
4596
|
+
error: normalizeError(error),
|
|
4597
|
+
}, position, traceAborts);
|
|
4598
|
+
throw error;
|
|
4599
|
+
};
|
|
4600
|
+
// DR-067 §1: the working leaves of one parallel state entered
|
|
4601
|
+
// together run as one all-`unchanged` repository cohort, at most once
|
|
4602
|
+
// per public boundary; a corrective re-ask and a single region
|
|
4603
|
+
// resuming after its Boss reply take the exclusive path below.
|
|
4604
|
+
const cohortState = deferredContinuation === undefined
|
|
4605
|
+
? cohortParallelStateFor(input.stateId)
|
|
4606
|
+
: undefined;
|
|
4607
|
+
if (cohortState !== undefined) {
|
|
4608
|
+
const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
|
|
4609
|
+
if (effectBoundary === undefined ||
|
|
4610
|
+
repositoryCapability?.runCohort === undefined) {
|
|
4611
|
+
throw new Error(`${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`);
|
|
4612
|
+
}
|
|
4613
|
+
activeGovernedBoundarySeen = true;
|
|
4614
|
+
return await joinCohort(cohortState, {
|
|
4615
|
+
region: cohortState.regions.find(({ workingStateId }) => workingStateId === input.stateId),
|
|
4616
|
+
input,
|
|
4617
|
+
roleId,
|
|
4618
|
+
playerId,
|
|
4619
|
+
playerKey,
|
|
4620
|
+
signal,
|
|
4621
|
+
effectBoundary,
|
|
4622
|
+
run: (callSignal, cohortCancellation, preExistingBlock) => runTracedPlayerCall(selectedResume, preExistingBlock, callSignal, cohortCancellation),
|
|
4623
|
+
reportCollision: reportPlayerKeyCollision,
|
|
4624
|
+
settled: deferredValue(),
|
|
4625
|
+
});
|
|
4626
|
+
}
|
|
4627
|
+
if (activePlayerKeys.has(playerKey)) {
|
|
4628
|
+
await reportPlayerKeyCollision(signal);
|
|
4629
|
+
}
|
|
4630
|
+
activePlayerKeys.add(playerKey);
|
|
4631
|
+
try {
|
|
3784
4632
|
const effectBoundary = governedBoundarySeed(input, roleId, callId, turnId);
|
|
3785
4633
|
if (deferredContinuation !== undefined) {
|
|
3786
4634
|
if (effectBoundary === undefined ||
|
|
@@ -3849,10 +4697,13 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
3849
4697
|
activePlayerKeys.delete(playerKey);
|
|
3850
4698
|
}
|
|
3851
4699
|
},
|
|
3852
|
-
markPlayerResultFailure(error) {
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4700
|
+
markPlayerResultFailure(error, result) {
|
|
4701
|
+
const cause = (result === undefined ? undefined : resultFailureCauses.get(result)) ??
|
|
4702
|
+
pendingResultFailureCause;
|
|
4703
|
+
if (cause === undefined)
|
|
4704
|
+
return error;
|
|
4705
|
+
failureCauses.retain(error, cause);
|
|
4706
|
+
return attachPlaybookFailureCause(error, cause);
|
|
3856
4707
|
},
|
|
3857
4708
|
takeGovernedPlayerOutput(result) {
|
|
3858
4709
|
const settlement = governedPlayerSettlements.get(result);
|
|
@@ -4040,6 +4891,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4040
4891
|
});
|
|
4041
4892
|
},
|
|
4042
4893
|
};
|
|
4894
|
+
if (parallelProfile !== undefined) {
|
|
4895
|
+
// DR-067 §1: a parallel machine's boundary drain waits for every player
|
|
4896
|
+
// and judge call it started.
|
|
4897
|
+
const untrackedCallPlayer = boundary.callPlayer;
|
|
4898
|
+
const untrackedCallJudge = boundary.callJudge;
|
|
4899
|
+
boundary.callPlayer = (input, roleId, freshPrompt, signal) => trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
|
|
4900
|
+
boundary.callJudge = (purpose, stateId, prompt, signal) => trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
|
|
4901
|
+
}
|
|
4043
4902
|
function playerActor(ports) {
|
|
4044
4903
|
return createPlayerBridge({
|
|
4045
4904
|
resolveRoleId: requireRoleId,
|
|
@@ -4308,7 +5167,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4308
5167
|
const nestedBridge = createNestedPlaybookBridge({
|
|
4309
5168
|
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
4310
5169
|
getBoundarySignal: () => activeSignal,
|
|
4311
|
-
callPlaybook: (request, signal) =>
|
|
5170
|
+
callPlaybook: (request, signal) => {
|
|
5171
|
+
const call = requireHostPorts().callPlaybook(request, signal);
|
|
5172
|
+
return parallelProfile === undefined
|
|
5173
|
+
? call
|
|
5174
|
+
: trackBoundaryCall(Promise.resolve(call));
|
|
5175
|
+
},
|
|
4312
5176
|
emitStarted: async (event, aborts) => {
|
|
4313
5177
|
playbookCallTurnIds.set(event.callId, activeTurnId);
|
|
4314
5178
|
if (hasGovernedPlayerStates) {
|
|
@@ -4347,7 +5211,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4347
5211
|
activeAborts = aborts ?? abortReasonClassifier(signal);
|
|
4348
5212
|
},
|
|
4349
5213
|
bindActorSettlement: (aborts) => {
|
|
4350
|
-
|
|
5214
|
+
if (parallelProfile === undefined)
|
|
5215
|
+
actorSettlementAborts.length = 0;
|
|
5216
|
+
actorSettlementAborts.push(aborts);
|
|
4351
5217
|
},
|
|
4352
5218
|
onControlPlaneError: (error, aborts) => {
|
|
4353
5219
|
// The shared bridge classifies before reporting against its own
|
|
@@ -4381,12 +5247,39 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4381
5247
|
}
|
|
4382
5248
|
return {
|
|
4383
5249
|
...normalized,
|
|
4384
|
-
cause:
|
|
5250
|
+
cause: failureCauses.causeOf(context.lastError) ??
|
|
5251
|
+
runtimeDefectCause(normalized.message),
|
|
4385
5252
|
};
|
|
4386
5253
|
}
|
|
5254
|
+
// DR-063 §2: the failure that parks the machine is the error its parking
|
|
5255
|
+
// transition carried, but a compiled machine keeps a JSON-safe record of
|
|
5256
|
+
// it as `lastError` (gears2fsm requires JSON-safe context), so neither
|
|
5257
|
+
// that value's identity nor its marker reaches the failed state. The cause
|
|
5258
|
+
// decided for the carried error is retained under the value the machine
|
|
5259
|
+
// kept, so every surface of this turn and of each later one publishes the
|
|
5260
|
+
// runtime's own decision for as long as that failure stands. A kept value
|
|
5261
|
+
// that already answers keeps its cause.
|
|
5262
|
+
function bindParkedFailureCause(context, event) {
|
|
5263
|
+
const kept = context.lastError;
|
|
5264
|
+
if (kept === undefined || kept === null)
|
|
5265
|
+
return;
|
|
5266
|
+
if (failureCauses.causeOf(kept) !== undefined)
|
|
5267
|
+
return;
|
|
5268
|
+
const decided = failureCauses.causeOf(actorErrorOf(event));
|
|
5269
|
+
if (decided !== undefined)
|
|
5270
|
+
failureCauses.retain(kept, decided);
|
|
5271
|
+
}
|
|
4387
5272
|
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
4388
|
-
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
4389
|
-
|
|
5273
|
+
// a `player-failed` failure named by role; an abort is `aborted`. A
|
|
5274
|
+
// cancellation the runtime itself issued to a cohort member because its
|
|
5275
|
+
// sibling failed is not this player's failure and decides nothing, so the
|
|
5276
|
+
// originator's cause stands (DR-067 §1, playbook-55).
|
|
5277
|
+
function decidePlayerCallFailure(error, roleId, playerId, signal, cohortCancellation) {
|
|
5278
|
+
if (cohortCancellation?.aborted === true &&
|
|
5279
|
+
Object.is(error, cohortCancellation.reason) &&
|
|
5280
|
+
!activeAborts?.isAbortReason(error)) {
|
|
5281
|
+
return error;
|
|
5282
|
+
}
|
|
4390
5283
|
const cause = isAbortFailure(error, signal)
|
|
4391
5284
|
? ABORTED_FAILURE_CAUSE
|
|
4392
5285
|
: playerFailureCause({
|
|
@@ -4394,7 +5287,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4394
5287
|
...(playerId === undefined ? {} : { playerId }),
|
|
4395
5288
|
error,
|
|
4396
5289
|
});
|
|
4397
|
-
|
|
5290
|
+
failureCauses.retain(error, cause);
|
|
4398
5291
|
return attachPlaybookFailureCause(error, cause);
|
|
4399
5292
|
}
|
|
4400
5293
|
function completeFailedStatuses(statuses, state, context) {
|
|
@@ -4431,10 +5324,20 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4431
5324
|
previousState: previousState ?? null,
|
|
4432
5325
|
state,
|
|
4433
5326
|
};
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
5327
|
+
if (parallelProfile === undefined) {
|
|
5328
|
+
const pendingBossQuestion = pendingBossQuestionForState(state, context);
|
|
5329
|
+
if (pendingBossQuestion !== undefined &&
|
|
5330
|
+
!hasUnresolvedReconciliation()) {
|
|
5331
|
+
payload.pendingBossQuestion = pendingBossQuestion;
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
else if (!hasUnresolvedReconciliation()) {
|
|
5335
|
+
// DR-067 §1: a parallel machine's telemetry carries every question
|
|
5336
|
+
// pending in an active wait, in the plural.
|
|
5337
|
+
const pendingBossQuestions = pendingBossQuestionsFor(state, context);
|
|
5338
|
+
if (pendingBossQuestions.length > 0) {
|
|
5339
|
+
payload.pendingBossQuestions = pendingBossQuestions;
|
|
5340
|
+
}
|
|
4438
5341
|
}
|
|
4439
5342
|
if (state.stateId === 'failed') {
|
|
4440
5343
|
const lastError = failedStateError(context);
|
|
@@ -4454,7 +5357,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4454
5357
|
message,
|
|
4455
5358
|
...(data === undefined ? {} : { data }),
|
|
4456
5359
|
state,
|
|
4457
|
-
...stateIdentity(state
|
|
5360
|
+
...stateIdentity(statusTraceStateId(state)),
|
|
4458
5361
|
}, position),
|
|
4459
5362
|
}));
|
|
4460
5363
|
void enqueueEmission(async () => {
|
|
@@ -4495,8 +5398,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4495
5398
|
controlPlaneError ??= error;
|
|
4496
5399
|
}
|
|
4497
5400
|
function consumeActorSettlementAborts(forSnapshot = false) {
|
|
4498
|
-
const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
|
|
4499
|
-
actorSettlementAborts = undefined;
|
|
5401
|
+
const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
|
|
4500
5402
|
actorSettlementErrorAborts = undefined;
|
|
4501
5403
|
if (forSnapshot && aborts !== undefined) {
|
|
4502
5404
|
// XState can report an errored root through both its inspection
|
|
@@ -4571,7 +5473,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4571
5473
|
try {
|
|
4572
5474
|
const snap = inspectionEvent.snapshot;
|
|
4573
5475
|
const state = normalizePlaybookSnapshot(snap);
|
|
4574
|
-
if (state.stateId === undefined
|
|
5476
|
+
if (state.stateId === undefined &&
|
|
5477
|
+
!(parallelProfile?.states.some(({ stateId }) => state.activeStateIds.includes(stateId)) ?? false)) {
|
|
4575
5478
|
throw new Error(`${label} root snapshot must expose exactly one playbook state id`);
|
|
4576
5479
|
}
|
|
4577
5480
|
const previousState = priorState;
|
|
@@ -4596,6 +5499,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4596
5499
|
}
|
|
4597
5500
|
const context = (snap.context ??
|
|
4598
5501
|
{});
|
|
5502
|
+
if (state.stateId === 'failed') {
|
|
5503
|
+
bindParkedFailureCause(context, inspectionEvent.event);
|
|
5504
|
+
}
|
|
4599
5505
|
if (state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
|
|
4600
5506
|
deferredReconciliationOperationId !== undefined) {
|
|
4601
5507
|
priorState = state;
|
|
@@ -4607,7 +5513,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4607
5513
|
validateBoundQuestionProjection();
|
|
4608
5514
|
}
|
|
4609
5515
|
const payload = structuredStateTelemetryPayload(previousState, state, inspectionEvent.event, context);
|
|
4610
|
-
const stateStatuses = completeFailedStatuses(
|
|
5516
|
+
const stateStatuses = completeFailedStatuses(bossRelevantStateIds !== undefined && usesDefaultStatuses
|
|
5517
|
+
? parallelStatusesFor(previousState, state, context)
|
|
5518
|
+
: statusesForState(state, context, inspectionEvent.event), state, context);
|
|
4611
5519
|
const outcomeStatuses = usesDefaultStatuses
|
|
4612
5520
|
? acceptedOutcomes.map(({ acceptedOutcome }) => ({
|
|
4613
5521
|
message: `→ ${acceptedOutcome}`,
|
|
@@ -4788,13 +5696,17 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4788
5696
|
emissionQueue.clear();
|
|
4789
5697
|
judgeQueue.clear();
|
|
4790
5698
|
appliedReceipts.clear();
|
|
5699
|
+
pendingCohorts.clear();
|
|
5700
|
+
consumedCohortBoundaries.clear();
|
|
5701
|
+
activeBoundaryCalls.clear();
|
|
5702
|
+
cohortCompletionQueue.clear();
|
|
4791
5703
|
actor = undefined;
|
|
4792
5704
|
session = undefined;
|
|
4793
5705
|
savedPorts = undefined;
|
|
4794
5706
|
runtimePorts = undefined;
|
|
4795
5707
|
activeSignal = undefined;
|
|
4796
5708
|
activeAborts = undefined;
|
|
4797
|
-
actorSettlementAborts =
|
|
5709
|
+
actorSettlementAborts.length = 0;
|
|
4798
5710
|
actorSettlementErrorAborts = undefined;
|
|
4799
5711
|
activeAbortEmission = undefined;
|
|
4800
5712
|
activeTurnId = undefined;
|
|
@@ -4869,11 +5781,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4869
5781
|
// context member above sources. A candidate whose event the live
|
|
4870
5782
|
// snapshot does not accept — or whose payload the runtime can source
|
|
4871
5783
|
// from neither — is excluded rather than completed with invented text.
|
|
5784
|
+
// The caller has already applied the PBRT-71 replay fence.
|
|
4872
5785
|
function retryActionFor(snapshot, stateId) {
|
|
4873
5786
|
if (stateId !== 'failed')
|
|
4874
5787
|
return undefined;
|
|
4875
|
-
if (!failedAttemptAllowsReplay())
|
|
4876
|
-
return undefined;
|
|
4877
5788
|
const retryEvent = retryEventFrom(snapshot);
|
|
4878
5789
|
if (retryEvent === undefined)
|
|
4879
5790
|
return undefined;
|
|
@@ -4984,6 +5895,15 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
4984
5895
|
});
|
|
4985
5896
|
return derived;
|
|
4986
5897
|
}
|
|
5898
|
+
// PBRT-71 / DR-040 §4: out of the failure state, a jump restarts the
|
|
5899
|
+
// failed attempt's work exactly as the retry does, so one fence gates
|
|
5900
|
+
// both. Unless every governed boundary of that attempt carries a
|
|
5901
|
+
// complete `unchanged` receipt, neither is advertised — and `apply`,
|
|
5902
|
+
// which revalidates against this list, accepts neither — so no
|
|
5903
|
+
// control can replay a player call whose effect may already exist.
|
|
5904
|
+
if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
|
|
5905
|
+
return derived;
|
|
5906
|
+
}
|
|
4987
5907
|
const retry = retryActionFor(snapshot, state.stateId);
|
|
4988
5908
|
if (retry !== undefined)
|
|
4989
5909
|
derived.push(retry);
|
|
@@ -5049,9 +5969,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5049
5969
|
// state whose source declares no description publishes none: an id is
|
|
5050
5970
|
// never promoted into a description by default.
|
|
5051
5971
|
function stateDescriptionFor(state) {
|
|
5972
|
+
// DR-067 §1: inside a parallel state, the meaning published is the
|
|
5973
|
+
// root state's — the parallel parent's own description.
|
|
5052
5974
|
const keys = [
|
|
5053
5975
|
...(state.stateId === undefined ? [] : [state.stateId]),
|
|
5054
5976
|
...(typeof state.value === 'string' ? [state.value] : []),
|
|
5977
|
+
...(isPlainObject(state.value) ? Object.keys(state.value) : []),
|
|
5055
5978
|
...state.activeStateIds,
|
|
5056
5979
|
];
|
|
5057
5980
|
for (const key of keys) {
|
|
@@ -5335,7 +6258,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5335
6258
|
const context = (snapshot
|
|
5336
6259
|
?.context ?? {});
|
|
5337
6260
|
const actual = state?.stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
5338
|
-
?
|
|
6261
|
+
? singlePendingBossQuestion(state, context)
|
|
5339
6262
|
: undefined;
|
|
5340
6263
|
if (!isDeepStrictEqual(actual, expected)) {
|
|
5341
6264
|
throw new Error(`${label} deferred FSM question does not equal its durable binding`);
|
|
@@ -5594,7 +6517,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5594
6517
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
5595
6518
|
const context = (snapshot.context ??
|
|
5596
6519
|
{});
|
|
5597
|
-
const pending =
|
|
6520
|
+
const pending = singlePendingBossQuestion(state, context);
|
|
5598
6521
|
if (pending === undefined ||
|
|
5599
6522
|
currentBoundDeferredOperation(pending)?.operationId !== operationId) {
|
|
5600
6523
|
throw new TypeError(`${label} restored deferred wait does not equal its FSM question`);
|
|
@@ -5691,16 +6614,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5691
6614
|
const state = currentState();
|
|
5692
6615
|
if (state.status !== 'active' || !state.quiescent)
|
|
5693
6616
|
return undefined;
|
|
5694
|
-
const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot());
|
|
5695
6617
|
const context = actor.getSnapshot()
|
|
5696
6618
|
.context;
|
|
6619
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
6620
|
+
// surface publishes it, completed from the runtime's own cause slot, so
|
|
6621
|
+
// a thrown value that refused the cause still explains itself after a
|
|
6622
|
+
// restart.
|
|
6623
|
+
const machineSnapshot = detachPersistedMachineSnapshot(actor.getPersistedSnapshot(), state.stateId === 'failed'
|
|
6624
|
+
? { lastError: failedStateError(context ?? {}) }
|
|
6625
|
+
: {});
|
|
5697
6626
|
effectLedgerMirror = currentEffectLedger();
|
|
5698
6627
|
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
5699
6628
|
syncDeferredReconciliationOverlay();
|
|
5700
6629
|
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
5701
|
-
const
|
|
5702
|
-
?
|
|
5703
|
-
:
|
|
6630
|
+
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
6631
|
+
? pendingBossQuestionsFor(state, context ?? {})
|
|
6632
|
+
: [];
|
|
5704
6633
|
const failedEffectAttempt = hasGovernedPlayerStates &&
|
|
5705
6634
|
state.stateId === 'failed' &&
|
|
5706
6635
|
failedAttemptMatchesCurrentLedger(effectLedgerMirror)
|
|
@@ -5725,16 +6654,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5725
6654
|
: {}),
|
|
5726
6655
|
},
|
|
5727
6656
|
state,
|
|
5728
|
-
pendingBossQuestions: pending
|
|
5729
|
-
|
|
5730
|
-
:
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
question: pending.question,
|
|
5735
|
-
sourceItem: pending.sourceItem,
|
|
5736
|
-
},
|
|
5737
|
-
],
|
|
6657
|
+
pendingBossQuestions: pendingQuestions.map((pending) => ({
|
|
6658
|
+
questionId: pending.questionId,
|
|
6659
|
+
asker: pending.asker,
|
|
6660
|
+
question: pending.question,
|
|
6661
|
+
sourceItem: pending.sourceItem,
|
|
6662
|
+
})),
|
|
5738
6663
|
effectLedger: effectLedgerMirror,
|
|
5739
6664
|
...(retainedEffectSourceSessionId === undefined
|
|
5740
6665
|
? {}
|
|
@@ -5757,13 +6682,19 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5757
6682
|
await rehydrateSnapshot('restore', nextSession, snapshot);
|
|
5758
6683
|
},
|
|
5759
6684
|
// DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
|
|
5760
|
-
// engagement identity and counter lineage, exposed separately so
|
|
5761
|
-
// capability-less
|
|
5762
|
-
//
|
|
6685
|
+
// engagement identity and counter lineage, exposed separately so a
|
|
6686
|
+
// capability-less runtime can omit it. Runtime-visible preflight
|
|
6687
|
+
// mismatches reject before effects; after preflight the new
|
|
5763
6688
|
// session.started boundary owns failed-start cleanup just like init.
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
6689
|
+
// DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
|
|
6690
|
+
// machine that declares a parallel state omits the member.
|
|
6691
|
+
...(parallelProfile === undefined
|
|
6692
|
+
? {
|
|
6693
|
+
async adopt(nextSession, snapshot, context) {
|
|
6694
|
+
await rehydrateSnapshot('adopt', nextSession, snapshot, context);
|
|
6695
|
+
},
|
|
6696
|
+
}
|
|
6697
|
+
: {}),
|
|
5767
6698
|
// DR-029 / PBRT-52: side-effect-free control view over the live
|
|
5768
6699
|
// snapshot, valid at parked quiescence outside an active boundary.
|
|
5769
6700
|
// The view is detached and frozen; producing it emits nothing and
|
|
@@ -5784,9 +6715,9 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5784
6715
|
const state = currentState();
|
|
5785
6716
|
const context = (snapshot.context ??
|
|
5786
6717
|
{});
|
|
5787
|
-
const
|
|
5788
|
-
?
|
|
5789
|
-
:
|
|
6718
|
+
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
6719
|
+
? pendingBossQuestionsFor(state, context)
|
|
6720
|
+
: [];
|
|
5790
6721
|
const lastError = state.stateId === 'failed'
|
|
5791
6722
|
? failedStateError(context)
|
|
5792
6723
|
: normalizeErrorFull(context.lastError);
|
|
@@ -5800,16 +6731,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5800
6731
|
...(projectedContext !== undefined
|
|
5801
6732
|
? { context: projectedContext }
|
|
5802
6733
|
: {}),
|
|
5803
|
-
pendingQuestions: pending
|
|
5804
|
-
|
|
5805
|
-
:
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
question: pending.question,
|
|
5810
|
-
sourceItem: pending.sourceItem,
|
|
5811
|
-
},
|
|
5812
|
-
],
|
|
6734
|
+
pendingQuestions: pendingQuestions.map((pending) => ({
|
|
6735
|
+
questionId: pending.questionId,
|
|
6736
|
+
asker: pending.asker,
|
|
6737
|
+
question: pending.question,
|
|
6738
|
+
sourceItem: pending.sourceItem,
|
|
6739
|
+
})),
|
|
5813
6740
|
...(lastError !== undefined ? { lastError } : {}),
|
|
5814
6741
|
actions: deriveControlActions(snapshot).map(({ action }) => action),
|
|
5815
6742
|
});
|
|
@@ -5859,6 +6786,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
5859
6786
|
const callId = `apply-${++applyCallSequence}`;
|
|
5860
6787
|
const position = { turnId, callId };
|
|
5861
6788
|
activeTurnId = turnId;
|
|
6789
|
+
publicBoundarySequence += 1;
|
|
5862
6790
|
beginAutomaticReplayBoundary();
|
|
5863
6791
|
activeSignal = signal;
|
|
5864
6792
|
activeAborts = abortReasonClassifier(signal);
|
|
@@ -6030,6 +6958,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6030
6958
|
if (receipt !== undefined && receipt.disposition !== 'rejected') {
|
|
6031
6959
|
appliedReceipts.set(key, receipt);
|
|
6032
6960
|
}
|
|
6961
|
+
if (parallelProfile !== undefined)
|
|
6962
|
+
await drainBoundaryCalls();
|
|
6033
6963
|
try {
|
|
6034
6964
|
await drainEmissions();
|
|
6035
6965
|
}
|
|
@@ -6123,14 +7053,16 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6123
7053
|
refreshRetainedEffectFenceFromHost();
|
|
6124
7054
|
const turnId = ++turnSequence;
|
|
6125
7055
|
activeTurnId = turnId;
|
|
7056
|
+
publicBoundarySequence += 1;
|
|
6126
7057
|
beginAutomaticReplayBoundary();
|
|
6127
7058
|
activeSignal = signal;
|
|
6128
7059
|
activeAborts = abortReasonClassifier(signal);
|
|
6129
7060
|
activeAbortEmission = undefined;
|
|
6130
7061
|
controlPlaneError = undefined;
|
|
6131
|
-
// DR-063 §2: each turn decides its own failures, so
|
|
6132
|
-
// carries
|
|
6133
|
-
|
|
7062
|
+
// DR-063 §2: each turn decides its own failures, so no pending result
|
|
7063
|
+
// cause carries into this one; a cause decided for a parked failure
|
|
7064
|
+
// stays bound to that failure.
|
|
7065
|
+
pendingResultFailureCause = undefined;
|
|
6134
7066
|
let result;
|
|
6135
7067
|
let operationError;
|
|
6136
7068
|
// The boundary sentinel releases on every exit: a settlement defect
|
|
@@ -6161,7 +7093,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6161
7093
|
const stateId = normalizePlaybookSnapshot(snapshot).stateId;
|
|
6162
7094
|
const snapshotContext = (snapshot
|
|
6163
7095
|
.context ?? {});
|
|
6164
|
-
deferredPending =
|
|
7096
|
+
deferredPending = singlePendingBossQuestion(normalizePlaybookSnapshot(snapshot), snapshotContext);
|
|
6165
7097
|
pendingRequiresDeferredBinding =
|
|
6166
7098
|
deferredPending !== undefined &&
|
|
6167
7099
|
outcomeAuthority?.governedPlayerStates[deferredPending.resumeStateId]?.needsBossReply?.repositoryDisposition === 'deferred' &&
|
|
@@ -6292,6 +7224,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6292
7224
|
catch (error) {
|
|
6293
7225
|
operationError = error;
|
|
6294
7226
|
}
|
|
7227
|
+
if (parallelProfile !== undefined)
|
|
7228
|
+
await drainBoundaryCalls();
|
|
6295
7229
|
let drainError;
|
|
6296
7230
|
try {
|
|
6297
7231
|
await drainEmissions();
|
|
@@ -6380,6 +7314,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6380
7314
|
return runResultFor('no-action');
|
|
6381
7315
|
}
|
|
6382
7316
|
activeTurnId = playbookCallTurnIds.get(input.callId);
|
|
7317
|
+
publicBoundarySequence += 1;
|
|
6383
7318
|
bindAutomaticReplayBoundary(playbookCallEffectPrefixes.has(input.callId)
|
|
6384
7319
|
? playbookCallEffectPrefixes.get(input.callId)
|
|
6385
7320
|
: hasGovernedPlayerStates
|
|
@@ -6422,6 +7357,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6422
7357
|
error: normalizeError(input.signal.reason),
|
|
6423
7358
|
};
|
|
6424
7359
|
}
|
|
7360
|
+
if (parallelProfile !== undefined)
|
|
7361
|
+
await drainBoundaryCalls();
|
|
6425
7362
|
let drainError;
|
|
6426
7363
|
try {
|
|
6427
7364
|
await drainEmissions();
|
|
@@ -6535,10 +7472,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
6535
7472
|
emissionQueue.clear();
|
|
6536
7473
|
judgeQueue.clear();
|
|
6537
7474
|
appliedReceipts.clear();
|
|
7475
|
+
pendingCohorts.clear();
|
|
7476
|
+
consumedCohortBoundaries.clear();
|
|
7477
|
+
activeBoundaryCalls.clear();
|
|
7478
|
+
cohortCompletionQueue.clear();
|
|
6538
7479
|
actor = undefined;
|
|
6539
7480
|
activeSignal = undefined;
|
|
6540
7481
|
activeAborts = undefined;
|
|
6541
|
-
actorSettlementAborts =
|
|
7482
|
+
actorSettlementAborts.length = 0;
|
|
6542
7483
|
actorSettlementErrorAborts = undefined;
|
|
6543
7484
|
activeAbortEmission = undefined;
|
|
6544
7485
|
activeTurnId = undefined;
|