@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
|
@@ -158,7 +158,7 @@ export interface RuntimeBoundaryCalls {
|
|
|
158
158
|
* with the cause the boundary decided at the call itself, where the role,
|
|
159
159
|
* the resolved player, and the reported error are known.
|
|
160
160
|
*/
|
|
161
|
-
markPlayerResultFailure?(error: Error): Error;
|
|
161
|
+
markPlayerResultFailure?(error: Error, result?: PlayerResult): Error;
|
|
162
162
|
callJudge(
|
|
163
163
|
purpose: JudgePurpose,
|
|
164
164
|
stateId: string | undefined,
|
|
@@ -284,12 +284,12 @@ const GOVERNED_JUDGE_FAILURE_REASONS: readonly string[] = [
|
|
|
284
284
|
'semantic correction budget is unavailable',
|
|
285
285
|
];
|
|
286
286
|
|
|
287
|
-
const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
|
|
287
|
+
export const ABORTED_FAILURE_CAUSE: PlaybookFailureCause = assertPlaybookFailureCause({
|
|
288
288
|
code: 'aborted',
|
|
289
289
|
evidence: {},
|
|
290
290
|
});
|
|
291
291
|
|
|
292
|
-
function runtimeDefectCause(reason: string): PlaybookFailureCause {
|
|
292
|
+
export function runtimeDefectCause(reason: string): PlaybookFailureCause {
|
|
293
293
|
return assertPlaybookFailureCause({
|
|
294
294
|
code: 'runtime-defect',
|
|
295
295
|
evidence: { reason: reason.length === 0 ? 'unknown runtime defect' : reason },
|
|
@@ -315,7 +315,7 @@ function failureErrorCode(error: unknown): string | undefined {
|
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
function playerFailureCause(input: {
|
|
318
|
+
export function playerFailureCause(input: {
|
|
319
319
|
readonly roleId: string;
|
|
320
320
|
readonly playerId?: string;
|
|
321
321
|
readonly error: unknown;
|
|
@@ -336,6 +336,84 @@ function playerFailureCause(input: {
|
|
|
336
336
|
});
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/**
|
|
340
|
+
* DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
341
|
+
* mismatch supplies its own receipt-read cause; the reasons a runtime owns map
|
|
342
|
+
* to adjudication, abort, and runtime-defect codes. Exported so linked
|
|
343
|
+
* machinery of an artifact's own decides the same reasons.
|
|
344
|
+
*/
|
|
345
|
+
export function governedSettlementCause(
|
|
346
|
+
reason: string,
|
|
347
|
+
error: unknown,
|
|
348
|
+
aborted: boolean,
|
|
349
|
+
supplied: PlaybookFailureCause | undefined,
|
|
350
|
+
): PlaybookFailureCause {
|
|
351
|
+
if (supplied !== undefined) return supplied;
|
|
352
|
+
if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
|
|
353
|
+
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
354
|
+
const transport = failureErrorEvidence(error);
|
|
355
|
+
return assertPlaybookFailureCause({
|
|
356
|
+
code: 'judge-failed',
|
|
357
|
+
evidence: {
|
|
358
|
+
reason,
|
|
359
|
+
...(transport === undefined ? {} : { error: transport }),
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return runtimeDefectCause(reason);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
368
|
+
* marker — a string, a frozen error — and for the record a machine keeps in
|
|
369
|
+
* place of the thrown value, kept by identity for as long as the failure
|
|
370
|
+
* stands, so every surface that reads the FSM's own `lastError` publishes the
|
|
371
|
+
* cause decided for exactly that failure, whatever turn reads it. A value
|
|
372
|
+
* that carries the marker answers from it; the map is bounded, since only
|
|
373
|
+
* values that lack the marker need an entry.
|
|
374
|
+
*/
|
|
375
|
+
export interface PlaybookFailureCauseRetention {
|
|
376
|
+
retain(error: unknown, cause: PlaybookFailureCause): void;
|
|
377
|
+
causeOf(error: unknown): PlaybookFailureCause | undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const FAILURE_CAUSE_RETENTION_LIMIT = 8;
|
|
381
|
+
|
|
382
|
+
export function createFailureCauseRetention(): PlaybookFailureCauseRetention {
|
|
383
|
+
const causes = new Map<unknown, PlaybookFailureCause>();
|
|
384
|
+
return {
|
|
385
|
+
retain(error, cause) {
|
|
386
|
+
if (error === undefined || error === null) return;
|
|
387
|
+
causes.delete(error);
|
|
388
|
+
causes.set(error, cause);
|
|
389
|
+
while (causes.size > FAILURE_CAUSE_RETENTION_LIMIT) {
|
|
390
|
+
causes.delete(causes.keys().next().value);
|
|
391
|
+
}
|
|
392
|
+
},
|
|
393
|
+
causeOf(error) {
|
|
394
|
+
if (error === undefined || error === null) return undefined;
|
|
395
|
+
return normalizeError(error).cause ?? causes.get(error);
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* DR-063 §2: the value an invoked actor's rejection carried into the machine,
|
|
402
|
+
* read from the XState error event that delivered it, or `undefined` for any
|
|
403
|
+
* other event.
|
|
404
|
+
*/
|
|
405
|
+
function actorErrorOf(event: unknown): unknown {
|
|
406
|
+
if (typeof event !== 'object' || event === null) return undefined;
|
|
407
|
+
try {
|
|
408
|
+
const { type, error } = event as { type?: unknown; error?: unknown };
|
|
409
|
+
return typeof type === 'string' && type.startsWith('xstate.error.actor.')
|
|
410
|
+
? error
|
|
411
|
+
: undefined;
|
|
412
|
+
} catch {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
339
417
|
|
|
340
418
|
// ---------------------------------------------------------------------------
|
|
341
419
|
// DR-028: both call boundaries treat an `ok` result whose `finalText` is
|
|
@@ -448,6 +526,23 @@ type XStateEffectBoundarySeed = Omit<
|
|
|
448
526
|
'playbookId' | 'canonicalWorktree' | 'baseline' | 'cohortId'
|
|
449
527
|
>;
|
|
450
528
|
|
|
529
|
+
/** One member's completion inside a repository cohort (DR-067 §1). */
|
|
530
|
+
type XStateRepositoryCohortCompletion<T> =
|
|
531
|
+
XStateRepositoryExclusiveCompletion<T> & { readonly roleId: string };
|
|
532
|
+
|
|
533
|
+
interface XStateRepositoryCohortResult<T> {
|
|
534
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
535
|
+
readonly invocationId: string;
|
|
536
|
+
readonly operations: Readonly<
|
|
537
|
+
Record<
|
|
538
|
+
string,
|
|
539
|
+
XStateRepositoryOperationSettlement<T> | XStateRepositoryOperationRejection
|
|
540
|
+
>
|
|
541
|
+
>;
|
|
542
|
+
readonly receipts: Readonly<Record<string, PlaybookRepositoryReceipt>>;
|
|
543
|
+
readonly effectLedger: PlaybookEffectLedger;
|
|
544
|
+
}
|
|
545
|
+
|
|
451
546
|
export interface XStateRepositoryCapability {
|
|
452
547
|
runExclusive<T>(options: {
|
|
453
548
|
readonly signal: AbortSignal;
|
|
@@ -482,6 +577,34 @@ export interface XStateRepositoryCapability {
|
|
|
482
577
|
readonly signal: AbortSignal;
|
|
483
578
|
readonly operationId: string;
|
|
484
579
|
}): Promise<XStateRepositoryDeferredParked | XStateRepositoryDeferredRestoreResult>;
|
|
580
|
+
/**
|
|
581
|
+
* DR-067 §1 / PBRT-73: one repository claim over the working leaves of a
|
|
582
|
+
* parallel state entered together — every member an `unchanged` boundary
|
|
583
|
+
* of one cohort, the operations run concurrently under that claim, and
|
|
584
|
+
* one completion callback per member. Required for an artifact that
|
|
585
|
+
* declares a parallel state.
|
|
586
|
+
*/
|
|
587
|
+
runCohort?<T>(options: {
|
|
588
|
+
readonly signal: AbortSignal;
|
|
589
|
+
readonly invocationId: string;
|
|
590
|
+
readonly roleIds: readonly string[];
|
|
591
|
+
readonly dispositionsByRole: Readonly<Record<string, readonly ['unchanged']>>;
|
|
592
|
+
readonly effectBoundaries: Readonly<Record<string, XStateEffectBoundarySeed>>;
|
|
593
|
+
readonly operations: Readonly<
|
|
594
|
+
Record<
|
|
595
|
+
string,
|
|
596
|
+
(context: {
|
|
597
|
+
readonly baseline: PlaybookRepositoryReceipt['baseline'];
|
|
598
|
+
readonly identity: unknown;
|
|
599
|
+
readonly invocationId: string;
|
|
600
|
+
readonly roleId: string;
|
|
601
|
+
}) => Promise<T>
|
|
602
|
+
>
|
|
603
|
+
>;
|
|
604
|
+
readonly completeEffectBoundary: (
|
|
605
|
+
completion: XStateRepositoryCohortCompletion<T>,
|
|
606
|
+
) => XStateRepositoryCompletionEvidence | Promise<XStateRepositoryCompletionEvidence>;
|
|
607
|
+
}): Promise<XStateRepositoryCohortResult<T>>;
|
|
485
608
|
}
|
|
486
609
|
|
|
487
610
|
function assertNoConfiguredHostCapabilities(value: unknown, label: string): void {
|
|
@@ -583,6 +706,7 @@ function configuredOptionsFromFactoryInput(
|
|
|
583
706
|
function repositoryCapabilityFromHostCapabilities(
|
|
584
707
|
hostCapabilities: object | undefined,
|
|
585
708
|
label: string,
|
|
709
|
+
requireCohort = false,
|
|
586
710
|
): XStateRepositoryCapability {
|
|
587
711
|
const descriptor =
|
|
588
712
|
hostCapabilities === undefined
|
|
@@ -599,10 +723,15 @@ function repositoryCapabilityFromHostCapabilities(
|
|
|
599
723
|
Array.isArray(repository) ||
|
|
600
724
|
typeof (repository as { runExclusive?: unknown }).runExclusive !==
|
|
601
725
|
'function' ||
|
|
602
|
-
typeof (repository as { runDeferred?: unknown }).runDeferred !==
|
|
726
|
+
typeof (repository as { runDeferred?: unknown }).runDeferred !==
|
|
727
|
+
'function' ||
|
|
728
|
+
(requireCohort &&
|
|
729
|
+
typeof (repository as { runCohort?: unknown }).runCohort !== 'function')
|
|
603
730
|
) {
|
|
604
731
|
throw new TypeError(
|
|
605
|
-
|
|
732
|
+
requireCohort
|
|
733
|
+
? `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive, runCohort, and runDeferred`
|
|
734
|
+
: `${label} schema-3 factory input hostCapabilities.repository must be an own data property exposing runExclusive and runDeferred`,
|
|
606
735
|
);
|
|
607
736
|
}
|
|
608
737
|
return repository as XStateRepositoryCapability;
|
|
@@ -1058,7 +1187,7 @@ export function normalizeErrorFull(
|
|
|
1058
1187
|
// slc/link.md §Abort: cancellation is causal identity with the applicable
|
|
1059
1188
|
// signal's reason — never an `AbortError` name, never bare signal state. A
|
|
1060
1189
|
// distinct failure observed while the signal is aborted stays a non-abort
|
|
1061
|
-
// control error and takes precedence
|
|
1190
|
+
// control error and takes precedence.
|
|
1062
1191
|
function isAbortFailure(error: unknown, signal: AbortSignal): boolean {
|
|
1063
1192
|
return signal.aborted && Object.is(error, signal.reason);
|
|
1064
1193
|
}
|
|
@@ -1109,7 +1238,38 @@ function pendingBossQuestionForState(
|
|
|
1109
1238
|
export function pendingBossQuestionFromContext(
|
|
1110
1239
|
context: Record<string, unknown>,
|
|
1111
1240
|
): PlaybookPendingBossQuestionContext | undefined {
|
|
1112
|
-
|
|
1241
|
+
return pendingBossQuestionRecord(context.pendingBossQuestion);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* DR-067 §1: the keyed form of gears2fsm's Boss-reply suspension. Each
|
|
1246
|
+
* well-formed `context.pendingBossQuestions[stateId]` record whose own
|
|
1247
|
+
* `questionId` equals its key counts only while a wait state that resumes
|
|
1248
|
+
* its `resumeStateId` is active, so an answered question the context
|
|
1249
|
+
* retains for the resumed prompt never reads as pending. Ordered by
|
|
1250
|
+
* question id.
|
|
1251
|
+
*/
|
|
1252
|
+
function keyedPendingBossQuestions(
|
|
1253
|
+
context: Record<string, unknown>,
|
|
1254
|
+
resumesFromActiveWait: (resumeStateId: string) => boolean,
|
|
1255
|
+
): PlaybookPendingBossQuestionContext[] {
|
|
1256
|
+
const keyed = context.pendingBossQuestions;
|
|
1257
|
+
if (!isPlainObject(keyed)) return [];
|
|
1258
|
+
const pending: PlaybookPendingBossQuestionContext[] = [];
|
|
1259
|
+
for (const [key, value] of Object.entries(keyed)) {
|
|
1260
|
+
const record = pendingBossQuestionRecord(value);
|
|
1261
|
+
if (record === undefined || record.questionId !== key) continue;
|
|
1262
|
+
if (!resumesFromActiveWait(record.resumeStateId)) continue;
|
|
1263
|
+
pending.push(record);
|
|
1264
|
+
}
|
|
1265
|
+
return pending.sort((left, right) =>
|
|
1266
|
+
left.questionId.localeCompare(right.questionId),
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function pendingBossQuestionRecord(
|
|
1271
|
+
pending: unknown,
|
|
1272
|
+
): PlaybookPendingBossQuestionContext | undefined {
|
|
1113
1273
|
if (
|
|
1114
1274
|
pending === undefined ||
|
|
1115
1275
|
pending === null ||
|
|
@@ -1269,8 +1429,8 @@ function preExistingChangesBlock(
|
|
|
1269
1429
|
* `one-descendant-commit` disposition may commit nothing, so it is told
|
|
1270
1430
|
* nothing.
|
|
1271
1431
|
*/
|
|
1272
|
-
function effectAuthorizedPreExistingBlock(
|
|
1273
|
-
effectBoundary: Pick<
|
|
1432
|
+
export function effectAuthorizedPreExistingBlock(
|
|
1433
|
+
effectBoundary: Pick<PlaybookEffectBoundaryStart, 'dispositions'>,
|
|
1274
1434
|
baseline: PlaybookRepositoryReceipt['baseline'] | undefined,
|
|
1275
1435
|
): string | undefined {
|
|
1276
1436
|
return effectBoundary.dispositions.includes('one-descendant-commit')
|
|
@@ -1587,8 +1747,8 @@ export function defaultBuildJudgePrompt(
|
|
|
1587
1747
|
* so the judge omits it. Rendering the clause verbatim asked the judge for
|
|
1588
1748
|
* `question`, `planningResult`, or `evaluatedRevision`, which the reconciler
|
|
1589
1749
|
* rejects as a structural error, spending the single correction on a
|
|
1590
|
-
* self-inflicted defect. Exported so
|
|
1591
|
-
*
|
|
1750
|
+
* self-inflicted defect. Exported so linked machinery of an artifact's own
|
|
1751
|
+
* renders the identical contract instead of restating it.
|
|
1592
1752
|
*/
|
|
1593
1753
|
export function renderGovernedOutcomeContract(
|
|
1594
1754
|
guard: string,
|
|
@@ -1883,7 +2043,7 @@ export function createPlayerBridge(
|
|
|
1883
2043
|
const failure = new Error(
|
|
1884
2044
|
result.error ?? `captainBridge: callPlayer status "${result.status}"`,
|
|
1885
2045
|
);
|
|
1886
|
-
throw boundary?.markPlayerResultFailure?.(failure) ?? failure;
|
|
2046
|
+
throw boundary?.markPlayerResultFailure?.(failure, result) ?? failure;
|
|
1887
2047
|
}
|
|
1888
2048
|
const finalText = result.finalText ?? '';
|
|
1889
2049
|
if (isEmptyFinalText(finalText)) {
|
|
@@ -2110,7 +2270,81 @@ function transitionTargets(transition: unknown): string[] {
|
|
|
2110
2270
|
return targets;
|
|
2111
2271
|
}
|
|
2112
2272
|
|
|
2113
|
-
|
|
2273
|
+
function transitionArmTargets(transition: unknown): string[] {
|
|
2274
|
+
const arms = Array.isArray(transition) ? transition : [transition];
|
|
2275
|
+
const targets: string[] = [];
|
|
2276
|
+
for (const arm of arms) {
|
|
2277
|
+
if (typeof arm === 'string') {
|
|
2278
|
+
targets.push(arm);
|
|
2279
|
+
} else if (isPlainObject(arm) && typeof arm.target === 'string') {
|
|
2280
|
+
targets.push(arm.target);
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
return targets;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function playbookStateIdOf(stateDef: unknown): string | undefined {
|
|
2287
|
+
if (!isPlainObject(stateDef) || !isPlainObject(stateDef.meta)) {
|
|
2288
|
+
return undefined;
|
|
2289
|
+
}
|
|
2290
|
+
const playbook = stateDef.meta.playbook;
|
|
2291
|
+
return isPlainObject(playbook) && typeof playbook.stateId === 'string'
|
|
2292
|
+
? playbook.stateId
|
|
2293
|
+
: undefined;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
function stateTagsOf(stateDef: Record<string, unknown>): readonly string[] {
|
|
2297
|
+
const tags = stateDef.tags;
|
|
2298
|
+
if (typeof tags === 'string') return [tags];
|
|
2299
|
+
return Array.isArray(tags)
|
|
2300
|
+
? tags.filter((tag): tag is string => typeof tag === 'string')
|
|
2301
|
+
: [];
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
/** Every explicit node `id` in the config, mapped to its playbook state id. */
|
|
2305
|
+
function playbookStateIdsByNodeId(config: unknown): ReadonlyMap<string, string> {
|
|
2306
|
+
const byNodeId = new Map<string, string>();
|
|
2307
|
+
const visit = (stateDef: unknown): void => {
|
|
2308
|
+
if (!isPlainObject(stateDef)) return;
|
|
2309
|
+
const stateId = playbookStateIdOf(stateDef);
|
|
2310
|
+
if (typeof stateDef.id === 'string' && stateId !== undefined) {
|
|
2311
|
+
byNodeId.set(stateDef.id, stateId);
|
|
2312
|
+
}
|
|
2313
|
+
if (isPlainObject(stateDef.states)) {
|
|
2314
|
+
for (const child of Object.values(stateDef.states)) visit(child);
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
if (isPlainObject(config) && isPlainObject(config.states)) {
|
|
2318
|
+
for (const child of Object.values(config.states)) visit(child);
|
|
2319
|
+
}
|
|
2320
|
+
return byNodeId;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* One transition target written inside a parallel region, resolved to the
|
|
2325
|
+
* playbook state id it names: an `#id` reference through the node ids, and a
|
|
2326
|
+
* bare sibling key through the region's own leaves.
|
|
2327
|
+
*/
|
|
2328
|
+
function resolveRegionTarget(
|
|
2329
|
+
target: string,
|
|
2330
|
+
regionStates: Record<string, unknown>,
|
|
2331
|
+
byNodeId: ReadonlyMap<string, string>,
|
|
2332
|
+
): string {
|
|
2333
|
+
if (target.startsWith('#')) {
|
|
2334
|
+
const nodeId = target.slice(1);
|
|
2335
|
+
return byNodeId.get(nodeId) ?? nodeId;
|
|
2336
|
+
}
|
|
2337
|
+
if (Object.prototype.hasOwnProperty.call(regionStates, target)) {
|
|
2338
|
+
return playbookStateIdOf(regionStates[target]) ?? target;
|
|
2339
|
+
}
|
|
2340
|
+
return target;
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
/**
|
|
2344
|
+
* Targets of the FSM's `awaitBossReply` BOSS_REPLY transitions, plus — for
|
|
2345
|
+
* the parallel shape gears2fsm compiles (DR-067) — the targets of each
|
|
2346
|
+
* region wait leaf's branch-local BOSS_REPLY arms, resolved to state ids.
|
|
2347
|
+
*/
|
|
2114
2348
|
export function resumableStateIdsFromMachine(
|
|
2115
2349
|
machine: AnyStateMachine,
|
|
2116
2350
|
): ReadonlySet<string> {
|
|
@@ -2118,13 +2352,321 @@ export function resumableStateIdsFromMachine(
|
|
|
2118
2352
|
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
2119
2353
|
return new Set();
|
|
2120
2354
|
}
|
|
2355
|
+
const resumable = new Set<string>();
|
|
2121
2356
|
const awaitState = config.states[BOSS_REPLY_WAIT_STATE_ID];
|
|
2122
|
-
if (
|
|
2123
|
-
|
|
2357
|
+
if (isPlainObject(awaitState) && isPlainObject(awaitState.on)) {
|
|
2358
|
+
const bossReply = awaitState.on.BOSS_REPLY;
|
|
2359
|
+
if (bossReply !== undefined) {
|
|
2360
|
+
for (const target of transitionTargets(bossReply)) resumable.add(target);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
const byNodeId = playbookStateIdsByNodeId(config);
|
|
2364
|
+
for (const stateDef of Object.values(config.states)) {
|
|
2365
|
+
if (
|
|
2366
|
+
!isPlainObject(stateDef) ||
|
|
2367
|
+
stateDef.type !== 'parallel' ||
|
|
2368
|
+
!isPlainObject(stateDef.states)
|
|
2369
|
+
) {
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
for (const region of Object.values(stateDef.states)) {
|
|
2373
|
+
if (!isPlainObject(region) || !isPlainObject(region.states)) continue;
|
|
2374
|
+
const regionStates = region.states;
|
|
2375
|
+
for (const leaf of Object.values(regionStates)) {
|
|
2376
|
+
if (!isPlainObject(leaf) || !isPlainObject(leaf.on)) continue;
|
|
2377
|
+
const bossReply = leaf.on.BOSS_REPLY;
|
|
2378
|
+
if (bossReply === undefined) continue;
|
|
2379
|
+
for (const target of transitionArmTargets(bossReply)) {
|
|
2380
|
+
resumable.add(resolveRegionTarget(target, regionStates, byNodeId));
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
return resumable;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
/** One region of a compiled parallel state (DR-067 §1). */
|
|
2389
|
+
interface XStateParallelRegion {
|
|
2390
|
+
readonly key: string;
|
|
2391
|
+
readonly roleId: string;
|
|
2392
|
+
readonly workingStateId: string;
|
|
2393
|
+
readonly waitStateId: string;
|
|
2394
|
+
readonly finalStateId: string;
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/** One root `type: 'parallel'` state of the compiled shape. */
|
|
2398
|
+
interface XStateParallelState {
|
|
2399
|
+
readonly stateId: string;
|
|
2400
|
+
readonly regions: readonly XStateParallelRegion[];
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
/**
|
|
2404
|
+
* The parallel profile derived from a machine that declares a parallel
|
|
2405
|
+
* state (DR-067 §1): its parallel states, each wait state's resume targets
|
|
2406
|
+
* — region waits and the root reply wait alike — and, per state id, the
|
|
2407
|
+
* config nodes whose `on` maps an active state contributes.
|
|
2408
|
+
*/
|
|
2409
|
+
interface XStateParallelProfile {
|
|
2410
|
+
readonly states: readonly XStateParallelState[];
|
|
2411
|
+
readonly parallelStateByWorkingStateId: ReadonlyMap<
|
|
2412
|
+
string,
|
|
2413
|
+
XStateParallelState
|
|
2414
|
+
>;
|
|
2415
|
+
readonly waitResumeTargets: ReadonlyMap<string, ReadonlySet<string>>;
|
|
2416
|
+
readonly transitionNodesByStateId: ReadonlyMap<
|
|
2417
|
+
string,
|
|
2418
|
+
readonly Record<string, unknown>[]
|
|
2419
|
+
>;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/** One registered cohort member awaiting its cohort's settlement. */
|
|
2423
|
+
interface XStateCohortMember {
|
|
2424
|
+
readonly region: XStateParallelRegion;
|
|
2425
|
+
readonly input: PlaybookPlayerInput;
|
|
2426
|
+
readonly roleId: string;
|
|
2427
|
+
readonly playerId: string | undefined;
|
|
2428
|
+
readonly playerKey: string;
|
|
2429
|
+
readonly signal: AbortSignal;
|
|
2430
|
+
readonly effectBoundary: XStateEffectBoundarySeed;
|
|
2431
|
+
readonly run: (
|
|
2432
|
+
callSignal: AbortSignal,
|
|
2433
|
+
cohortCancellation: AbortSignal,
|
|
2434
|
+
preExistingBlock: string | undefined,
|
|
2435
|
+
) => Promise<PlayerResult>;
|
|
2436
|
+
readonly reportCollision: (callSignal: AbortSignal) => Promise<never>;
|
|
2437
|
+
readonly settled: DeferredValue<PlayerResult>;
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/** The cohort of one parallel state within one public boundary. */
|
|
2441
|
+
interface PendingCohort {
|
|
2442
|
+
readonly boundarySequence: number;
|
|
2443
|
+
readonly members: Map<string, XStateCohortMember>;
|
|
2444
|
+
started: boolean;
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function malformedParallelState(
|
|
2448
|
+
label: string,
|
|
2449
|
+
key: string,
|
|
2450
|
+
reason: string,
|
|
2451
|
+
): Error {
|
|
2452
|
+
return new Error(
|
|
2453
|
+
`${label} parallel state ${key} is not the compiled parallel shape: ${reason}`,
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* DR-067 §1 / PBRT-52: the factory's domain is flat root states plus root
|
|
2459
|
+
* `type: 'parallel'` states of the one shape gears2fsm compiles — each
|
|
2460
|
+
* region a compound state with exactly one player-invoking `playbook.busy`
|
|
2461
|
+
* working leaf, exactly one `playbook.parked` wait leaf whose BOSS_REPLY
|
|
2462
|
+
* arms resume that working leaf, and exactly one final leaf, every leaf
|
|
2463
|
+
* carrying a stable `meta.playbook.stateId` distinct from every other
|
|
2464
|
+
* state's, and the parent's `onDone` the join. Any other compound state
|
|
2465
|
+
* keeps the flat-domain rejection. Returns `undefined` for a flat machine.
|
|
2466
|
+
*/
|
|
2467
|
+
function parallelProfileFromMachine(
|
|
2468
|
+
machine: AnyStateMachine,
|
|
2469
|
+
label: string,
|
|
2470
|
+
): XStateParallelProfile | undefined {
|
|
2471
|
+
const config = (machine as unknown as { config?: unknown }).config;
|
|
2472
|
+
if (!isPlainObject(config) || !isPlainObject(config.states)) {
|
|
2473
|
+
return undefined;
|
|
2474
|
+
}
|
|
2475
|
+
const rootStates = config.states;
|
|
2476
|
+
const parallelKeys = Object.entries(rootStates)
|
|
2477
|
+
.filter(
|
|
2478
|
+
([, stateDef]) => isPlainObject(stateDef) && stateDef.type === 'parallel',
|
|
2479
|
+
)
|
|
2480
|
+
.map(([key]) => key);
|
|
2481
|
+
for (const stateDef of Object.values(rootStates)) {
|
|
2482
|
+
if (
|
|
2483
|
+
isPlainObject(stateDef) &&
|
|
2484
|
+
stateDef.type !== 'parallel' &&
|
|
2485
|
+
isPlainObject(stateDef.states) &&
|
|
2486
|
+
Object.keys(stateDef.states).length > 0
|
|
2487
|
+
) {
|
|
2488
|
+
throw new Error(
|
|
2489
|
+
`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`,
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if (parallelKeys.length === 0) return undefined;
|
|
2494
|
+
|
|
2495
|
+
const byNodeId = playbookStateIdsByNodeId(config);
|
|
2496
|
+
const seenStateIds = new Set<string>();
|
|
2497
|
+
for (const stateDef of Object.values(rootStates)) {
|
|
2498
|
+
const stateId = playbookStateIdOf(stateDef);
|
|
2499
|
+
if (stateId !== undefined) seenStateIds.add(stateId);
|
|
2500
|
+
}
|
|
2501
|
+
const states: XStateParallelState[] = [];
|
|
2502
|
+
const parallelStateByWorkingStateId = new Map<string, XStateParallelState>();
|
|
2503
|
+
const waitResumeTargets = new Map<string, ReadonlySet<string>>();
|
|
2504
|
+
const transitionNodesByStateId = new Map<
|
|
2505
|
+
string,
|
|
2506
|
+
readonly Record<string, unknown>[]
|
|
2507
|
+
>();
|
|
2508
|
+
for (const key of parallelKeys) {
|
|
2509
|
+
const parent = rootStates[key] as Record<string, unknown>;
|
|
2510
|
+
if (parent.onDone === undefined) {
|
|
2511
|
+
throw malformedParallelState(label, key, 'it declares no onDone join');
|
|
2512
|
+
}
|
|
2513
|
+
if (!isPlainObject(parent.states) || Object.keys(parent.states).length < 2) {
|
|
2514
|
+
throw malformedParallelState(
|
|
2515
|
+
label,
|
|
2516
|
+
key,
|
|
2517
|
+
'it must declare at least two regions',
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
const regions: XStateParallelRegion[] = [];
|
|
2521
|
+
const roleIds = new Set<string>();
|
|
2522
|
+
for (const [regionKey, region] of Object.entries(parent.states)) {
|
|
2523
|
+
const regionReason = (reason: string) =>
|
|
2524
|
+
malformedParallelState(label, key, `region ${regionKey} ${reason}`);
|
|
2525
|
+
if (
|
|
2526
|
+
!isPlainObject(region) ||
|
|
2527
|
+
region.type === 'parallel' ||
|
|
2528
|
+
region.type === 'final' ||
|
|
2529
|
+
region.type === 'history' ||
|
|
2530
|
+
!isPlainObject(region.states)
|
|
2531
|
+
) {
|
|
2532
|
+
throw regionReason('must be a compound state of leaves');
|
|
2533
|
+
}
|
|
2534
|
+
const regionStates = region.states;
|
|
2535
|
+
let working: [string, Record<string, unknown>] | undefined;
|
|
2536
|
+
let wait: [string, Record<string, unknown>] | undefined;
|
|
2537
|
+
let final: [string, Record<string, unknown>] | undefined;
|
|
2538
|
+
for (const [leafKey, leaf] of Object.entries(regionStates)) {
|
|
2539
|
+
if (!isPlainObject(leaf)) {
|
|
2540
|
+
throw regionReason(`leaf ${leafKey} must be a state object`);
|
|
2541
|
+
}
|
|
2542
|
+
if (
|
|
2543
|
+
(isPlainObject(leaf.states) && Object.keys(leaf.states).length > 0) ||
|
|
2544
|
+
leaf.type === 'parallel' ||
|
|
2545
|
+
leaf.type === 'history'
|
|
2546
|
+
) {
|
|
2547
|
+
throw regionReason(`leaf ${leafKey} must be an atomic state`);
|
|
2548
|
+
}
|
|
2549
|
+
const leafStateId = playbookStateIdOf(leaf);
|
|
2550
|
+
if (leafStateId === undefined || leafStateId.trim().length === 0) {
|
|
2551
|
+
throw regionReason(
|
|
2552
|
+
`leaf ${leafKey} must carry a string meta.playbook.stateId`,
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
if (seenStateIds.has(leafStateId)) {
|
|
2556
|
+
throw regionReason(
|
|
2557
|
+
`leaf ${leafKey} reuses playbook state id ${leafStateId}`,
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
seenStateIds.add(leafStateId);
|
|
2561
|
+
const invoke = leaf.invoke;
|
|
2562
|
+
const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
|
|
2563
|
+
const tags = stateTagsOf(leaf);
|
|
2564
|
+
if (leaf.type === 'final') {
|
|
2565
|
+
if (final !== undefined) throw regionReason('declares two final leaves');
|
|
2566
|
+
final = [leafKey, leaf];
|
|
2567
|
+
} else if (
|
|
2568
|
+
invokes.length === 1 &&
|
|
2569
|
+
isPlainObject(invokes[0]) &&
|
|
2570
|
+
invokes[0].src === 'player' &&
|
|
2571
|
+
tags.includes('playbook.busy')
|
|
2572
|
+
) {
|
|
2573
|
+
if (working !== undefined) {
|
|
2574
|
+
throw regionReason('declares two working leaves');
|
|
2575
|
+
}
|
|
2576
|
+
working = [leafKey, leaf];
|
|
2577
|
+
} else if (
|
|
2578
|
+
invokes.length === 0 &&
|
|
2579
|
+
tags.includes('playbook.parked') &&
|
|
2580
|
+
isPlainObject(leaf.on) &&
|
|
2581
|
+
leaf.on.BOSS_REPLY !== undefined
|
|
2582
|
+
) {
|
|
2583
|
+
if (wait !== undefined) throw regionReason('declares two wait leaves');
|
|
2584
|
+
wait = [leafKey, leaf];
|
|
2585
|
+
} else {
|
|
2586
|
+
throw regionReason(
|
|
2587
|
+
`leaf ${leafKey} is neither its player-invoking playbook.busy working leaf, its playbook.parked BOSS_REPLY wait leaf, nor its final leaf`,
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
if (working === undefined || wait === undefined || final === undefined) {
|
|
2592
|
+
throw regionReason(
|
|
2593
|
+
'must hold exactly one working leaf, one wait leaf, and one final leaf',
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
if (region.initial !== working[0]) {
|
|
2597
|
+
throw regionReason('must start in its working leaf');
|
|
2598
|
+
}
|
|
2599
|
+
const workingStateId = playbookStateIdOf(working[1])!;
|
|
2600
|
+
const waitStateId = playbookStateIdOf(wait[1])!;
|
|
2601
|
+
const finalStateId = playbookStateIdOf(final[1])!;
|
|
2602
|
+
const replyTargets = transitionArmTargets(
|
|
2603
|
+
(wait[1].on as Record<string, unknown>).BOSS_REPLY,
|
|
2604
|
+
).map((target) => resolveRegionTarget(target, regionStates, byNodeId));
|
|
2605
|
+
const regionStateIds = new Set([waitStateId, finalStateId]);
|
|
2606
|
+
if (
|
|
2607
|
+
!replyTargets.includes(workingStateId) ||
|
|
2608
|
+
replyTargets.some((target) => regionStateIds.has(target))
|
|
2609
|
+
) {
|
|
2610
|
+
throw regionReason(
|
|
2611
|
+
'wait leaf BOSS_REPLY arms must resume its working leaf',
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
const workingMeta = (working[1].meta as Record<string, unknown>)
|
|
2615
|
+
.playbook as Record<string, unknown>;
|
|
2616
|
+
const roleId = workingMeta.role;
|
|
2617
|
+
if (typeof roleId !== 'string' || roleId.trim().length === 0) {
|
|
2618
|
+
throw regionReason('working leaf must declare meta.playbook.role');
|
|
2619
|
+
}
|
|
2620
|
+
if (roleIds.has(roleId)) {
|
|
2621
|
+
throw malformedParallelState(
|
|
2622
|
+
label,
|
|
2623
|
+
key,
|
|
2624
|
+
`role ${roleId} works in more than one region`,
|
|
2625
|
+
);
|
|
2626
|
+
}
|
|
2627
|
+
roleIds.add(roleId);
|
|
2628
|
+
regions.push({
|
|
2629
|
+
key: regionKey,
|
|
2630
|
+
roleId,
|
|
2631
|
+
workingStateId,
|
|
2632
|
+
waitStateId,
|
|
2633
|
+
finalStateId,
|
|
2634
|
+
});
|
|
2635
|
+
waitResumeTargets.set(waitStateId, new Set([workingStateId]));
|
|
2636
|
+
for (const [, leaf] of [working, wait, final]) {
|
|
2637
|
+
transitionNodesByStateId.set(playbookStateIdOf(leaf)!, [region, leaf]);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
const parallelState: XStateParallelState = {
|
|
2641
|
+
stateId: key,
|
|
2642
|
+
regions: Object.freeze(regions),
|
|
2643
|
+
};
|
|
2644
|
+
states.push(parallelState);
|
|
2645
|
+
for (const region of regions) {
|
|
2646
|
+
parallelStateByWorkingStateId.set(region.workingStateId, parallelState);
|
|
2647
|
+
}
|
|
2648
|
+
transitionNodesByStateId.set(key, [parent]);
|
|
2649
|
+
}
|
|
2650
|
+
for (const [key, stateDef] of Object.entries(rootStates)) {
|
|
2651
|
+
if (!isPlainObject(stateDef) || parallelKeys.includes(key)) continue;
|
|
2652
|
+
transitionNodesByStateId.set(key, [stateDef]);
|
|
2124
2653
|
}
|
|
2125
|
-
const
|
|
2126
|
-
if (
|
|
2127
|
-
|
|
2654
|
+
const rootWait = rootStates[BOSS_REPLY_WAIT_STATE_ID];
|
|
2655
|
+
if (isPlainObject(rootWait) && isPlainObject(rootWait.on)) {
|
|
2656
|
+
const bossReply = rootWait.on.BOSS_REPLY;
|
|
2657
|
+
if (bossReply !== undefined) {
|
|
2658
|
+
waitResumeTargets.set(
|
|
2659
|
+
BOSS_REPLY_WAIT_STATE_ID,
|
|
2660
|
+
new Set(transitionTargets(bossReply)),
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return Object.freeze({
|
|
2665
|
+
states: Object.freeze(states),
|
|
2666
|
+
parallelStateByWorkingStateId,
|
|
2667
|
+
waitResumeTargets,
|
|
2668
|
+
transitionNodesByStateId,
|
|
2669
|
+
});
|
|
2128
2670
|
}
|
|
2129
2671
|
|
|
2130
2672
|
// ---------------------------------------------------------------------------
|
|
@@ -2937,10 +3479,63 @@ function eventContractPrompt(contract: XStateBossEventSpec): string {
|
|
|
2937
3479
|
}`;
|
|
2938
3480
|
}
|
|
2939
3481
|
|
|
3482
|
+
/**
|
|
3483
|
+
* DR-067 §1: the questions pending in a parallel-declaring machine — the
|
|
3484
|
+
* keyed records whose resume state an active wait state (a region wait leaf
|
|
3485
|
+
* or the root reply wait) resumes.
|
|
3486
|
+
*/
|
|
3487
|
+
function parallelPendingBossQuestions(
|
|
3488
|
+
profile: XStateParallelProfile,
|
|
3489
|
+
activeStateIds: readonly string[],
|
|
3490
|
+
context: Record<string, unknown>,
|
|
3491
|
+
): PlaybookPendingBossQuestionContext[] {
|
|
3492
|
+
return keyedPendingBossQuestions(context, (resumeStateId) =>
|
|
3493
|
+
activeStateIds.some(
|
|
3494
|
+
(stateId) =>
|
|
3495
|
+
profile.waitResumeTargets.get(stateId)?.has(resumeStateId) === true,
|
|
3496
|
+
),
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
/** Event types the root and every active state node of a parallel machine configure. */
|
|
3501
|
+
function parallelConfiguredEventTypes(
|
|
3502
|
+
machine: AnyStateMachine,
|
|
3503
|
+
profile: XStateParallelProfile,
|
|
3504
|
+
activeStateIds: readonly string[],
|
|
3505
|
+
): ReadonlySet<string> {
|
|
3506
|
+
const configured = new Set<string>();
|
|
3507
|
+
const config = (machine as unknown as { config?: unknown }).config;
|
|
3508
|
+
if (!isPlainObject(config)) return configured;
|
|
3509
|
+
if (isPlainObject(config.on)) {
|
|
3510
|
+
for (const type of Object.keys(config.on)) configured.add(type);
|
|
3511
|
+
}
|
|
3512
|
+
for (const stateId of activeStateIds) {
|
|
3513
|
+
for (const node of profile.transitionNodesByStateId.get(stateId) ?? []) {
|
|
3514
|
+
if (!isPlainObject(node.on)) continue;
|
|
3515
|
+
for (const type of Object.keys(node.on)) configured.add(type);
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
return configured;
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
function activeStateIdsOf(snapshotOrState: unknown): readonly string[] {
|
|
3522
|
+
const listed = (snapshotOrState as { activeStateIds?: unknown } | null)
|
|
3523
|
+
?.activeStateIds;
|
|
3524
|
+
if (Array.isArray(listed)) {
|
|
3525
|
+
return listed.filter((stateId): stateId is string => typeof stateId === 'string');
|
|
3526
|
+
}
|
|
3527
|
+
try {
|
|
3528
|
+
return normalizePlaybookSnapshot(snapshotOrState).activeStateIds;
|
|
3529
|
+
} catch {
|
|
3530
|
+
return [];
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
|
|
2940
3534
|
function makeDefaultClassifyBossText(
|
|
2941
3535
|
machine: AnyStateMachine,
|
|
2942
3536
|
entryEvent: { type: string; textField: string } | undefined,
|
|
2943
3537
|
bossEvents: readonly XStateBossEventSpec[],
|
|
3538
|
+
parallelProfile?: XStateParallelProfile,
|
|
2944
3539
|
): (
|
|
2945
3540
|
text: string,
|
|
2946
3541
|
ports: PlaybookPorts,
|
|
@@ -2960,16 +3555,54 @@ function makeDefaultClassifyBossText(
|
|
|
2960
3555
|
// is answered history, so the prompt must not present it as pending —
|
|
2961
3556
|
// a judge told a question awaits at the failure state is steered toward
|
|
2962
3557
|
// a reply it cannot select or toward no action at all.
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3558
|
+
let pendingQuestions: readonly PlaybookPendingBossQuestionContext[];
|
|
3559
|
+
let configuredTypes: ReadonlySet<string>;
|
|
3560
|
+
if (parallelProfile === undefined) {
|
|
3561
|
+
const pending =
|
|
3562
|
+
stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
3563
|
+
? pendingBossQuestionFromContext(state.context)
|
|
3564
|
+
: undefined;
|
|
3565
|
+
pendingQuestions = pending === undefined ? [] : [pending];
|
|
3566
|
+
configuredTypes = configuredEventTypesForState(machine, stateId);
|
|
3567
|
+
} else {
|
|
3568
|
+
// DR-067 §1: every question pending in any active wait is offered, and
|
|
3569
|
+
// the events of every active state node apply.
|
|
3570
|
+
const activeStateIds = activeStateIdsOf(snapshotOrState);
|
|
3571
|
+
pendingQuestions = parallelPendingBossQuestions(
|
|
3572
|
+
parallelProfile,
|
|
3573
|
+
activeStateIds,
|
|
3574
|
+
state.context,
|
|
3575
|
+
);
|
|
3576
|
+
configuredTypes = parallelConfiguredEventTypes(
|
|
3577
|
+
machine,
|
|
3578
|
+
parallelProfile,
|
|
3579
|
+
activeStateIds,
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
// Several pending questions: a reply must name the one it answers, and
|
|
3583
|
+
// the classifier never guesses among them (gears2fsm §Parallel groups).
|
|
3584
|
+
const severalPending = pendingQuestions.length > 1;
|
|
3585
|
+
const applicable = [...contracts.values()]
|
|
3586
|
+
.filter(
|
|
3587
|
+
(contract) =>
|
|
3588
|
+
configuredTypes.has(contract.type) &&
|
|
3589
|
+
(contract.type !== 'BOSS_REPLY' || pendingQuestions.length > 0),
|
|
3590
|
+
)
|
|
3591
|
+
.map((contract): XStateBossEventSpec =>
|
|
3592
|
+
contract.type === 'BOSS_REPLY' && severalPending
|
|
3593
|
+
? {
|
|
3594
|
+
type: 'BOSS_REPLY',
|
|
3595
|
+
fields: {
|
|
3596
|
+
questionId: {
|
|
3597
|
+
source: 'judge',
|
|
3598
|
+
required: true,
|
|
3599
|
+
values: pendingQuestions.map(({ questionId }) => questionId),
|
|
3600
|
+
},
|
|
3601
|
+
answer: { source: 'text', required: true },
|
|
3602
|
+
},
|
|
3603
|
+
}
|
|
3604
|
+
: contract,
|
|
3605
|
+
);
|
|
2973
3606
|
|
|
2974
3607
|
const lines = [
|
|
2975
3608
|
'Classify the following Boss message into exactly one event.',
|
|
@@ -2978,13 +3611,18 @@ function makeDefaultClassifyBossText(
|
|
|
2978
3611
|
'',
|
|
2979
3612
|
`Current state: ${currentState}`,
|
|
2980
3613
|
];
|
|
2981
|
-
|
|
3614
|
+
for (const pending of pendingQuestions) {
|
|
2982
3615
|
lines.push(
|
|
2983
3616
|
`Pending question id: ${pending.questionId}`,
|
|
2984
3617
|
`Pending asker: ${askerLabel(pending.asker)}`,
|
|
2985
3618
|
`Pending Boss question: ${pending.question}`,
|
|
2986
3619
|
);
|
|
2987
3620
|
}
|
|
3621
|
+
if (severalPending) {
|
|
3622
|
+
lines.push(
|
|
3623
|
+
'Several Boss questions are pending: a BOSS_REPLY must name the questionId of the one question it answers.',
|
|
3624
|
+
);
|
|
3625
|
+
}
|
|
2988
3626
|
lines.push('', 'Allowed JSON objects:', '- { "type": "NO_ACTION" }');
|
|
2989
3627
|
for (const contract of applicable) {
|
|
2990
3628
|
lines.push(`- ${eventContractPrompt(contract)}`);
|
|
@@ -3077,20 +3715,23 @@ function makeDefaultClassifyBossText(
|
|
|
3077
3715
|
}
|
|
3078
3716
|
|
|
3079
3717
|
if (eventType === 'BOSS_REPLY') {
|
|
3080
|
-
if (
|
|
3718
|
+
if (pendingQuestions.length === 0) {
|
|
3081
3719
|
await ports.emitStatus(
|
|
3082
3720
|
'Classifier returned BOSS_REPLY without a pending question',
|
|
3083
3721
|
);
|
|
3084
3722
|
return undefined;
|
|
3085
3723
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3724
|
+
if (!severalPending) {
|
|
3725
|
+
const pending = pendingQuestions[0]!;
|
|
3726
|
+
const questionId = event.questionId;
|
|
3727
|
+
if (questionId !== undefined && questionId !== pending.questionId) {
|
|
3728
|
+
await ports.emitStatus(
|
|
3729
|
+
`Classifier supplied unknown questionId for BOSS_REPLY: ${String(questionId)}`,
|
|
3730
|
+
);
|
|
3731
|
+
return undefined;
|
|
3732
|
+
}
|
|
3733
|
+
event.questionId = pending.questionId;
|
|
3092
3734
|
}
|
|
3093
|
-
event.questionId = pending.questionId;
|
|
3094
3735
|
}
|
|
3095
3736
|
const can = (snapshotOrState as { can?: unknown } | null)?.can;
|
|
3096
3737
|
if (
|
|
@@ -3127,34 +3768,6 @@ interface TracePosition {
|
|
|
3127
3768
|
callId?: string;
|
|
3128
3769
|
}
|
|
3129
3770
|
|
|
3130
|
-
function machineDeclaresParallelState(machine: AnyStateMachine): boolean {
|
|
3131
|
-
const visit = (stateDef: unknown): boolean => {
|
|
3132
|
-
if (!isPlainObject(stateDef)) return false;
|
|
3133
|
-
if (stateDef.type === 'parallel') return true;
|
|
3134
|
-
if (!isPlainObject(stateDef.states)) return false;
|
|
3135
|
-
return Object.values(stateDef.states).some(visit);
|
|
3136
|
-
};
|
|
3137
|
-
return visit((machine as unknown as { config?: unknown }).config);
|
|
3138
|
-
}
|
|
3139
|
-
|
|
3140
|
-
// PBRT-52: the factory's domain is FLAT single-region machines — every
|
|
3141
|
-
// state a direct child of the root, so each snapshot exposes exactly one
|
|
3142
|
-
// playbook state id and every state-keyed lookup (deterministic entries,
|
|
3143
|
-
// retry, reply-wait pendingness, configured events, descriptions) indexes
|
|
3144
|
-
// one unambiguous identity. A compound child would be accepted and then
|
|
3145
|
-
// silently misbehave on all of those gates, so it is rejected up front
|
|
3146
|
-
// exactly like a parallel region.
|
|
3147
|
-
function machineDeclaresNestedState(machine: AnyStateMachine): boolean {
|
|
3148
|
-
const config = (machine as unknown as { config?: unknown }).config;
|
|
3149
|
-
if (!isPlainObject(config) || !isPlainObject(config.states)) return false;
|
|
3150
|
-
return Object.values(config.states).some(
|
|
3151
|
-
(stateDef) =>
|
|
3152
|
-
isPlainObject(stateDef) &&
|
|
3153
|
-
isPlainObject(stateDef.states) &&
|
|
3154
|
-
Object.keys(stateDef.states).length > 0,
|
|
3155
|
-
);
|
|
3156
|
-
}
|
|
3157
|
-
|
|
3158
3771
|
// PBRT-52: the factory's lookups index states by their root key, and the
|
|
3159
3772
|
// published playbook identity is `meta.playbook.stateId` — the two must
|
|
3160
3773
|
// coincide or a machine can advertise a pending question or retry under an
|
|
@@ -3249,10 +3862,12 @@ function assertUnfinishedFinalStateIds(
|
|
|
3249
3862
|
* the optional parked-session snapshot capability (DR-014) and the retained-
|
|
3250
3863
|
* snapshot adoption capability (DR-038).
|
|
3251
3864
|
*
|
|
3252
|
-
* Scope: flat single-region machines —
|
|
3253
|
-
*
|
|
3254
|
-
* state
|
|
3255
|
-
*
|
|
3865
|
+
* Scope: flat single-region machines — every root state's
|
|
3866
|
+
* `meta.playbook.stateId` equal to its state key, so each snapshot exposes
|
|
3867
|
+
* exactly one playbook state id — plus the parallel profile of DR-067: root
|
|
3868
|
+
* `type: 'parallel'` states of the shape gears2fsm compiles, whose regions
|
|
3869
|
+
* run as one all-`unchanged` repository cohort with keyed pending questions.
|
|
3870
|
+
* A machine declaring a parallel state omits `adopt`.
|
|
3256
3871
|
*/
|
|
3257
3872
|
export function createXStatePlaybookRuntime<
|
|
3258
3873
|
TOptions,
|
|
@@ -3288,16 +3903,9 @@ export function createXStatePlaybookRuntime<
|
|
|
3288
3903
|
`${label} artifacts must not derive concrete player bindings`,
|
|
3289
3904
|
);
|
|
3290
3905
|
}
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
);
|
|
3295
|
-
}
|
|
3296
|
-
if (machineDeclaresNestedState(machine)) {
|
|
3297
|
-
throw new Error(
|
|
3298
|
-
`${label} declares a compound state; the shared runtime supports only flat single-region FSMs`,
|
|
3299
|
-
);
|
|
3300
|
-
}
|
|
3906
|
+
// DR-067 §1: a flat machine, or one whose only compound states are root
|
|
3907
|
+
// parallel states of the compiled shape; everything else is rejected.
|
|
3908
|
+
const parallelProfile = parallelProfileFromMachine(machine, label);
|
|
3301
3909
|
assertFlatStateIdentity(machine, label);
|
|
3302
3910
|
assertUnfinishedFinalStateIds(
|
|
3303
3911
|
spec.unfinishedFinalStateIds,
|
|
@@ -3338,6 +3946,18 @@ export function createXStatePlaybookRuntime<
|
|
|
3338
3946
|
const declaredRoleIds = Object.freeze([
|
|
3339
3947
|
...new Set([...roleStates.values()].map(({ role }) => role)),
|
|
3340
3948
|
]);
|
|
3949
|
+
// DR-067 §1: the Boss-relevant states of a parallel-declaring machine —
|
|
3950
|
+
// each player state, each wait state, and the failure state. A status is
|
|
3951
|
+
// scheduled for each one newly entered, and a status trace names a
|
|
3952
|
+
// `stateId` only while exactly one is active.
|
|
3953
|
+
const bossRelevantStateIds: ReadonlySet<string> | undefined =
|
|
3954
|
+
parallelProfile === undefined
|
|
3955
|
+
? undefined
|
|
3956
|
+
: new Set([
|
|
3957
|
+
...roleStates.keys(),
|
|
3958
|
+
...parallelProfile.waitResumeTargets.keys(),
|
|
3959
|
+
'failed',
|
|
3960
|
+
]);
|
|
3341
3961
|
// PBRT-52: the artifact's own ControlView context projection. Nothing is
|
|
3342
3962
|
// exported by default, so an FSM context member — including one added
|
|
3343
3963
|
// after this artifact was linked — is private until named here. The two
|
|
@@ -3402,6 +4022,7 @@ export function createXStatePlaybookRuntime<
|
|
|
3402
4022
|
machine,
|
|
3403
4023
|
spec.entryEvent,
|
|
3404
4024
|
spec.bossEvents ?? [],
|
|
4025
|
+
parallelProfile,
|
|
3405
4026
|
);
|
|
3406
4027
|
const classifyBossText: NonNullable<
|
|
3407
4028
|
XStatePlaybookRuntimeSpec<TOptions>['classifyBossText']
|
|
@@ -3425,8 +4046,126 @@ export function createXStatePlaybookRuntime<
|
|
|
3425
4046
|
return typeof cwd === 'string' ? cwd : undefined;
|
|
3426
4047
|
});
|
|
3427
4048
|
|
|
3428
|
-
|
|
3429
|
-
|
|
4049
|
+
// DR-067 §1: the questions pending in a state — the singular reply-wait
|
|
4050
|
+
// question of a flat machine, or a parallel machine's keyed questions
|
|
4051
|
+
// whose own wait state is active.
|
|
4052
|
+
const pendingBossQuestionsFor = (
|
|
4053
|
+
state: PlaybookState,
|
|
4054
|
+
context: Record<string, unknown>,
|
|
4055
|
+
): readonly PlaybookPendingBossQuestionContext[] => {
|
|
4056
|
+
if (parallelProfile === undefined) {
|
|
4057
|
+
const pending = pendingBossQuestionForState(state, context);
|
|
4058
|
+
return pending === undefined ? [] : [pending];
|
|
4059
|
+
}
|
|
4060
|
+
return parallelPendingBossQuestions(
|
|
4061
|
+
parallelProfile,
|
|
4062
|
+
state.activeStateIds,
|
|
4063
|
+
context,
|
|
4064
|
+
);
|
|
4065
|
+
};
|
|
4066
|
+
const singlePendingBossQuestion = (
|
|
4067
|
+
state: PlaybookState,
|
|
4068
|
+
context: Record<string, unknown>,
|
|
4069
|
+
): PlaybookPendingBossQuestionContext | undefined => {
|
|
4070
|
+
const pending = pendingBossQuestionsFor(state, context);
|
|
4071
|
+
return pending.length === 1 ? pending[0] : undefined;
|
|
4072
|
+
};
|
|
4073
|
+
// DR-067 §1: a status trace names a `stateId` only while exactly one
|
|
4074
|
+
// Boss-relevant state is active; a flat machine's is its one state.
|
|
4075
|
+
const statusTraceStateId = (state: PlaybookState): string | undefined => {
|
|
4076
|
+
if (bossRelevantStateIds === undefined) return state.stateId;
|
|
4077
|
+
const relevant = state.activeStateIds.filter((stateId) =>
|
|
4078
|
+
bossRelevantStateIds.has(stateId),
|
|
4079
|
+
);
|
|
4080
|
+
return relevant.length === 1 ? relevant[0] : undefined;
|
|
4081
|
+
};
|
|
4082
|
+
// DR-067 §1: a parallel machine schedules statuses for each newly entered
|
|
4083
|
+
// Boss-relevant state — a player state's role line, a wait state's
|
|
4084
|
+
// question lines, and the failure line — diffed against the previous
|
|
4085
|
+
// snapshot's active state ids, so a sibling still running is not
|
|
4086
|
+
// announced again.
|
|
4087
|
+
const parallelStatusesFor = (
|
|
4088
|
+
previousState: PlaybookState | undefined,
|
|
4089
|
+
state: PlaybookState,
|
|
4090
|
+
context: Record<string, unknown>,
|
|
4091
|
+
): ScheduledStatus[] => {
|
|
4092
|
+
const prior = new Set(previousState?.activeStateIds ?? []);
|
|
4093
|
+
const statuses: ScheduledStatus[] = [];
|
|
4094
|
+
for (const stateId of state.activeStateIds) {
|
|
4095
|
+
if (prior.has(stateId) || !bossRelevantStateIds!.has(stateId)) continue;
|
|
4096
|
+
const resumeTargets = parallelProfile!.waitResumeTargets.get(stateId);
|
|
4097
|
+
if (resumeTargets !== undefined) {
|
|
4098
|
+
const questions = keyedPendingBossQuestions(context, (resumeStateId) =>
|
|
4099
|
+
resumeTargets.has(resumeStateId),
|
|
4100
|
+
);
|
|
4101
|
+
if (questions.length === 0) {
|
|
4102
|
+
statuses.push({ message: 'Awaiting Boss reply.' });
|
|
4103
|
+
continue;
|
|
4104
|
+
}
|
|
4105
|
+
for (const pending of questions) {
|
|
4106
|
+
statuses.push(
|
|
4107
|
+
{
|
|
4108
|
+
message: `${askerLabel(pending.asker)} asks: ${pending.question}`,
|
|
4109
|
+
},
|
|
4110
|
+
{
|
|
4111
|
+
message:
|
|
4112
|
+
`◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
4113
|
+
`${askerLabel(pending.asker)} · ${pending.sourceItem}`,
|
|
4114
|
+
},
|
|
4115
|
+
);
|
|
4116
|
+
}
|
|
4117
|
+
continue;
|
|
4118
|
+
}
|
|
4119
|
+
if (stateId === 'failed') {
|
|
4120
|
+
const lastError = normalizeErrorCompact(context.lastError);
|
|
4121
|
+
statuses.push({
|
|
4122
|
+
message: '◆ workflow failed; awaiting Boss recovery.',
|
|
4123
|
+
...(lastError === undefined
|
|
4124
|
+
? {}
|
|
4125
|
+
: {
|
|
4126
|
+
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
4127
|
+
}),
|
|
4128
|
+
});
|
|
4129
|
+
continue;
|
|
4130
|
+
}
|
|
4131
|
+
const roleState = roleStates.get(stateId);
|
|
4132
|
+
if (roleState !== undefined) {
|
|
4133
|
+
statuses.push({ message: `⤷ ${roleState.role}: ${roleState.label}` });
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
return statuses;
|
|
4137
|
+
};
|
|
4138
|
+
// DR-067 §1: each parallel state's region roles, in region order, are one
|
|
4139
|
+
// concurrent role set the FSM exports; a host that forwards those sets in
|
|
4140
|
+
// its schema-3 authority is held to them before any agent call.
|
|
4141
|
+
const assertDeclaredCohorts = (hostCapabilities: object): void => {
|
|
4142
|
+
const authority = Object.getOwnPropertyDescriptor(
|
|
4143
|
+
hostCapabilities,
|
|
4144
|
+
'authority',
|
|
4145
|
+
)?.value;
|
|
4146
|
+
if (!isPlainObject(authority) || !Array.isArray(authority.concurrentRoleSets)) {
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
const declared = authority.concurrentRoleSets as readonly unknown[];
|
|
4150
|
+
for (const parallelState of parallelProfile!.states) {
|
|
4151
|
+
const roles = parallelState.regions.map(({ roleId }) => roleId);
|
|
4152
|
+
if (
|
|
4153
|
+
!declared.some(
|
|
4154
|
+
(candidate) =>
|
|
4155
|
+
Array.isArray(candidate) &&
|
|
4156
|
+
candidate.length === roles.length &&
|
|
4157
|
+
candidate.every((roleId, index) => roleId === roles[index]),
|
|
4158
|
+
)
|
|
4159
|
+
) {
|
|
4160
|
+
throw new TypeError(
|
|
4161
|
+
`${label} parallel state ${parallelState.stateId} roles [${roles.join(', ')}] are not a declared concurrent role set`,
|
|
4162
|
+
);
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
|
|
4167
|
+
const createPlaybookRuntime = function createPlaybookRuntime(
|
|
4168
|
+
factoryOptions: unknown,
|
|
3430
4169
|
): PlaybookRuntime {
|
|
3431
4170
|
const construction = configuredOptionsFromFactoryInput(
|
|
3432
4171
|
factoryOptions,
|
|
@@ -3459,8 +4198,12 @@ export function createXStatePlaybookRuntime<
|
|
|
3459
4198
|
? repositoryCapabilityFromHostCapabilities(
|
|
3460
4199
|
construction.hostCapabilities,
|
|
3461
4200
|
label,
|
|
4201
|
+
parallelProfile !== undefined,
|
|
3462
4202
|
)
|
|
3463
4203
|
: undefined;
|
|
4204
|
+
if (parallelProfile !== undefined) {
|
|
4205
|
+
assertDeclaredCohorts(construction.hostCapabilities);
|
|
4206
|
+
}
|
|
3464
4207
|
const currentEffectLedger = (): PlaybookEffectLedger =>
|
|
3465
4208
|
assertPlaybookEffectLedger(
|
|
3466
4209
|
effectLedgerCapability.snapshot(),
|
|
@@ -3531,9 +4274,11 @@ export function createXStatePlaybookRuntime<
|
|
|
3531
4274
|
// mutable `activeSignal` alone cannot classify a late invocation reason.
|
|
3532
4275
|
let activeAborts: AbortReasonClassifier | undefined;
|
|
3533
4276
|
// The bridge binds the provenance of a child result immediately before
|
|
3534
|
-
// its promise actor settles. The next root snapshot/error consumes
|
|
3535
|
-
//
|
|
3536
|
-
|
|
4277
|
+
// its promise actor settles. The next root snapshot/error consumes it so
|
|
4278
|
+
// background settlement emissions retain their owner. A flat machine has
|
|
4279
|
+
// one settling actor at a time, so a later binding replaces an unconsumed
|
|
4280
|
+
// one; a parallel machine's settlements queue in order (DR-067 §1).
|
|
4281
|
+
const actorSettlementAborts: (AbortReasonClassifier | undefined)[] = [];
|
|
3537
4282
|
let actorSettlementErrorAborts: AbortReasonClassifier | undefined;
|
|
3538
4283
|
// Exact cancellation observed by an emission owned by the active
|
|
3539
4284
|
// boundary. Ordinary runs settle from their signal/state; apply also
|
|
@@ -3577,15 +4322,35 @@ export function createXStatePlaybookRuntime<
|
|
|
3577
4322
|
let deferInspectionEmissions = false;
|
|
3578
4323
|
let deferredInspectionEmissions: Array<() => void> = [];
|
|
3579
4324
|
let controlPlaneError: unknown;
|
|
3580
|
-
// DR-063 §2: the cause decided
|
|
3581
|
-
//
|
|
3582
|
-
//
|
|
3583
|
-
//
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
4325
|
+
// DR-063 §2: the cause decided for a non-`ok` player result before the
|
|
4326
|
+
// compiled artifact builds the Error that carries it and hands it to
|
|
4327
|
+
// `markPlayerResultFailure`. It is cleared at each Boss turn start and at
|
|
4328
|
+
// each successful result, so it is never stale.
|
|
4329
|
+
let pendingResultFailureCause: PlaybookFailureCause | undefined;
|
|
4330
|
+
// DR-063 §2: the causes decided for thrown values that cannot carry the
|
|
4331
|
+
// marker — a string, a frozen error — and for the record a machine keeps
|
|
4332
|
+
// in place of the thrown value, kept by identity for as long as the
|
|
4333
|
+
// failure stands, so every surface that reads the FSM's own `lastError`
|
|
4334
|
+
// publishes the cause decided for exactly that failure, whatever turn
|
|
4335
|
+
// reads it.
|
|
4336
|
+
const failureCauses = createFailureCauseRetention();
|
|
4337
|
+
// DR-063 §2 / DR-067 §1: the cause decided for each non-`ok` player
|
|
4338
|
+
// result, keyed by the result itself, so concurrent region calls never
|
|
4339
|
+
// lend one another the single slot above.
|
|
4340
|
+
const resultFailureCauses = new WeakMap<object, PlaybookFailureCause>();
|
|
4341
|
+
// DR-067 §1: the parallel profile's cohort bookkeeping. Every public
|
|
4342
|
+
// boundary advances the sequence; a parallel state's working leaves run
|
|
4343
|
+
// as one cohort at most once per boundary, and each cohort's semantic
|
|
4344
|
+
// completions — which the host settles concurrently — reconcile one at a
|
|
4345
|
+
// time so their durable correction-budget writes cannot race.
|
|
4346
|
+
let publicBoundarySequence = 0;
|
|
4347
|
+
const consumedCohortBoundaries = new Map<string, number>();
|
|
4348
|
+
const pendingCohorts = new Map<string, PendingCohort>();
|
|
4349
|
+
const cohortCompletionQueue = new PQueue({ concurrency: 1 });
|
|
4350
|
+
// DR-067 §1: every player, judge, and nested call a parallel machine
|
|
4351
|
+
// starts, so a boundary's drain waits for a cancelled sibling whose port
|
|
4352
|
+
// ignores its signal before it settles.
|
|
4353
|
+
const activeBoundaryCalls = new Set<Promise<unknown>>();
|
|
3589
4354
|
// Previous root-machine state for the inspect-driven telemetry /
|
|
3590
4355
|
// status emitter. undefined before the first inspect firing.
|
|
3591
4356
|
let priorState: PlaybookState | undefined;
|
|
@@ -4654,7 +5419,7 @@ export function createXStatePlaybookRuntime<
|
|
|
4654
5419
|
...(descriptor !== undefined
|
|
4655
5420
|
? {
|
|
4656
5421
|
state: descriptor,
|
|
4657
|
-
...stateIdentity(descriptor
|
|
5422
|
+
...stateIdentity(statusTraceStateId(descriptor)),
|
|
4658
5423
|
}
|
|
4659
5424
|
: {}),
|
|
4660
5425
|
},
|
|
@@ -4705,9 +5470,12 @@ export function createXStatePlaybookRuntime<
|
|
|
4705
5470
|
// start-only fields, so it passes its own canonical pre-acceptance
|
|
4706
5471
|
// base (slc/link.md §Playbook trace).
|
|
4707
5472
|
finishIdentity: Record<string, unknown> = identity,
|
|
5473
|
+
// DR-067 §1: the per-call abort classification a parallel machine's
|
|
5474
|
+
// player pairs enqueue under; absent, the active boundary's.
|
|
5475
|
+
aborts?: AbortReasonClassifier,
|
|
4708
5476
|
): Promise<void> {
|
|
4709
5477
|
try {
|
|
4710
|
-
await emitTrace(startedType, identity, position);
|
|
5478
|
+
await emitTrace(startedType, identity, position, aborts);
|
|
4711
5479
|
} catch (error) {
|
|
4712
5480
|
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
4713
5481
|
try {
|
|
@@ -4719,6 +5487,7 @@ export function createXStatePlaybookRuntime<
|
|
|
4719
5487
|
error: normalizeError(error),
|
|
4720
5488
|
},
|
|
4721
5489
|
position,
|
|
5490
|
+
aborts,
|
|
4722
5491
|
);
|
|
4723
5492
|
} catch {
|
|
4724
5493
|
// Preserve the start failure after one best-effort finish attempt.
|
|
@@ -4865,30 +5634,6 @@ export function createXStatePlaybookRuntime<
|
|
|
4865
5634
|
});
|
|
4866
5635
|
}
|
|
4867
5636
|
|
|
4868
|
-
// DR-063 §1: the cause of an unresolved governed settlement. A reconciled
|
|
4869
|
-
// mismatch supplies its own receipt-read cause; the reasons this function
|
|
4870
|
-
// owns map to adjudication, abort, and runtime-defect codes.
|
|
4871
|
-
function governedSettlementCause(
|
|
4872
|
-
reason: string,
|
|
4873
|
-
error: unknown,
|
|
4874
|
-
aborted: boolean,
|
|
4875
|
-
supplied: PlaybookFailureCause | undefined,
|
|
4876
|
-
): PlaybookFailureCause {
|
|
4877
|
-
if (supplied !== undefined) return supplied;
|
|
4878
|
-
if (aborted || reason.includes('aborted')) return ABORTED_FAILURE_CAUSE;
|
|
4879
|
-
if (GOVERNED_JUDGE_FAILURE_REASONS.includes(reason)) {
|
|
4880
|
-
const transport = failureErrorEvidence(error);
|
|
4881
|
-
return assertPlaybookFailureCause({
|
|
4882
|
-
code: 'judge-failed',
|
|
4883
|
-
evidence: {
|
|
4884
|
-
reason,
|
|
4885
|
-
...(transport === undefined ? {} : { error: transport }),
|
|
4886
|
-
},
|
|
4887
|
-
});
|
|
4888
|
-
}
|
|
4889
|
-
return runtimeDefectCause(reason);
|
|
4890
|
-
}
|
|
4891
|
-
|
|
4892
5637
|
function unresolvedGovernedSettlement(
|
|
4893
5638
|
reason: string,
|
|
4894
5639
|
error?: unknown,
|
|
@@ -4900,8 +5645,8 @@ export function createXStatePlaybookRuntime<
|
|
|
4900
5645
|
? Object.is(error, signal.reason)
|
|
4901
5646
|
: false;
|
|
4902
5647
|
const decided = governedSettlementCause(reason, error, aborted, cause);
|
|
4903
|
-
rememberFailureCause(decided);
|
|
4904
5648
|
if (aborted) {
|
|
5649
|
+
failureCauses.retain(error, decided);
|
|
4905
5650
|
return {
|
|
4906
5651
|
status: 'unresolved',
|
|
4907
5652
|
error: attachPlaybookFailureCause(error, decided),
|
|
@@ -4911,6 +5656,7 @@ export function createXStatePlaybookRuntime<
|
|
|
4911
5656
|
error instanceof Error
|
|
4912
5657
|
? error
|
|
4913
5658
|
: new Error(`${label} governed outcome remains unresolved: ${reason}`);
|
|
5659
|
+
failureCauses.retain(failure, decided);
|
|
4914
5660
|
return {
|
|
4915
5661
|
status: 'unresolved',
|
|
4916
5662
|
error: attachPlaybookFailureCause(
|
|
@@ -4971,12 +5717,46 @@ export function createXStatePlaybookRuntime<
|
|
|
4971
5717
|
...(semanticCandidate === undefined ? {} : { semanticCandidate }),
|
|
4972
5718
|
correctionBudget: { limit: 1, spent: true },
|
|
4973
5719
|
};
|
|
5720
|
+
// DR-067 §1 / PBRT-69: a cohort's shared receipt becomes visible for
|
|
5721
|
+
// every member in one ledger revision — publishing only this member's
|
|
5722
|
+
// spend would leave the invalid half-complete cohort the ledger admits
|
|
5723
|
+
// nowhere.
|
|
5724
|
+
const cohortMembers =
|
|
5725
|
+
current.cohortId === undefined
|
|
5726
|
+
? [current]
|
|
5727
|
+
: currentLedger.boundaries.filter(
|
|
5728
|
+
({ cohortId }) => cohortId === current.cohortId,
|
|
5729
|
+
);
|
|
5730
|
+
const replacements = cohortMembers.map((member) => {
|
|
5731
|
+
if (member.boundaryId === current.boundaryId) {
|
|
5732
|
+
return { expected: current, next };
|
|
5733
|
+
}
|
|
5734
|
+
if (
|
|
5735
|
+
member.physicalReceipt !== undefined &&
|
|
5736
|
+
!isDeepStrictEqual(member.physicalReceipt, receipt)
|
|
5737
|
+
) {
|
|
5738
|
+
throw new TypeError(
|
|
5739
|
+
`${label} correction budget cohort conflicts with its shared repository receipt`,
|
|
5740
|
+
);
|
|
5741
|
+
}
|
|
5742
|
+
return {
|
|
5743
|
+
expected: member,
|
|
5744
|
+
next: {
|
|
5745
|
+
...member,
|
|
5746
|
+
...(receipt.after === undefined ? {} : { after: receipt.after }),
|
|
5747
|
+
physicalReceipt: receipt,
|
|
5748
|
+
},
|
|
5749
|
+
};
|
|
5750
|
+
}) as [
|
|
5751
|
+
{ readonly expected: PlaybookEffectBoundary; readonly next: PlaybookEffectBoundary },
|
|
5752
|
+
...{
|
|
5753
|
+
readonly expected: PlaybookEffectBoundary;
|
|
5754
|
+
readonly next: PlaybookEffectBoundary;
|
|
5755
|
+
}[],
|
|
5756
|
+
];
|
|
4974
5757
|
const acknowledged = assertPlaybookEffectLedger(
|
|
4975
5758
|
await effectLedgerCapability.writeAhead([
|
|
4976
|
-
{
|
|
4977
|
-
kind: 'replace-boundaries',
|
|
4978
|
-
replacements: [{ expected: current, next }],
|
|
4979
|
-
},
|
|
5759
|
+
{ kind: 'replace-boundaries', replacements },
|
|
4980
5760
|
]),
|
|
4981
5761
|
`${label} semantic correction budget acknowledgement`,
|
|
4982
5762
|
);
|
|
@@ -5362,7 +6142,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5362
6142
|
function acknowledgeGovernedPlayerResult(
|
|
5363
6143
|
value: unknown,
|
|
5364
6144
|
boundaryId: string,
|
|
5365
|
-
source: 'runExclusive' | 'runDeferred' = 'runExclusive',
|
|
6145
|
+
source: 'runExclusive' | 'runDeferred' | 'runCohort' = 'runExclusive',
|
|
5366
6146
|
): PlayerResult {
|
|
5367
6147
|
if (!isPlainObject(value) || !isPlainObject(value.operation)) {
|
|
5368
6148
|
throw new TypeError(
|
|
@@ -5473,7 +6253,7 @@ export function createXStatePlaybookRuntime<
|
|
|
5473
6253
|
);
|
|
5474
6254
|
governedOutput = undefined;
|
|
5475
6255
|
}
|
|
5476
|
-
} else if (source
|
|
6256
|
+
} else if (source !== 'runDeferred' && value.deferredStatus !== undefined) {
|
|
5477
6257
|
throw new TypeError(
|
|
5478
6258
|
`${label} non-deferred settlement returned a deferred binding status`,
|
|
5479
6259
|
);
|
|
@@ -5633,6 +6413,371 @@ export function createXStatePlaybookRuntime<
|
|
|
5633
6413
|
failedGovernedAttemptId = attemptIds.values().next().value;
|
|
5634
6414
|
}
|
|
5635
6415
|
|
|
6416
|
+
// -----------------------------------------------------------------
|
|
6417
|
+
// DR-067 §1: the parallel profile's repository cohort. The working
|
|
6418
|
+
// leaves of one parallel state entered together run under one
|
|
6419
|
+
// `runCohort` claim, each member an `unchanged` boundary: a member's
|
|
6420
|
+
// result is adjudicated through the one reconciler in serial, a spent
|
|
6421
|
+
// correction rewrites every member's boundary in one acknowledged batch,
|
|
6422
|
+
// members are acknowledged and released in completion order, and a
|
|
6423
|
+
// member's failure cancels its siblings and fails the cohort with the
|
|
6424
|
+
// cause decided first.
|
|
6425
|
+
// -----------------------------------------------------------------
|
|
6426
|
+
|
|
6427
|
+
function trackBoundaryCall<T>(call: Promise<T>): Promise<T> {
|
|
6428
|
+
if (parallelProfile === undefined) return call;
|
|
6429
|
+
activeBoundaryCalls.add(call);
|
|
6430
|
+
const forget = (): void => {
|
|
6431
|
+
activeBoundaryCalls.delete(call);
|
|
6432
|
+
};
|
|
6433
|
+
void call.then(forget, forget);
|
|
6434
|
+
return call;
|
|
6435
|
+
}
|
|
6436
|
+
|
|
6437
|
+
async function drainBoundaryCalls(): Promise<void> {
|
|
6438
|
+
while (activeBoundaryCalls.size > 0) {
|
|
6439
|
+
await Promise.allSettled([...activeBoundaryCalls]);
|
|
6440
|
+
}
|
|
6441
|
+
}
|
|
6442
|
+
|
|
6443
|
+
function cohortParallelStateFor(
|
|
6444
|
+
stateId: string,
|
|
6445
|
+
): XStateParallelState | undefined {
|
|
6446
|
+
const parallelState =
|
|
6447
|
+
parallelProfile?.parallelStateByWorkingStateId.get(stateId);
|
|
6448
|
+
if (parallelState === undefined) return undefined;
|
|
6449
|
+
if (
|
|
6450
|
+
consumedCohortBoundaries.get(parallelState.stateId) ===
|
|
6451
|
+
publicBoundarySequence
|
|
6452
|
+
) {
|
|
6453
|
+
return undefined;
|
|
6454
|
+
}
|
|
6455
|
+
const pending = pendingCohorts.get(parallelState.stateId);
|
|
6456
|
+
if (
|
|
6457
|
+
pending !== undefined &&
|
|
6458
|
+
pending.boundarySequence === publicBoundarySequence &&
|
|
6459
|
+
!pending.started
|
|
6460
|
+
) {
|
|
6461
|
+
return parallelState;
|
|
6462
|
+
}
|
|
6463
|
+
const active = currentState().activeStateIds;
|
|
6464
|
+
return parallelState.regions.every(({ workingStateId }) =>
|
|
6465
|
+
active.includes(workingStateId),
|
|
6466
|
+
)
|
|
6467
|
+
? parallelState
|
|
6468
|
+
: undefined;
|
|
6469
|
+
}
|
|
6470
|
+
|
|
6471
|
+
function startCohort(
|
|
6472
|
+
parallelState: XStateParallelState,
|
|
6473
|
+
cohort: PendingCohort,
|
|
6474
|
+
): void {
|
|
6475
|
+
cohort.started = true;
|
|
6476
|
+
consumedCohortBoundaries.set(
|
|
6477
|
+
parallelState.stateId,
|
|
6478
|
+
cohort.boundarySequence,
|
|
6479
|
+
);
|
|
6480
|
+
if (pendingCohorts.get(parallelState.stateId) === cohort) {
|
|
6481
|
+
pendingCohorts.delete(parallelState.stateId);
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
async function joinCohort(
|
|
6486
|
+
parallelState: XStateParallelState,
|
|
6487
|
+
member: XStateCohortMember,
|
|
6488
|
+
): Promise<PlayerResult> {
|
|
6489
|
+
let cohort = pendingCohorts.get(parallelState.stateId);
|
|
6490
|
+
if (
|
|
6491
|
+
cohort === undefined ||
|
|
6492
|
+
cohort.boundarySequence !== publicBoundarySequence ||
|
|
6493
|
+
cohort.started
|
|
6494
|
+
) {
|
|
6495
|
+
cohort = {
|
|
6496
|
+
boundarySequence: publicBoundarySequence,
|
|
6497
|
+
members: new Map(),
|
|
6498
|
+
started: false,
|
|
6499
|
+
};
|
|
6500
|
+
pendingCohorts.set(parallelState.stateId, cohort);
|
|
6501
|
+
}
|
|
6502
|
+
const registered = cohort;
|
|
6503
|
+
if (registered.members.has(member.region.key)) {
|
|
6504
|
+
throw new Error(
|
|
6505
|
+
`${label} parallel state ${parallelState.stateId} registered region ${member.region.key} twice`,
|
|
6506
|
+
);
|
|
6507
|
+
}
|
|
6508
|
+
registered.members.set(member.region.key, member);
|
|
6509
|
+
// A member whose invocation stops before every sibling registered —
|
|
6510
|
+
// its region exited, or the boundary aborted — leaves no cohort to
|
|
6511
|
+
// run: each registered member settles with its own cancellation and
|
|
6512
|
+
// the boundary's cohort is spent.
|
|
6513
|
+
const withdraw = (): void => {
|
|
6514
|
+
if (registered.started) return;
|
|
6515
|
+
startCohort(parallelState, registered);
|
|
6516
|
+
for (const candidate of registered.members.values()) {
|
|
6517
|
+
candidate.settled.reject(
|
|
6518
|
+
candidate.signal.aborted
|
|
6519
|
+
? candidate.signal.reason
|
|
6520
|
+
: member.signal.reason,
|
|
6521
|
+
);
|
|
6522
|
+
}
|
|
6523
|
+
};
|
|
6524
|
+
if (member.signal.aborted) {
|
|
6525
|
+
withdraw();
|
|
6526
|
+
} else {
|
|
6527
|
+
member.signal.addEventListener('abort', withdraw, { once: true });
|
|
6528
|
+
}
|
|
6529
|
+
if (
|
|
6530
|
+
!registered.started &&
|
|
6531
|
+
registered.members.size === parallelState.regions.length
|
|
6532
|
+
) {
|
|
6533
|
+
startCohort(parallelState, registered);
|
|
6534
|
+
void settleCohort(parallelState, registered);
|
|
6535
|
+
}
|
|
6536
|
+
return await member.settled.promise;
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6539
|
+
async function settleCohort(
|
|
6540
|
+
parallelState: XStateParallelState,
|
|
6541
|
+
cohort: PendingCohort,
|
|
6542
|
+
): Promise<void> {
|
|
6543
|
+
const members = parallelState.regions.map(
|
|
6544
|
+
({ key }) => cohort.members.get(key)!,
|
|
6545
|
+
);
|
|
6546
|
+
const cancellation = new AbortController();
|
|
6547
|
+
const completionOrder: XStateCohortMember[] = [];
|
|
6548
|
+
const decided = new Map<
|
|
6549
|
+
XStateCohortMember,
|
|
6550
|
+
{
|
|
6551
|
+
readonly error: unknown;
|
|
6552
|
+
readonly cause: PlaybookFailureCause | undefined;
|
|
6553
|
+
readonly sequence: number;
|
|
6554
|
+
}
|
|
6555
|
+
>();
|
|
6556
|
+
let decisionSequence = 0;
|
|
6557
|
+
// DR-063 §2: each member failure is one decision, ordered as it was
|
|
6558
|
+
// decided, with the cause decided for exactly that value at that
|
|
6559
|
+
// moment — so a value a sibling later rethrows cannot rename it.
|
|
6560
|
+
const decide = (
|
|
6561
|
+
member: XStateCohortMember,
|
|
6562
|
+
error: unknown,
|
|
6563
|
+
cause: PlaybookFailureCause | undefined = failureCauses.causeOf(error),
|
|
6564
|
+
): void => {
|
|
6565
|
+
if (decided.has(member)) return;
|
|
6566
|
+
decided.set(member, { error, cause, sequence: ++decisionSequence });
|
|
6567
|
+
};
|
|
6568
|
+
const isCancellation = (error: unknown): boolean =>
|
|
6569
|
+
cancellation.signal.aborted &&
|
|
6570
|
+
Object.is(error, cancellation.signal.reason) &&
|
|
6571
|
+
!activeAborts?.isAbortReason(error);
|
|
6572
|
+
const operationFor =
|
|
6573
|
+
(member: XStateCohortMember) =>
|
|
6574
|
+
async (
|
|
6575
|
+
context:
|
|
6576
|
+
| { readonly baseline?: PlaybookRepositoryReceipt['baseline'] }
|
|
6577
|
+
| undefined,
|
|
6578
|
+
): Promise<PlayerResult> => {
|
|
6579
|
+
const callSignal = combineAbortSignals(
|
|
6580
|
+
member.signal,
|
|
6581
|
+
cancellation.signal,
|
|
6582
|
+
);
|
|
6583
|
+
try {
|
|
6584
|
+
if (activePlayerKeys.has(member.playerKey)) {
|
|
6585
|
+
await member.reportCollision(callSignal);
|
|
6586
|
+
}
|
|
6587
|
+
activePlayerKeys.add(member.playerKey);
|
|
6588
|
+
try {
|
|
6589
|
+
const result = await member.run(
|
|
6590
|
+
callSignal,
|
|
6591
|
+
cancellation.signal,
|
|
6592
|
+
effectAuthorizedPreExistingBlock(
|
|
6593
|
+
member.effectBoundary,
|
|
6594
|
+
context?.baseline,
|
|
6595
|
+
),
|
|
6596
|
+
);
|
|
6597
|
+
if (result.status !== 'ok' && !cancellation.signal.aborted) {
|
|
6598
|
+
// DR-063 §2: the failure built for a non-`ok` member result
|
|
6599
|
+
// carries the cause decided for exactly that result, and its
|
|
6600
|
+
// siblings are cancelled with it.
|
|
6601
|
+
const cause =
|
|
6602
|
+
resultFailureCauses.get(result) ??
|
|
6603
|
+
runtimeDefectCause(
|
|
6604
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
6605
|
+
);
|
|
6606
|
+
const failure = attachPlaybookFailureCause(
|
|
6607
|
+
markFsmResultFailure(
|
|
6608
|
+
new Error(
|
|
6609
|
+
result.error ??
|
|
6610
|
+
`captainBridge: callPlayer status "${result.status}"`,
|
|
6611
|
+
),
|
|
6612
|
+
),
|
|
6613
|
+
cause,
|
|
6614
|
+
);
|
|
6615
|
+
failureCauses.retain(failure, cause);
|
|
6616
|
+
decide(member, failure, cause);
|
|
6617
|
+
cancellation.abort(failure);
|
|
6618
|
+
}
|
|
6619
|
+
return result;
|
|
6620
|
+
} finally {
|
|
6621
|
+
activePlayerKeys.delete(member.playerKey);
|
|
6622
|
+
}
|
|
6623
|
+
} catch (error) {
|
|
6624
|
+
if (!isCancellation(error)) decide(member, error);
|
|
6625
|
+
if (!cancellation.signal.aborted) cancellation.abort(error);
|
|
6626
|
+
throw error;
|
|
6627
|
+
} finally {
|
|
6628
|
+
completionOrder.push(member);
|
|
6629
|
+
}
|
|
6630
|
+
};
|
|
6631
|
+
const completeEffectBoundary = (
|
|
6632
|
+
completion: XStateRepositoryCohortCompletion<PlayerResult>,
|
|
6633
|
+
): Promise<XStateRepositoryCompletionEvidence> =>
|
|
6634
|
+
cohortCompletionQueue.add(async () => {
|
|
6635
|
+
const member = members.find(
|
|
6636
|
+
({ roleId }) => roleId === completion.roleId,
|
|
6637
|
+
);
|
|
6638
|
+
if (
|
|
6639
|
+
member === undefined ||
|
|
6640
|
+
completion.boundary.boundaryId !== member.effectBoundary.boundaryId
|
|
6641
|
+
) {
|
|
6642
|
+
throw new TypeError(
|
|
6643
|
+
`${label} repository cohort completed an undeclared member`,
|
|
6644
|
+
);
|
|
6645
|
+
}
|
|
6646
|
+
const evidence = await completionEvidenceFor(
|
|
6647
|
+
member.input,
|
|
6648
|
+
member.roleId,
|
|
6649
|
+
member.playerId,
|
|
6650
|
+
member.signal,
|
|
6651
|
+
undefined,
|
|
6652
|
+
)(completion);
|
|
6653
|
+
const settlement = governedSettlementsByBoundaryId.get(
|
|
6654
|
+
member.effectBoundary.boundaryId,
|
|
6655
|
+
);
|
|
6656
|
+
if (settlement?.status === 'unresolved') {
|
|
6657
|
+
decide(member, settlement.error);
|
|
6658
|
+
}
|
|
6659
|
+
return evidence;
|
|
6660
|
+
}) as Promise<XStateRepositoryCompletionEvidence>;
|
|
6661
|
+
const runSignal = activeSignal ?? members[0]!.signal;
|
|
6662
|
+
const invocationId = randomUUID();
|
|
6663
|
+
let settled: XStateRepositoryCohortResult<PlayerResult>;
|
|
6664
|
+
try {
|
|
6665
|
+
for (const member of members) member.signal.throwIfAborted();
|
|
6666
|
+
settled = await repositoryCapability!.runCohort!<PlayerResult>({
|
|
6667
|
+
signal: runSignal,
|
|
6668
|
+
invocationId,
|
|
6669
|
+
roleIds: members.map(({ roleId }) => roleId),
|
|
6670
|
+
dispositionsByRole: Object.fromEntries(
|
|
6671
|
+
members.map(({ roleId }) => [roleId, ['unchanged'] as const]),
|
|
6672
|
+
),
|
|
6673
|
+
effectBoundaries: Object.fromEntries(
|
|
6674
|
+
members.map(({ roleId, effectBoundary }) => [
|
|
6675
|
+
roleId,
|
|
6676
|
+
effectBoundary,
|
|
6677
|
+
]),
|
|
6678
|
+
),
|
|
6679
|
+
operations: Object.fromEntries(
|
|
6680
|
+
members.map((member) => [member.roleId, operationFor(member)]),
|
|
6681
|
+
),
|
|
6682
|
+
completeEffectBoundary,
|
|
6683
|
+
});
|
|
6684
|
+
if (!isPlainObject(settled) || settled.invocationId !== invocationId) {
|
|
6685
|
+
throw new TypeError(
|
|
6686
|
+
`${label} repository cohort changed its invocation identity`,
|
|
6687
|
+
);
|
|
6688
|
+
}
|
|
6689
|
+
} catch (error) {
|
|
6690
|
+
for (const member of members) {
|
|
6691
|
+
governedSettlementsByBoundaryId.delete(
|
|
6692
|
+
member.effectBoundary.boundaryId,
|
|
6693
|
+
);
|
|
6694
|
+
governedCompletionEvidenceByBoundaryId.delete(
|
|
6695
|
+
member.effectBoundary.boundaryId,
|
|
6696
|
+
);
|
|
6697
|
+
refreshGovernedBoundaryStart(member.effectBoundary.boundaryId);
|
|
6698
|
+
}
|
|
6699
|
+
if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
|
|
6700
|
+
for (const member of members) member.settled.reject(error);
|
|
6701
|
+
return;
|
|
6702
|
+
}
|
|
6703
|
+
const releaseOrder = [
|
|
6704
|
+
...completionOrder,
|
|
6705
|
+
...members.filter((member) => !completionOrder.includes(member)),
|
|
6706
|
+
];
|
|
6707
|
+
const acknowledged = new Map<XStateCohortMember, PlayerResult>();
|
|
6708
|
+
const failures = new Map<XStateCohortMember, unknown>();
|
|
6709
|
+
for (const member of releaseOrder) {
|
|
6710
|
+
const operation = isPlainObject(settled.operations)
|
|
6711
|
+
? settled.operations[member.roleId]
|
|
6712
|
+
: undefined;
|
|
6713
|
+
try {
|
|
6714
|
+
const result = acknowledgeGovernedPlayerResult(
|
|
6715
|
+
{
|
|
6716
|
+
operation,
|
|
6717
|
+
receipt: isPlainObject(settled.receipts)
|
|
6718
|
+
? settled.receipts[member.roleId]
|
|
6719
|
+
: undefined,
|
|
6720
|
+
effectLedger: settled.effectLedger,
|
|
6721
|
+
},
|
|
6722
|
+
member.effectBoundary.boundaryId,
|
|
6723
|
+
'runCohort',
|
|
6724
|
+
);
|
|
6725
|
+
acknowledged.set(member, result);
|
|
6726
|
+
const settlement = governedPlayerSettlements.get(result);
|
|
6727
|
+
if (settlement?.status === 'unresolved') {
|
|
6728
|
+
failures.set(member, settlement.error);
|
|
6729
|
+
} else if (result.status !== 'ok') {
|
|
6730
|
+
failures.set(member, decided.get(member)?.error);
|
|
6731
|
+
}
|
|
6732
|
+
} catch (error) {
|
|
6733
|
+
failures.set(member, error);
|
|
6734
|
+
const operationReason =
|
|
6735
|
+
isPlainObject(operation) &&
|
|
6736
|
+
operation.status === 'rejected' &&
|
|
6737
|
+
Object.is(operation.reason, error);
|
|
6738
|
+
if (!operationReason) {
|
|
6739
|
+
// An acknowledgement the host could not prove is a control-plane
|
|
6740
|
+
// defect, exactly as on the exclusive path.
|
|
6741
|
+
if (!isAbortFailure(error, runSignal)) controlPlaneError ??= error;
|
|
6742
|
+
decide(member, error);
|
|
6743
|
+
}
|
|
6744
|
+
} finally {
|
|
6745
|
+
governedSettlementsByBoundaryId.delete(
|
|
6746
|
+
member.effectBoundary.boundaryId,
|
|
6747
|
+
);
|
|
6748
|
+
governedCompletionEvidenceByBoundaryId.delete(
|
|
6749
|
+
member.effectBoundary.boundaryId,
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6752
|
+
}
|
|
6753
|
+
if (failures.size === 0) {
|
|
6754
|
+
for (const member of releaseOrder) {
|
|
6755
|
+
member.settled.resolve(acknowledged.get(member)!);
|
|
6756
|
+
}
|
|
6757
|
+
return;
|
|
6758
|
+
}
|
|
6759
|
+
for (const result of acknowledged.values()) {
|
|
6760
|
+
governedPlayerSettlements.delete(result);
|
|
6761
|
+
}
|
|
6762
|
+
const first = [...decided.values()].sort(
|
|
6763
|
+
(left, right) => left.sequence - right.sequence,
|
|
6764
|
+
)[0];
|
|
6765
|
+
const failure =
|
|
6766
|
+
first?.error ??
|
|
6767
|
+
failures.get(releaseOrder.find((member) => failures.has(member))!) ??
|
|
6768
|
+
markFsmResultFailure(
|
|
6769
|
+
new Error(
|
|
6770
|
+
`${label} parallel state ${parallelState.stateId} cohort failed`,
|
|
6771
|
+
),
|
|
6772
|
+
);
|
|
6773
|
+
const cause = first?.cause ?? failureCauses.causeOf(failure);
|
|
6774
|
+
if (cause !== undefined) {
|
|
6775
|
+
failureCauses.retain(failure, cause);
|
|
6776
|
+
attachPlaybookFailureCause(failure, cause);
|
|
6777
|
+
}
|
|
6778
|
+
for (const member of members) member.settled.reject(failure);
|
|
6779
|
+
}
|
|
6780
|
+
|
|
5636
6781
|
const boundary: RuntimeBoundaryCalls = {
|
|
5637
6782
|
async callPlayer(
|
|
5638
6783
|
input,
|
|
@@ -5690,16 +6835,192 @@ export function createXStatePlaybookRuntime<
|
|
|
5690
6835
|
};
|
|
5691
6836
|
|
|
5692
6837
|
const playerKey = continuationKey(roleId, playerId);
|
|
5693
|
-
|
|
6838
|
+
const runTracedPlayerCall = async (
|
|
6839
|
+
resume: string | false = selectedResume,
|
|
6840
|
+
// DR-062 §4: composed from this call's own baseline observation,
|
|
6841
|
+
// so the prompt the trace records is the prompt the player is sent.
|
|
6842
|
+
preExistingBlock?: string,
|
|
6843
|
+
// DR-067 §1: a cohort member's call signal also carries its
|
|
6844
|
+
// cohort's sibling cancellation.
|
|
6845
|
+
callSignal: AbortSignal = signal,
|
|
6846
|
+
cohortCancellation?: AbortSignal,
|
|
6847
|
+
): Promise<PlayerResult> => {
|
|
6848
|
+
const identity = callIdentity(resume);
|
|
6849
|
+
// DR-067 §1: a parallel machine classifies aborts per call, so a
|
|
6850
|
+
// trace sink rejecting with a sibling's cancellation is forgiven
|
|
6851
|
+
// for that call alone.
|
|
6852
|
+
const traceAborts =
|
|
6853
|
+
parallelProfile === undefined
|
|
6854
|
+
? undefined
|
|
6855
|
+
: abortReasonClassifier(callSignal, activeAborts);
|
|
6856
|
+
const withPreExisting = (text: string): string =>
|
|
6857
|
+
preExistingBlock === undefined
|
|
6858
|
+
? text
|
|
6859
|
+
: `${text}\n\n${preExistingBlock}`;
|
|
6860
|
+
const callPrompt = withPreExisting(
|
|
6861
|
+
resume === false ? freshPrompt : prompt,
|
|
6862
|
+
);
|
|
6863
|
+
const callFreshPrompt = withPreExisting(freshPrompt);
|
|
6864
|
+
await emitCallStarted(
|
|
6865
|
+
'player.call.started',
|
|
6866
|
+
'player.call.finished',
|
|
6867
|
+
{ ...identity, prompt: callPrompt },
|
|
6868
|
+
position,
|
|
6869
|
+
callSignal,
|
|
6870
|
+
undefined,
|
|
6871
|
+
traceAborts,
|
|
6872
|
+
);
|
|
6873
|
+
|
|
6874
|
+
let rawResult: unknown;
|
|
6875
|
+
try {
|
|
6876
|
+
// An abort may land while the awaited started emission drains
|
|
6877
|
+
// (e.g. fired from the trace sink itself); the host call must
|
|
6878
|
+
// never start after abort, so settle the already-started pair
|
|
6879
|
+
// as `aborted` through the catch below.
|
|
6880
|
+
callSignal.throwIfAborted();
|
|
6881
|
+
rawResult = await requireHostPorts().callPlayer(
|
|
6882
|
+
roleId,
|
|
6883
|
+
callPrompt,
|
|
6884
|
+
callSignal,
|
|
6885
|
+
{
|
|
6886
|
+
resume,
|
|
6887
|
+
...(callPrompt === callFreshPrompt
|
|
6888
|
+
? {}
|
|
6889
|
+
: { freshPrompt: callFreshPrompt }),
|
|
6890
|
+
},
|
|
6891
|
+
);
|
|
6892
|
+
// A host promise is not required to honor cancellation. Do not
|
|
6893
|
+
// let a late result mutate continuity or publish a successful
|
|
6894
|
+
// finish.
|
|
6895
|
+
callSignal.throwIfAborted();
|
|
6896
|
+
} catch (error) {
|
|
6897
|
+
if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
|
|
6898
|
+
try {
|
|
6899
|
+
await emitTrace(
|
|
6900
|
+
'player.call.finished',
|
|
6901
|
+
{
|
|
6902
|
+
...identity,
|
|
6903
|
+
status: isAbortFailure(error, callSignal)
|
|
6904
|
+
? 'aborted'
|
|
6905
|
+
: 'error',
|
|
6906
|
+
error: normalizeError(error),
|
|
6907
|
+
},
|
|
6908
|
+
position,
|
|
6909
|
+
traceAborts,
|
|
6910
|
+
);
|
|
6911
|
+
} catch {
|
|
6912
|
+
// The original non-abort port rejection remains authoritative.
|
|
6913
|
+
}
|
|
6914
|
+
// A thrown port call carries no authoritative result, so the
|
|
6915
|
+
// prior token remains available for a later explicit resume.
|
|
6916
|
+
throw decidePlayerCallFailure(
|
|
6917
|
+
error,
|
|
6918
|
+
roleId,
|
|
6919
|
+
playerId,
|
|
6920
|
+
callSignal,
|
|
6921
|
+
cohortCancellation,
|
|
6922
|
+
);
|
|
6923
|
+
}
|
|
6924
|
+
|
|
6925
|
+
let result: PlayerResult;
|
|
6926
|
+
try {
|
|
6927
|
+
result = validatePlayerResult(rawResult);
|
|
6928
|
+
} catch (error) {
|
|
6929
|
+
if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
|
|
6930
|
+
try {
|
|
6931
|
+
await emitTrace(
|
|
6932
|
+
'player.call.finished',
|
|
6933
|
+
{ ...identity, status: 'error', error: normalizeError(error) },
|
|
6934
|
+
position,
|
|
6935
|
+
traceAborts,
|
|
6936
|
+
);
|
|
6937
|
+
} catch {
|
|
6938
|
+
// The malformed host result remains authoritative.
|
|
6939
|
+
}
|
|
6940
|
+
throw decidePlayerCallFailure(
|
|
6941
|
+
error,
|
|
6942
|
+
roleId,
|
|
6943
|
+
playerId,
|
|
6944
|
+
callSignal,
|
|
6945
|
+
cohortCancellation,
|
|
6946
|
+
);
|
|
6947
|
+
}
|
|
6948
|
+
|
|
6949
|
+
try {
|
|
6950
|
+
updatePlayerResume(roleId, playerId, result);
|
|
6951
|
+
} catch (error) {
|
|
6952
|
+
if (!isAbortFailure(error, callSignal)) controlPlaneError ??= error;
|
|
6953
|
+
try {
|
|
6954
|
+
await emitTrace(
|
|
6955
|
+
'player.call.finished',
|
|
6956
|
+
{ ...identity, status: 'error', error: normalizeError(error) },
|
|
6957
|
+
position,
|
|
6958
|
+
traceAborts,
|
|
6959
|
+
);
|
|
6960
|
+
} catch {
|
|
6961
|
+
// The continuation-store failure remains authoritative.
|
|
6962
|
+
}
|
|
6963
|
+
throw error;
|
|
6964
|
+
}
|
|
6965
|
+
|
|
6966
|
+
await emitTrace(
|
|
6967
|
+
'player.call.finished',
|
|
6968
|
+
{
|
|
6969
|
+
...identity,
|
|
6970
|
+
status: result.status,
|
|
6971
|
+
...(result.finalText !== undefined
|
|
6972
|
+
? { finalText: result.finalText }
|
|
6973
|
+
: {}),
|
|
6974
|
+
...(result.error !== undefined
|
|
6975
|
+
? { error: normalizeError(result.error) }
|
|
6976
|
+
: {}),
|
|
6977
|
+
...(result.resumeToken !== undefined
|
|
6978
|
+
? { resumeToken: result.resumeToken }
|
|
6979
|
+
: {}),
|
|
6980
|
+
},
|
|
6981
|
+
position,
|
|
6982
|
+
traceAborts,
|
|
6983
|
+
);
|
|
6984
|
+
// DR-063 §2: the player bridge builds the failure Error for a
|
|
6985
|
+
// non-`ok` result, so the cause is decided here, where the role,
|
|
6986
|
+
// the player, and the reported error are known, and a successful
|
|
6987
|
+
// result clears it so no later failure inherits it.
|
|
6988
|
+
if (result.status === 'ok') {
|
|
6989
|
+
pendingResultFailureCause = undefined;
|
|
6990
|
+
} else if (result.status === 'aborted') {
|
|
6991
|
+
pendingResultFailureCause = ABORTED_FAILURE_CAUSE;
|
|
6992
|
+
} else {
|
|
6993
|
+
pendingResultFailureCause = playerFailureCause({
|
|
6994
|
+
roleId,
|
|
6995
|
+
...(playerId === undefined ? {} : { playerId }),
|
|
6996
|
+
error:
|
|
6997
|
+
result.error ??
|
|
6998
|
+
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
6999
|
+
});
|
|
7000
|
+
}
|
|
7001
|
+
if (pendingResultFailureCause !== undefined) {
|
|
7002
|
+
resultFailureCauses.set(result, pendingResultFailureCause);
|
|
7003
|
+
}
|
|
7004
|
+
return result;
|
|
7005
|
+
};
|
|
7006
|
+
const reportPlayerKeyCollision = async (
|
|
7007
|
+
callSignal: AbortSignal,
|
|
7008
|
+
): Promise<never> => {
|
|
5694
7009
|
const error = new Error(
|
|
5695
7010
|
`simultaneous calls to player key ${playerKey} are not allowed`,
|
|
5696
7011
|
);
|
|
7012
|
+
const traceAborts =
|
|
7013
|
+
parallelProfile === undefined
|
|
7014
|
+
? undefined
|
|
7015
|
+
: abortReasonClassifier(callSignal, activeAborts);
|
|
5697
7016
|
await emitCallStarted(
|
|
5698
7017
|
'player.call.started',
|
|
5699
7018
|
'player.call.finished',
|
|
5700
7019
|
{ ...callIdentity(selectedResume), prompt },
|
|
5701
7020
|
position,
|
|
5702
|
-
|
|
7021
|
+
callSignal,
|
|
7022
|
+
undefined,
|
|
7023
|
+
traceAborts,
|
|
5703
7024
|
);
|
|
5704
7025
|
await emitTrace(
|
|
5705
7026
|
'player.call.finished',
|
|
@@ -5709,149 +7030,63 @@ export function createXStatePlaybookRuntime<
|
|
|
5709
7030
|
error: normalizeError(error),
|
|
5710
7031
|
},
|
|
5711
7032
|
position,
|
|
7033
|
+
traceAborts,
|
|
5712
7034
|
);
|
|
5713
7035
|
throw error;
|
|
5714
|
-
}
|
|
5715
|
-
activePlayerKeys.add(playerKey);
|
|
7036
|
+
};
|
|
5716
7037
|
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
signal,
|
|
7038
|
+
// DR-067 §1: the working leaves of one parallel state entered
|
|
7039
|
+
// together run as one all-`unchanged` repository cohort, at most once
|
|
7040
|
+
// per public boundary; a corrective re-ask and a single region
|
|
7041
|
+
// resuming after its Boss reply take the exclusive path below.
|
|
7042
|
+
const cohortState =
|
|
7043
|
+
deferredContinuation === undefined
|
|
7044
|
+
? cohortParallelStateFor(input.stateId)
|
|
7045
|
+
: undefined;
|
|
7046
|
+
if (cohortState !== undefined) {
|
|
7047
|
+
const effectBoundary = governedBoundarySeed(
|
|
7048
|
+
input,
|
|
7049
|
+
roleId,
|
|
7050
|
+
callId,
|
|
7051
|
+
turnId,
|
|
7052
|
+
);
|
|
7053
|
+
if (
|
|
7054
|
+
effectBoundary === undefined ||
|
|
7055
|
+
repositoryCapability?.runCohort === undefined
|
|
7056
|
+
) {
|
|
7057
|
+
throw new Error(
|
|
7058
|
+
`${label} parallel state ${cohortState.stateId} requires governed player states and repository.runCohort`,
|
|
5739
7059
|
);
|
|
7060
|
+
}
|
|
7061
|
+
activeGovernedBoundarySeen = true;
|
|
7062
|
+
return await joinCohort(cohortState, {
|
|
7063
|
+
region: cohortState.regions.find(
|
|
7064
|
+
({ workingStateId }) => workingStateId === input.stateId,
|
|
7065
|
+
)!,
|
|
7066
|
+
input,
|
|
7067
|
+
roleId,
|
|
7068
|
+
playerId,
|
|
7069
|
+
playerKey,
|
|
7070
|
+
signal,
|
|
7071
|
+
effectBoundary,
|
|
7072
|
+
run: (callSignal, cohortCancellation, preExistingBlock) =>
|
|
7073
|
+
runTracedPlayerCall(
|
|
7074
|
+
selectedResume,
|
|
7075
|
+
preExistingBlock,
|
|
7076
|
+
callSignal,
|
|
7077
|
+
cohortCancellation,
|
|
7078
|
+
),
|
|
7079
|
+
reportCollision: reportPlayerKeyCollision,
|
|
7080
|
+
settled: deferredValue<PlayerResult>(),
|
|
7081
|
+
});
|
|
7082
|
+
}
|
|
5740
7083
|
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
// never start after abort, so settle the already-started pair
|
|
5746
|
-
// as `aborted` through the catch below.
|
|
5747
|
-
signal.throwIfAborted();
|
|
5748
|
-
rawResult = await requireHostPorts().callPlayer(
|
|
5749
|
-
roleId,
|
|
5750
|
-
callPrompt,
|
|
5751
|
-
signal,
|
|
5752
|
-
{
|
|
5753
|
-
resume,
|
|
5754
|
-
...(callPrompt === callFreshPrompt
|
|
5755
|
-
? {}
|
|
5756
|
-
: { freshPrompt: callFreshPrompt }),
|
|
5757
|
-
},
|
|
5758
|
-
);
|
|
5759
|
-
// A host promise is not required to honor cancellation. Do not
|
|
5760
|
-
// let a late result mutate continuity or publish a successful
|
|
5761
|
-
// finish.
|
|
5762
|
-
signal.throwIfAborted();
|
|
5763
|
-
} catch (error) {
|
|
5764
|
-
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
5765
|
-
try {
|
|
5766
|
-
await emitTrace(
|
|
5767
|
-
'player.call.finished',
|
|
5768
|
-
{
|
|
5769
|
-
...identity,
|
|
5770
|
-
status: isAbortFailure(error, signal) ? 'aborted' : 'error',
|
|
5771
|
-
error: normalizeError(error),
|
|
5772
|
-
},
|
|
5773
|
-
position,
|
|
5774
|
-
);
|
|
5775
|
-
} catch {
|
|
5776
|
-
// The original non-abort port rejection remains authoritative.
|
|
5777
|
-
}
|
|
5778
|
-
// A thrown port call carries no authoritative result, so the
|
|
5779
|
-
// prior token remains available for a later explicit resume.
|
|
5780
|
-
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5781
|
-
}
|
|
5782
|
-
|
|
5783
|
-
let result: PlayerResult;
|
|
5784
|
-
try {
|
|
5785
|
-
result = validatePlayerResult(rawResult);
|
|
5786
|
-
} catch (error) {
|
|
5787
|
-
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
5788
|
-
try {
|
|
5789
|
-
await emitTrace(
|
|
5790
|
-
'player.call.finished',
|
|
5791
|
-
{ ...identity, status: 'error', error: normalizeError(error) },
|
|
5792
|
-
position,
|
|
5793
|
-
);
|
|
5794
|
-
} catch {
|
|
5795
|
-
// The malformed host result remains authoritative.
|
|
5796
|
-
}
|
|
5797
|
-
throw decidePlayerCallFailure(error, roleId, playerId, signal);
|
|
5798
|
-
}
|
|
5799
|
-
|
|
5800
|
-
try {
|
|
5801
|
-
updatePlayerResume(roleId, playerId, result);
|
|
5802
|
-
} catch (error) {
|
|
5803
|
-
if (!isAbortFailure(error, signal)) controlPlaneError ??= error;
|
|
5804
|
-
try {
|
|
5805
|
-
await emitTrace(
|
|
5806
|
-
'player.call.finished',
|
|
5807
|
-
{ ...identity, status: 'error', error: normalizeError(error) },
|
|
5808
|
-
position,
|
|
5809
|
-
);
|
|
5810
|
-
} catch {
|
|
5811
|
-
// The continuation-store failure remains authoritative.
|
|
5812
|
-
}
|
|
5813
|
-
throw error;
|
|
5814
|
-
}
|
|
5815
|
-
|
|
5816
|
-
await emitTrace(
|
|
5817
|
-
'player.call.finished',
|
|
5818
|
-
{
|
|
5819
|
-
...identity,
|
|
5820
|
-
status: result.status,
|
|
5821
|
-
...(result.finalText !== undefined
|
|
5822
|
-
? { finalText: result.finalText }
|
|
5823
|
-
: {}),
|
|
5824
|
-
...(result.error !== undefined
|
|
5825
|
-
? { error: normalizeError(result.error) }
|
|
5826
|
-
: {}),
|
|
5827
|
-
...(result.resumeToken !== undefined
|
|
5828
|
-
? { resumeToken: result.resumeToken }
|
|
5829
|
-
: {}),
|
|
5830
|
-
},
|
|
5831
|
-
position,
|
|
5832
|
-
);
|
|
5833
|
-
// DR-063 §2: the player bridge builds the failure Error for a
|
|
5834
|
-
// non-`ok` result, so the cause is decided here, where the role,
|
|
5835
|
-
// the player, and the reported error are known, and a successful
|
|
5836
|
-
// result clears it so no later failure inherits it.
|
|
5837
|
-
if (result.status === 'ok') {
|
|
5838
|
-
lastFailureCause = undefined;
|
|
5839
|
-
} else if (result.status === 'aborted') {
|
|
5840
|
-
rememberFailureCause(ABORTED_FAILURE_CAUSE);
|
|
5841
|
-
} else {
|
|
5842
|
-
rememberFailureCause(
|
|
5843
|
-
playerFailureCause({
|
|
5844
|
-
roleId,
|
|
5845
|
-
...(playerId === undefined ? {} : { playerId }),
|
|
5846
|
-
error:
|
|
5847
|
-
result.error ??
|
|
5848
|
-
`callPlayer status ${JSON.stringify(result.status)}`,
|
|
5849
|
-
}),
|
|
5850
|
-
);
|
|
5851
|
-
}
|
|
5852
|
-
return result;
|
|
5853
|
-
};
|
|
7084
|
+
if (activePlayerKeys.has(playerKey)) {
|
|
7085
|
+
await reportPlayerKeyCollision(signal);
|
|
7086
|
+
}
|
|
7087
|
+
activePlayerKeys.add(playerKey);
|
|
5854
7088
|
|
|
7089
|
+
try {
|
|
5855
7090
|
const effectBoundary = governedBoundarySeed(
|
|
5856
7091
|
input,
|
|
5857
7092
|
roleId,
|
|
@@ -5956,10 +7191,13 @@ export function createXStatePlaybookRuntime<
|
|
|
5956
7191
|
}
|
|
5957
7192
|
},
|
|
5958
7193
|
|
|
5959
|
-
markPlayerResultFailure(error): Error {
|
|
5960
|
-
|
|
5961
|
-
?
|
|
5962
|
-
|
|
7194
|
+
markPlayerResultFailure(error, result): Error {
|
|
7195
|
+
const cause =
|
|
7196
|
+
(result === undefined ? undefined : resultFailureCauses.get(result)) ??
|
|
7197
|
+
pendingResultFailureCause;
|
|
7198
|
+
if (cause === undefined) return error;
|
|
7199
|
+
failureCauses.retain(error, cause);
|
|
7200
|
+
return attachPlaybookFailureCause(error, cause);
|
|
5963
7201
|
},
|
|
5964
7202
|
|
|
5965
7203
|
takeGovernedPlayerOutput(result): GovernedPlayerSettlement | undefined {
|
|
@@ -6200,6 +7438,16 @@ export function createXStatePlaybookRuntime<
|
|
|
6200
7438
|
}) as Promise<CaptainResult>;
|
|
6201
7439
|
},
|
|
6202
7440
|
};
|
|
7441
|
+
if (parallelProfile !== undefined) {
|
|
7442
|
+
// DR-067 §1: a parallel machine's boundary drain waits for every player
|
|
7443
|
+
// and judge call it started.
|
|
7444
|
+
const untrackedCallPlayer = boundary.callPlayer;
|
|
7445
|
+
const untrackedCallJudge = boundary.callJudge;
|
|
7446
|
+
boundary.callPlayer = (input, roleId, freshPrompt, signal) =>
|
|
7447
|
+
trackBoundaryCall(untrackedCallPlayer(input, roleId, freshPrompt, signal));
|
|
7448
|
+
boundary.callJudge = (purpose, stateId, prompt, signal) =>
|
|
7449
|
+
trackBoundaryCall(untrackedCallJudge(purpose, stateId, prompt, signal));
|
|
7450
|
+
}
|
|
6203
7451
|
|
|
6204
7452
|
function playerActor(
|
|
6205
7453
|
ports: PlaybookPorts,
|
|
@@ -6526,8 +7774,12 @@ export function createXStatePlaybookRuntime<
|
|
|
6526
7774
|
const nestedBridge = createNestedPlaybookBridge({
|
|
6527
7775
|
nextCallId: () => `playbook-${++playbookCallSequence}`,
|
|
6528
7776
|
getBoundarySignal: () => activeSignal,
|
|
6529
|
-
callPlaybook: (request, signal) =>
|
|
6530
|
-
requireHostPorts().callPlaybook(request, signal)
|
|
7777
|
+
callPlaybook: (request, signal) => {
|
|
7778
|
+
const call = requireHostPorts().callPlaybook(request, signal);
|
|
7779
|
+
return parallelProfile === undefined
|
|
7780
|
+
? call
|
|
7781
|
+
: trackBoundaryCall(Promise.resolve(call));
|
|
7782
|
+
},
|
|
6531
7783
|
emitStarted: async (event, aborts) => {
|
|
6532
7784
|
playbookCallTurnIds.set(event.callId, activeTurnId);
|
|
6533
7785
|
if (hasGovernedPlayerStates) {
|
|
@@ -6578,7 +7830,8 @@ export function createXStatePlaybookRuntime<
|
|
|
6578
7830
|
activeAborts = aborts ?? abortReasonClassifier(signal);
|
|
6579
7831
|
},
|
|
6580
7832
|
bindActorSettlement: (aborts) => {
|
|
6581
|
-
actorSettlementAborts =
|
|
7833
|
+
if (parallelProfile === undefined) actorSettlementAborts.length = 0;
|
|
7834
|
+
actorSettlementAborts.push(aborts);
|
|
6582
7835
|
},
|
|
6583
7836
|
onControlPlaneError: (error, aborts) => {
|
|
6584
7837
|
// The shared bridge classifies before reporting against its own
|
|
@@ -6619,18 +7872,50 @@ export function createXStatePlaybookRuntime<
|
|
|
6619
7872
|
}
|
|
6620
7873
|
return {
|
|
6621
7874
|
...normalized,
|
|
6622
|
-
cause:
|
|
7875
|
+
cause:
|
|
7876
|
+
failureCauses.causeOf(context.lastError) ??
|
|
7877
|
+
runtimeDefectCause(normalized.message),
|
|
6623
7878
|
};
|
|
6624
7879
|
}
|
|
6625
7880
|
|
|
7881
|
+
// DR-063 §2: the failure that parks the machine is the error its parking
|
|
7882
|
+
// transition carried, but a compiled machine keeps a JSON-safe record of
|
|
7883
|
+
// it as `lastError` (gears2fsm requires JSON-safe context), so neither
|
|
7884
|
+
// that value's identity nor its marker reaches the failed state. The cause
|
|
7885
|
+
// decided for the carried error is retained under the value the machine
|
|
7886
|
+
// kept, so every surface of this turn and of each later one publishes the
|
|
7887
|
+
// runtime's own decision for as long as that failure stands. A kept value
|
|
7888
|
+
// that already answers keeps its cause.
|
|
7889
|
+
function bindParkedFailureCause(
|
|
7890
|
+
context: Record<string, unknown>,
|
|
7891
|
+
event: unknown,
|
|
7892
|
+
): void {
|
|
7893
|
+
const kept = context.lastError;
|
|
7894
|
+
if (kept === undefined || kept === null) return;
|
|
7895
|
+
if (failureCauses.causeOf(kept) !== undefined) return;
|
|
7896
|
+
const decided = failureCauses.causeOf(actorErrorOf(event));
|
|
7897
|
+
if (decided !== undefined) failureCauses.retain(kept, decided);
|
|
7898
|
+
}
|
|
7899
|
+
|
|
6626
7900
|
// DR-063 §2: a rejected player port or a result the runtime cannot read is
|
|
6627
|
-
// a `player-failed` failure named by role; an abort is `aborted`.
|
|
7901
|
+
// a `player-failed` failure named by role; an abort is `aborted`. A
|
|
7902
|
+
// cancellation the runtime itself issued to a cohort member because its
|
|
7903
|
+
// sibling failed is not this player's failure and decides nothing, so the
|
|
7904
|
+
// originator's cause stands (DR-067 §1, playbook-55).
|
|
6628
7905
|
function decidePlayerCallFailure(
|
|
6629
7906
|
error: unknown,
|
|
6630
7907
|
roleId: string,
|
|
6631
7908
|
playerId: string | undefined,
|
|
6632
7909
|
signal: AbortSignal,
|
|
7910
|
+
cohortCancellation?: AbortSignal,
|
|
6633
7911
|
): unknown {
|
|
7912
|
+
if (
|
|
7913
|
+
cohortCancellation?.aborted === true &&
|
|
7914
|
+
Object.is(error, cohortCancellation.reason) &&
|
|
7915
|
+
!activeAborts?.isAbortReason(error)
|
|
7916
|
+
) {
|
|
7917
|
+
return error;
|
|
7918
|
+
}
|
|
6634
7919
|
const cause = isAbortFailure(error, signal)
|
|
6635
7920
|
? ABORTED_FAILURE_CAUSE
|
|
6636
7921
|
: playerFailureCause({
|
|
@@ -6638,7 +7923,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6638
7923
|
...(playerId === undefined ? {} : { playerId }),
|
|
6639
7924
|
error,
|
|
6640
7925
|
});
|
|
6641
|
-
|
|
7926
|
+
failureCauses.retain(error, cause);
|
|
6642
7927
|
return attachPlaybookFailureCause(error, cause);
|
|
6643
7928
|
}
|
|
6644
7929
|
|
|
@@ -6689,12 +7974,21 @@ export function createXStatePlaybookRuntime<
|
|
|
6689
7974
|
previousState: previousState ?? null,
|
|
6690
7975
|
state,
|
|
6691
7976
|
};
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
7977
|
+
if (parallelProfile === undefined) {
|
|
7978
|
+
const pendingBossQuestion = pendingBossQuestionForState(state, context);
|
|
7979
|
+
if (
|
|
7980
|
+
pendingBossQuestion !== undefined &&
|
|
7981
|
+
!hasUnresolvedReconciliation()
|
|
7982
|
+
) {
|
|
7983
|
+
payload.pendingBossQuestion = pendingBossQuestion;
|
|
7984
|
+
}
|
|
7985
|
+
} else if (!hasUnresolvedReconciliation()) {
|
|
7986
|
+
// DR-067 §1: a parallel machine's telemetry carries every question
|
|
7987
|
+
// pending in an active wait, in the plural.
|
|
7988
|
+
const pendingBossQuestions = pendingBossQuestionsFor(state, context);
|
|
7989
|
+
if (pendingBossQuestions.length > 0) {
|
|
7990
|
+
payload.pendingBossQuestions = pendingBossQuestions;
|
|
7991
|
+
}
|
|
6698
7992
|
}
|
|
6699
7993
|
if (state.stateId === 'failed') {
|
|
6700
7994
|
const lastError = failedStateError(context);
|
|
@@ -6729,7 +8023,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6729
8023
|
message,
|
|
6730
8024
|
...(data === undefined ? {} : { data }),
|
|
6731
8025
|
state,
|
|
6732
|
-
...stateIdentity(state
|
|
8026
|
+
...stateIdentity(statusTraceStateId(state)),
|
|
6733
8027
|
},
|
|
6734
8028
|
position,
|
|
6735
8029
|
),
|
|
@@ -6779,8 +8073,7 @@ export function createXStatePlaybookRuntime<
|
|
|
6779
8073
|
function consumeActorSettlementAborts(
|
|
6780
8074
|
forSnapshot = false,
|
|
6781
8075
|
): AbortReasonClassifier | undefined {
|
|
6782
|
-
const aborts = actorSettlementAborts ?? actorSettlementErrorAborts;
|
|
6783
|
-
actorSettlementAborts = undefined;
|
|
8076
|
+
const aborts = actorSettlementAborts.shift() ?? actorSettlementErrorAborts;
|
|
6784
8077
|
actorSettlementErrorAborts = undefined;
|
|
6785
8078
|
if (forSnapshot && aborts !== undefined) {
|
|
6786
8079
|
// XState can report an errored root through both its inspection
|
|
@@ -6852,7 +8145,14 @@ export function createXStatePlaybookRuntime<
|
|
|
6852
8145
|
try {
|
|
6853
8146
|
const snap = inspectionEvent.snapshot;
|
|
6854
8147
|
const state = normalizePlaybookSnapshot(snap);
|
|
6855
|
-
if (
|
|
8148
|
+
if (
|
|
8149
|
+
state.stateId === undefined &&
|
|
8150
|
+
!(
|
|
8151
|
+
parallelProfile?.states.some(({ stateId }) =>
|
|
8152
|
+
state.activeStateIds.includes(stateId),
|
|
8153
|
+
) ?? false
|
|
8154
|
+
)
|
|
8155
|
+
) {
|
|
6856
8156
|
throw new Error(
|
|
6857
8157
|
`${label} root snapshot must expose exactly one playbook state id`,
|
|
6858
8158
|
);
|
|
@@ -6882,6 +8182,9 @@ export function createXStatePlaybookRuntime<
|
|
|
6882
8182
|
}
|
|
6883
8183
|
const context = ((snap as { context?: unknown }).context ??
|
|
6884
8184
|
{}) as Record<string, unknown>;
|
|
8185
|
+
if (state.stateId === 'failed') {
|
|
8186
|
+
bindParkedFailureCause(context, inspectionEvent.event);
|
|
8187
|
+
}
|
|
6885
8188
|
if (
|
|
6886
8189
|
state.stateId === BOSS_REPLY_WAIT_STATE_ID &&
|
|
6887
8190
|
deferredReconciliationOperationId !== undefined
|
|
@@ -6903,7 +8206,9 @@ export function createXStatePlaybookRuntime<
|
|
|
6903
8206
|
context,
|
|
6904
8207
|
);
|
|
6905
8208
|
const stateStatuses = completeFailedStatuses(
|
|
6906
|
-
|
|
8209
|
+
bossRelevantStateIds !== undefined && usesDefaultStatuses
|
|
8210
|
+
? parallelStatusesFor(previousState, state, context)
|
|
8211
|
+
: statusesForState(state, context, inspectionEvent.event),
|
|
6907
8212
|
state,
|
|
6908
8213
|
context,
|
|
6909
8214
|
);
|
|
@@ -7110,13 +8415,17 @@ export function createXStatePlaybookRuntime<
|
|
|
7110
8415
|
emissionQueue.clear();
|
|
7111
8416
|
judgeQueue.clear();
|
|
7112
8417
|
appliedReceipts.clear();
|
|
8418
|
+
pendingCohorts.clear();
|
|
8419
|
+
consumedCohortBoundaries.clear();
|
|
8420
|
+
activeBoundaryCalls.clear();
|
|
8421
|
+
cohortCompletionQueue.clear();
|
|
7113
8422
|
actor = undefined;
|
|
7114
8423
|
session = undefined;
|
|
7115
8424
|
savedPorts = undefined;
|
|
7116
8425
|
runtimePorts = undefined;
|
|
7117
8426
|
activeSignal = undefined;
|
|
7118
8427
|
activeAborts = undefined;
|
|
7119
|
-
actorSettlementAborts =
|
|
8428
|
+
actorSettlementAborts.length = 0;
|
|
7120
8429
|
actorSettlementErrorAborts = undefined;
|
|
7121
8430
|
activeAbortEmission = undefined;
|
|
7122
8431
|
activeTurnId = undefined;
|
|
@@ -7206,12 +8515,12 @@ export function createXStatePlaybookRuntime<
|
|
|
7206
8515
|
// context member above sources. A candidate whose event the live
|
|
7207
8516
|
// snapshot does not accept — or whose payload the runtime can source
|
|
7208
8517
|
// from neither — is excluded rather than completed with invented text.
|
|
8518
|
+
// The caller has already applied the PBRT-71 replay fence.
|
|
7209
8519
|
function retryActionFor(
|
|
7210
8520
|
snapshot: unknown,
|
|
7211
8521
|
stateId: string | undefined,
|
|
7212
8522
|
): DerivedControlAction | undefined {
|
|
7213
8523
|
if (stateId !== 'failed') return undefined;
|
|
7214
|
-
if (!failedAttemptAllowsReplay()) return undefined;
|
|
7215
8524
|
const retryEvent = retryEventFrom(snapshot);
|
|
7216
8525
|
if (retryEvent === undefined) return undefined;
|
|
7217
8526
|
if (!snapshotCan(snapshot, retryEvent)) return undefined;
|
|
@@ -7341,6 +8650,15 @@ export function createXStatePlaybookRuntime<
|
|
|
7341
8650
|
);
|
|
7342
8651
|
return derived;
|
|
7343
8652
|
}
|
|
8653
|
+
// PBRT-71 / DR-040 §4: out of the failure state, a jump restarts the
|
|
8654
|
+
// failed attempt's work exactly as the retry does, so one fence gates
|
|
8655
|
+
// both. Unless every governed boundary of that attempt carries a
|
|
8656
|
+
// complete `unchanged` receipt, neither is advertised — and `apply`,
|
|
8657
|
+
// which revalidates against this list, accepts neither — so no
|
|
8658
|
+
// control can replay a player call whose effect may already exist.
|
|
8659
|
+
if (state.stateId === 'failed' && !failedAttemptAllowsReplay()) {
|
|
8660
|
+
return derived;
|
|
8661
|
+
}
|
|
7344
8662
|
const retry = retryActionFor(snapshot, state.stateId);
|
|
7345
8663
|
if (retry !== undefined) derived.push(retry);
|
|
7346
8664
|
// Jump entries: resumable targets whose explicit-state-jump event the
|
|
@@ -7408,9 +8726,12 @@ export function createXStatePlaybookRuntime<
|
|
|
7408
8726
|
// state whose source declares no description publishes none: an id is
|
|
7409
8727
|
// never promoted into a description by default.
|
|
7410
8728
|
function stateDescriptionFor(state: PlaybookState): string | undefined {
|
|
8729
|
+
// DR-067 §1: inside a parallel state, the meaning published is the
|
|
8730
|
+
// root state's — the parallel parent's own description.
|
|
7411
8731
|
const keys = [
|
|
7412
8732
|
...(state.stateId === undefined ? [] : [state.stateId]),
|
|
7413
8733
|
...(typeof state.value === 'string' ? [state.value] : []),
|
|
8734
|
+
...(isPlainObject(state.value) ? Object.keys(state.value) : []),
|
|
7414
8735
|
...state.activeStateIds,
|
|
7415
8736
|
];
|
|
7416
8737
|
for (const key of keys) {
|
|
@@ -7781,7 +9102,7 @@ export function createXStatePlaybookRuntime<
|
|
|
7781
9102
|
?.context ?? {}) as Record<string, unknown>;
|
|
7782
9103
|
const actual =
|
|
7783
9104
|
state?.stateId === BOSS_REPLY_WAIT_STATE_ID
|
|
7784
|
-
?
|
|
9105
|
+
? singlePendingBossQuestion(state, context)
|
|
7785
9106
|
: undefined;
|
|
7786
9107
|
if (!isDeepStrictEqual(actual, expected)) {
|
|
7787
9108
|
throw new Error(
|
|
@@ -8138,7 +9459,7 @@ export function createXStatePlaybookRuntime<
|
|
|
8138
9459
|
const state = normalizePlaybookSnapshot(snapshot);
|
|
8139
9460
|
const context = ((snapshot as { context?: unknown }).context ??
|
|
8140
9461
|
{}) as Record<string, unknown>;
|
|
8141
|
-
const pending =
|
|
9462
|
+
const pending = singlePendingBossQuestion(state, context);
|
|
8142
9463
|
if (
|
|
8143
9464
|
pending === undefined ||
|
|
8144
9465
|
currentBoundDeferredOperation(pending)?.operationId !== operationId
|
|
@@ -8243,19 +9564,25 @@ export function createXStatePlaybookRuntime<
|
|
|
8243
9564
|
}
|
|
8244
9565
|
const state = currentState();
|
|
8245
9566
|
if (state.status !== 'active' || !state.quiescent) return undefined;
|
|
9567
|
+
const context = (actor.getSnapshot() as { context?: unknown })
|
|
9568
|
+
.context as Record<string, unknown>;
|
|
9569
|
+
// DR-063 §2: the failed state's `lastError` is persisted as every live
|
|
9570
|
+
// surface publishes it, completed from the runtime's own cause slot, so
|
|
9571
|
+
// a thrown value that refused the cause still explains itself after a
|
|
9572
|
+
// restart.
|
|
8246
9573
|
const machineSnapshot = detachPersistedMachineSnapshot(
|
|
8247
9574
|
actor.getPersistedSnapshot(),
|
|
9575
|
+
state.stateId === 'failed'
|
|
9576
|
+
? { lastError: failedStateError(context ?? {}) }
|
|
9577
|
+
: {},
|
|
8248
9578
|
);
|
|
8249
|
-
const context = (actor.getSnapshot() as { context?: unknown })
|
|
8250
|
-
.context as Record<string, unknown>;
|
|
8251
9579
|
effectLedgerMirror = currentEffectLedger();
|
|
8252
9580
|
refreshRetainedEffectReconciliation(effectLedgerMirror);
|
|
8253
9581
|
syncDeferredReconciliationOverlay();
|
|
8254
9582
|
refreshUnresolvedSemanticReconciliation(effectLedgerMirror);
|
|
8255
|
-
const
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
: undefined;
|
|
9583
|
+
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
9584
|
+
? pendingBossQuestionsFor(state, context ?? {})
|
|
9585
|
+
: [];
|
|
8259
9586
|
const failedEffectAttempt =
|
|
8260
9587
|
hasGovernedPlayerStates &&
|
|
8261
9588
|
state.stateId === 'failed' &&
|
|
@@ -8281,17 +9608,12 @@ export function createXStatePlaybookRuntime<
|
|
|
8281
9608
|
: {}),
|
|
8282
9609
|
},
|
|
8283
9610
|
state,
|
|
8284
|
-
pendingBossQuestions:
|
|
8285
|
-
pending
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
asker: pending.asker,
|
|
8291
|
-
question: pending.question,
|
|
8292
|
-
sourceItem: pending.sourceItem,
|
|
8293
|
-
},
|
|
8294
|
-
],
|
|
9611
|
+
pendingBossQuestions: pendingQuestions.map((pending) => ({
|
|
9612
|
+
questionId: pending.questionId,
|
|
9613
|
+
asker: pending.asker,
|
|
9614
|
+
question: pending.question,
|
|
9615
|
+
sourceItem: pending.sourceItem,
|
|
9616
|
+
})),
|
|
8295
9617
|
effectLedger: effectLedgerMirror,
|
|
8296
9618
|
...(retainedEffectSourceSessionId === undefined
|
|
8297
9619
|
? {}
|
|
@@ -8319,17 +9641,23 @@ export function createXStatePlaybookRuntime<
|
|
|
8319
9641
|
},
|
|
8320
9642
|
|
|
8321
9643
|
// DR-038 §§1,5 / PBRT-61/PBRT-65: adoption is restore under a fresh
|
|
8322
|
-
// engagement identity and counter lineage, exposed separately so
|
|
8323
|
-
// capability-less
|
|
8324
|
-
//
|
|
9644
|
+
// engagement identity and counter lineage, exposed separately so a
|
|
9645
|
+
// capability-less runtime can omit it. Runtime-visible preflight
|
|
9646
|
+
// mismatches reject before effects; after preflight the new
|
|
8325
9647
|
// session.started boundary owns failed-start cleanup just like init.
|
|
8326
|
-
|
|
8327
|
-
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
9648
|
+
// DR-067 §2: retained-snapshot adoption stays in the flat domain, so a
|
|
9649
|
+
// machine that declares a parallel state omits the member.
|
|
9650
|
+
...(parallelProfile === undefined
|
|
9651
|
+
? {
|
|
9652
|
+
async adopt(
|
|
9653
|
+
nextSession: PlaybookSession,
|
|
9654
|
+
snapshot: PlaybookRuntimeSnapshot,
|
|
9655
|
+
context: PlaybookAdoptionContext,
|
|
9656
|
+
): Promise<void> {
|
|
9657
|
+
await rehydrateSnapshot('adopt', nextSession, snapshot, context);
|
|
9658
|
+
},
|
|
9659
|
+
}
|
|
9660
|
+
: {}),
|
|
8333
9661
|
|
|
8334
9662
|
// DR-029 / PBRT-52: side-effect-free control view over the live
|
|
8335
9663
|
// snapshot, valid at parked quiescence outside an active boundary.
|
|
@@ -8357,10 +9685,9 @@ export function createXStatePlaybookRuntime<
|
|
|
8357
9685
|
const state = currentState();
|
|
8358
9686
|
const context = ((snapshot as { context?: unknown }).context ??
|
|
8359
9687
|
{}) as Record<string, unknown>;
|
|
8360
|
-
const
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
: undefined;
|
|
9688
|
+
const pendingQuestions = !hasUnresolvedReconciliation()
|
|
9689
|
+
? pendingBossQuestionsFor(state, context)
|
|
9690
|
+
: [];
|
|
8364
9691
|
const lastError =
|
|
8365
9692
|
state.stateId === 'failed'
|
|
8366
9693
|
? failedStateError(context)
|
|
@@ -8376,17 +9703,12 @@ export function createXStatePlaybookRuntime<
|
|
|
8376
9703
|
...(projectedContext !== undefined
|
|
8377
9704
|
? { context: projectedContext }
|
|
8378
9705
|
: {}),
|
|
8379
|
-
pendingQuestions:
|
|
8380
|
-
pending
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
asker: pending.asker,
|
|
8386
|
-
question: pending.question,
|
|
8387
|
-
sourceItem: pending.sourceItem,
|
|
8388
|
-
},
|
|
8389
|
-
],
|
|
9706
|
+
pendingQuestions: pendingQuestions.map((pending) => ({
|
|
9707
|
+
questionId: pending.questionId,
|
|
9708
|
+
asker: pending.asker,
|
|
9709
|
+
question: pending.question,
|
|
9710
|
+
sourceItem: pending.sourceItem,
|
|
9711
|
+
})),
|
|
8390
9712
|
...(lastError !== undefined ? { lastError } : {}),
|
|
8391
9713
|
actions: deriveControlActions(snapshot).map(({ action }) => action),
|
|
8392
9714
|
});
|
|
@@ -8455,6 +9777,7 @@ export function createXStatePlaybookRuntime<
|
|
|
8455
9777
|
const callId = `apply-${++applyCallSequence}`;
|
|
8456
9778
|
const position: TracePosition = { turnId, callId };
|
|
8457
9779
|
activeTurnId = turnId;
|
|
9780
|
+
publicBoundarySequence += 1;
|
|
8458
9781
|
beginAutomaticReplayBoundary();
|
|
8459
9782
|
activeSignal = signal;
|
|
8460
9783
|
activeAborts = abortReasonClassifier(signal);
|
|
@@ -8655,6 +9978,7 @@ export function createXStatePlaybookRuntime<
|
|
|
8655
9978
|
if (receipt !== undefined && receipt.disposition !== 'rejected') {
|
|
8656
9979
|
appliedReceipts.set(key, receipt);
|
|
8657
9980
|
}
|
|
9981
|
+
if (parallelProfile !== undefined) await drainBoundaryCalls();
|
|
8658
9982
|
try {
|
|
8659
9983
|
await drainEmissions();
|
|
8660
9984
|
} catch (error) {
|
|
@@ -8758,14 +10082,16 @@ export function createXStatePlaybookRuntime<
|
|
|
8758
10082
|
refreshRetainedEffectFenceFromHost();
|
|
8759
10083
|
const turnId = ++turnSequence;
|
|
8760
10084
|
activeTurnId = turnId;
|
|
10085
|
+
publicBoundarySequence += 1;
|
|
8761
10086
|
beginAutomaticReplayBoundary();
|
|
8762
10087
|
activeSignal = signal;
|
|
8763
10088
|
activeAborts = abortReasonClassifier(signal);
|
|
8764
10089
|
activeAbortEmission = undefined;
|
|
8765
10090
|
controlPlaneError = undefined;
|
|
8766
|
-
// DR-063 §2: each turn decides its own failures, so
|
|
8767
|
-
// carries
|
|
8768
|
-
|
|
10091
|
+
// DR-063 §2: each turn decides its own failures, so no pending result
|
|
10092
|
+
// cause carries into this one; a cause decided for a parked failure
|
|
10093
|
+
// stays bound to that failure.
|
|
10094
|
+
pendingResultFailureCause = undefined;
|
|
8769
10095
|
let result: PlaybookRunResult | undefined;
|
|
8770
10096
|
let operationError: unknown;
|
|
8771
10097
|
// The boundary sentinel releases on every exit: a settlement defect
|
|
@@ -8798,7 +10124,7 @@ export function createXStatePlaybookRuntime<
|
|
|
8798
10124
|
const stateId = normalizePlaybookSnapshot(snapshot).stateId;
|
|
8799
10125
|
const snapshotContext = ((snapshot as { context?: unknown })
|
|
8800
10126
|
.context ?? {}) as Record<string, unknown>;
|
|
8801
|
-
deferredPending =
|
|
10127
|
+
deferredPending = singlePendingBossQuestion(
|
|
8802
10128
|
normalizePlaybookSnapshot(snapshot),
|
|
8803
10129
|
snapshotContext,
|
|
8804
10130
|
);
|
|
@@ -8965,6 +10291,7 @@ export function createXStatePlaybookRuntime<
|
|
|
8965
10291
|
operationError = error;
|
|
8966
10292
|
}
|
|
8967
10293
|
|
|
10294
|
+
if (parallelProfile !== undefined) await drainBoundaryCalls();
|
|
8968
10295
|
let drainError: unknown;
|
|
8969
10296
|
try {
|
|
8970
10297
|
await drainEmissions();
|
|
@@ -9078,6 +10405,7 @@ export function createXStatePlaybookRuntime<
|
|
|
9078
10405
|
return runResultFor('no-action');
|
|
9079
10406
|
}
|
|
9080
10407
|
activeTurnId = playbookCallTurnIds.get(input.callId);
|
|
10408
|
+
publicBoundarySequence += 1;
|
|
9081
10409
|
bindAutomaticReplayBoundary(
|
|
9082
10410
|
playbookCallEffectPrefixes.has(input.callId)
|
|
9083
10411
|
? playbookCallEffectPrefixes.get(input.callId)
|
|
@@ -9122,6 +10450,7 @@ export function createXStatePlaybookRuntime<
|
|
|
9122
10450
|
error: normalizeError(input.signal.reason),
|
|
9123
10451
|
};
|
|
9124
10452
|
}
|
|
10453
|
+
if (parallelProfile !== undefined) await drainBoundaryCalls();
|
|
9125
10454
|
let drainError: unknown;
|
|
9126
10455
|
try {
|
|
9127
10456
|
await drainEmissions();
|
|
@@ -9239,10 +10568,14 @@ export function createXStatePlaybookRuntime<
|
|
|
9239
10568
|
emissionQueue.clear();
|
|
9240
10569
|
judgeQueue.clear();
|
|
9241
10570
|
appliedReceipts.clear();
|
|
10571
|
+
pendingCohorts.clear();
|
|
10572
|
+
consumedCohortBoundaries.clear();
|
|
10573
|
+
activeBoundaryCalls.clear();
|
|
10574
|
+
cohortCompletionQueue.clear();
|
|
9242
10575
|
actor = undefined;
|
|
9243
10576
|
activeSignal = undefined;
|
|
9244
10577
|
activeAborts = undefined;
|
|
9245
|
-
actorSettlementAborts =
|
|
10578
|
+
actorSettlementAborts.length = 0;
|
|
9246
10579
|
actorSettlementErrorAborts = undefined;
|
|
9247
10580
|
activeAbortEmission = undefined;
|
|
9248
10581
|
activeTurnId = undefined;
|