@sublang/playbook 5.0.0 → 7.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 +22 -14
- package/docs/cli.md +138 -90
- package/docs/configuration.md +113 -29
- package/docs/embedding.md +24 -16
- package/package.json +42 -21
- package/reference/sdlc/captain.playbook/captain.playbook.js +2 -0
- package/reference/sdlc/captain.playbook/captain.playbook.ts +2 -0
- package/reference/sdlc/code.md +55 -97
- package/reference/sdlc/code.playbook/bin/launch-config.js +938 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +145 -562
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1171 -983
- package/reference/sdlc/code.playbook/bin/session-store.js +1169 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +229 -94
- package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +26 -44
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +61 -66
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +100 -149
- package/reference/sdlc/code.playbook/code.fsm.js +587 -1347
- package/reference/sdlc/code.playbook/code.fsm.ts +809 -1650
- package/reference/sdlc/code.playbook/code.gears.md +51 -263
- package/reference/sdlc/code.playbook/code.playbook.d.ts +8 -47
- package/reference/sdlc/code.playbook/code.playbook.js +69 -656
- package/reference/sdlc/code.playbook/code.playbook.ts +90 -867
- package/reference/sdlc/code.playbook/code.registry.d.ts +9 -25
- package/reference/sdlc/code.playbook/code.registry.js +20 -78
- package/reference/sdlc/code.playbook/code.registry.ts +58 -122
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +70 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +954 -80
- package/reference/sdlc/code.playbook/playbook-captain.ts +1408 -80
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +37 -36
- package/reference/sdlc/decide.md +54 -0
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +261 -0
- package/reference/sdlc/decide.playbook/decide.fsm.js +894 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +1152 -0
- package/reference/sdlc/decide.playbook/decide.gears.md +88 -0
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +67 -0
- package/reference/sdlc/{discuss.playbook/discuss.playbook.js → decide.playbook/decide.playbook.js} +545 -372
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +665 -454
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +41 -0
- package/reference/sdlc/decide.playbook/decide.registry.js +60 -0
- package/reference/sdlc/decide.playbook/decide.registry.ts +125 -0
- package/reference/sdlc/review.md +81 -0
- package/reference/sdlc/review.playbook/review.fsm.d.ts +183 -0
- package/reference/sdlc/review.playbook/review.fsm.js +524 -0
- package/reference/sdlc/review.playbook/review.fsm.ts +652 -0
- package/reference/sdlc/review.playbook/review.gears.md +112 -0
- package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -0
- package/reference/sdlc/review.playbook/review.playbook.js +112 -0
- package/reference/sdlc/review.playbook/review.playbook.ts +201 -0
- package/reference/sdlc/review.playbook/review.registry.d.ts +43 -0
- package/reference/sdlc/review.playbook/review.registry.js +73 -0
- package/reference/sdlc/review.playbook/review.registry.ts +138 -0
- package/slc/gears2fsm.md +13 -4
- package/slc/link.md +83 -14
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +21 -2
- package/src/runtime.ts +38 -6
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +319 -35
- package/src/xstate-playbook-runtime.ts +412 -41
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +384 -57
- package/src/xstate-runtime.ts +542 -71
- package/reference/sdlc/discuss.md +0 -93
- package/reference/sdlc/discuss.playbook/discuss.fsm.d.ts +0 -396
- package/reference/sdlc/discuss.playbook/discuss.fsm.js +0 -2067
- package/reference/sdlc/discuss.playbook/discuss.fsm.ts +0 -2465
- package/reference/sdlc/discuss.playbook/discuss.gears.md +0 -258
- package/reference/sdlc/discuss.playbook/discuss.playbook.d.ts +0 -113
- package/reference/sdlc/discuss.playbook/discuss.registry.d.ts +0 -58
- package/reference/sdlc/discuss.playbook/discuss.registry.js +0 -97
- package/reference/sdlc/discuss.playbook/discuss.registry.ts +0 -153
|
@@ -589,6 +589,48 @@ function collectInvokeSources(machine) {
|
|
|
589
589
|
visit(machine.config);
|
|
590
590
|
return sources;
|
|
591
591
|
}
|
|
592
|
+
function collectPlayerStatePlayers(machine) {
|
|
593
|
+
const players = new Map();
|
|
594
|
+
const visit = (stateDef, stateKey) => {
|
|
595
|
+
if (!isPlainObject(stateDef))
|
|
596
|
+
return;
|
|
597
|
+
const invoke = stateDef.invoke;
|
|
598
|
+
const invokes = Array.isArray(invoke) ? invoke : invoke ? [invoke] : [];
|
|
599
|
+
if (invokes.some((entry) => isPlainObject(entry) && entry.src === 'player')) {
|
|
600
|
+
const playbookMeta = isPlainObject(stateDef.meta)
|
|
601
|
+
? stateDef.meta.playbook
|
|
602
|
+
: undefined;
|
|
603
|
+
const stateId = isPlainObject(playbookMeta) &&
|
|
604
|
+
typeof playbookMeta.stateId === 'string'
|
|
605
|
+
? playbookMeta.stateId
|
|
606
|
+
: typeof stateDef.id === 'string'
|
|
607
|
+
? stateDef.id
|
|
608
|
+
: stateKey;
|
|
609
|
+
if (stateId.trim().length === 0) {
|
|
610
|
+
throw new TypeError('player state metadata must use a non-empty state id');
|
|
611
|
+
}
|
|
612
|
+
const player = isPlainObject(playbookMeta)
|
|
613
|
+
? playbookMeta.player
|
|
614
|
+
: undefined;
|
|
615
|
+
if (typeof player !== 'string' || player.trim().length === 0) {
|
|
616
|
+
throw new TypeError(`player state ${stateId} meta.playbook.player must be a non-empty string`);
|
|
617
|
+
}
|
|
618
|
+
players.set(stateId, player);
|
|
619
|
+
}
|
|
620
|
+
if (isPlainObject(stateDef.states)) {
|
|
621
|
+
for (const [childKey, child] of Object.entries(stateDef.states)) {
|
|
622
|
+
visit(child, childKey);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
const config = machine.config;
|
|
627
|
+
if (isPlainObject(config) && isPlainObject(config.states)) {
|
|
628
|
+
for (const [stateKey, stateDef] of Object.entries(config.states)) {
|
|
629
|
+
visit(stateDef, stateKey);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return players;
|
|
633
|
+
}
|
|
592
634
|
function transitionTargets(transition) {
|
|
593
635
|
const arms = Array.isArray(transition) ? transition : [transition];
|
|
594
636
|
const targets = [];
|
|
@@ -732,16 +774,65 @@ function makeDefaultNormalizeTransitionEvent(transitionEventFields) {
|
|
|
732
774
|
return snapshotJsonValue(out, 'FSM event');
|
|
733
775
|
};
|
|
734
776
|
}
|
|
735
|
-
function
|
|
777
|
+
function snapshotPlayerStateStatuses(value, label, machine, stateDescriptions) {
|
|
778
|
+
if (value === undefined)
|
|
779
|
+
return new Map();
|
|
780
|
+
if (!isPlainObject(value)) {
|
|
781
|
+
throw new TypeError(`${label} playerStates must be an object`);
|
|
782
|
+
}
|
|
783
|
+
const declared = collectPlayerStatePlayers(machine);
|
|
784
|
+
const statuses = new Map();
|
|
785
|
+
for (const [stateId, candidate] of Object.entries(value)) {
|
|
786
|
+
if (!declared.has(stateId)) {
|
|
787
|
+
throw new TypeError(`${label} playerStates.${stateId} does not name a player state`);
|
|
788
|
+
}
|
|
789
|
+
if (!isPlainObject(candidate) ||
|
|
790
|
+
typeof candidate.player !== 'string' ||
|
|
791
|
+
candidate.player.trim().length === 0 ||
|
|
792
|
+
typeof candidate.label !== 'string' ||
|
|
793
|
+
candidate.label.trim().length === 0) {
|
|
794
|
+
throw new TypeError(`${label} playerStates.${stateId} must carry non-empty player and label strings`);
|
|
795
|
+
}
|
|
796
|
+
const expectedLabel = stateDescriptions.get(stateId);
|
|
797
|
+
if (candidate.label !== expectedLabel) {
|
|
798
|
+
throw new TypeError(`${label} playerStates.${stateId}.label must equal its FSM description`);
|
|
799
|
+
}
|
|
800
|
+
if (candidate.player !== declared.get(stateId)) {
|
|
801
|
+
throw new TypeError(`${label} playerStates.${stateId}.player must equal its FSM player`);
|
|
802
|
+
}
|
|
803
|
+
statuses.set(stateId, {
|
|
804
|
+
player: candidate.player,
|
|
805
|
+
label: candidate.label,
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
for (const stateId of declared.keys()) {
|
|
809
|
+
if (!statuses.has(stateId)) {
|
|
810
|
+
throw new TypeError(`${label} playerStates must declare player state ${stateId}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return statuses;
|
|
814
|
+
}
|
|
815
|
+
function settlingGuard(event) {
|
|
816
|
+
if (!isPlainObject(event) || !isPlainObject(event.output))
|
|
817
|
+
return undefined;
|
|
818
|
+
const guard = event.output.guard;
|
|
819
|
+
return typeof guard === 'string' && guard.trim().length > 0
|
|
820
|
+
? guard
|
|
821
|
+
: undefined;
|
|
822
|
+
}
|
|
823
|
+
function legacyStatusesForState(state, context) {
|
|
736
824
|
const stateId = state.stateId;
|
|
737
825
|
if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId))
|
|
738
826
|
return [];
|
|
739
827
|
if (stateId === 'awaitBossReply') {
|
|
740
828
|
const pending = pendingBossQuestionFromContext(context);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
829
|
+
return [
|
|
830
|
+
{
|
|
831
|
+
message: pending === undefined
|
|
832
|
+
? 'Awaiting Boss reply.'
|
|
833
|
+
: `${pending.player} asks: ${pending.question}`,
|
|
834
|
+
},
|
|
835
|
+
];
|
|
745
836
|
}
|
|
746
837
|
if (stateId === 'failed') {
|
|
747
838
|
const lastError = normalizeErrorFull(context.lastError);
|
|
@@ -756,6 +847,53 @@ function defaultStatusesForState(state, context) {
|
|
|
756
847
|
}
|
|
757
848
|
return [{ message: `Entered ${stateId}.` }];
|
|
758
849
|
}
|
|
850
|
+
function makeDefaultStatusesForState(playerStates) {
|
|
851
|
+
return (state, context, event) => {
|
|
852
|
+
const statuses = [];
|
|
853
|
+
const guard = settlingGuard(event);
|
|
854
|
+
if (guard !== undefined)
|
|
855
|
+
statuses.push({ message: `→ ${guard}` });
|
|
856
|
+
const stateId = state.stateId;
|
|
857
|
+
if (stateId === undefined || SUPPRESSED_ENTRY_STATES.has(stateId)) {
|
|
858
|
+
return statuses;
|
|
859
|
+
}
|
|
860
|
+
if (stateId === 'awaitBossReply') {
|
|
861
|
+
const pending = pendingBossQuestionFromContext(context);
|
|
862
|
+
if (pending === undefined) {
|
|
863
|
+
return [...statuses, { message: 'Awaiting Boss reply.' }];
|
|
864
|
+
}
|
|
865
|
+
return [
|
|
866
|
+
...statuses,
|
|
867
|
+
{ message: `${pending.player} asks: ${pending.question}` },
|
|
868
|
+
{
|
|
869
|
+
message: `◆ awaiting Boss reply · ${pending.resumeStateId} · ` +
|
|
870
|
+
`${pending.player} · ${pending.sourceItem}`,
|
|
871
|
+
},
|
|
872
|
+
];
|
|
873
|
+
}
|
|
874
|
+
if (stateId === 'failed') {
|
|
875
|
+
const lastError = normalizeErrorCompact(context.lastError);
|
|
876
|
+
return [
|
|
877
|
+
...statuses,
|
|
878
|
+
{
|
|
879
|
+
message: '◆ workflow failed; awaiting Boss recovery.',
|
|
880
|
+
...(lastError === undefined
|
|
881
|
+
? {}
|
|
882
|
+
: {
|
|
883
|
+
data: snapshotJsonValue({ lastError }, 'failed status data'),
|
|
884
|
+
}),
|
|
885
|
+
},
|
|
886
|
+
];
|
|
887
|
+
}
|
|
888
|
+
const playerState = playerStates.get(stateId);
|
|
889
|
+
if (playerState !== undefined) {
|
|
890
|
+
statuses.push({
|
|
891
|
+
message: `⤷ ${playerState.player}: ${playerState.label}`,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return statuses;
|
|
895
|
+
};
|
|
896
|
+
}
|
|
759
897
|
function classifierState(snapshotOrState) {
|
|
760
898
|
if (snapshotOrState !== null &&
|
|
761
899
|
typeof snapshotOrState === 'object' &&
|
|
@@ -1061,6 +1199,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1061
1199
|
// DR-029: source state descriptions label the control actions the
|
|
1062
1200
|
// runtime advertises through `describe()`.
|
|
1063
1201
|
const stateDescriptions = stateDescriptionsFromMachine(machine);
|
|
1202
|
+
const hasCanonicalStatusProfile = spec.playerStates !== undefined;
|
|
1203
|
+
const playerStates = snapshotPlayerStateStatuses(spec.playerStates, label, machine, stateDescriptions);
|
|
1064
1204
|
// PBRT-52: the artifact's own ControlView context projection. Nothing is
|
|
1065
1205
|
// exported by default, so an FSM context member — including one added
|
|
1066
1206
|
// after this artifact was linked — is private until named here. The two
|
|
@@ -1100,7 +1240,14 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1100
1240
|
const classifyBossText = spec.classifyBossText ?? derivedClassifyBossText;
|
|
1101
1241
|
const normalizeTransitionEvent = spec.normalizeTransitionEvent ??
|
|
1102
1242
|
makeDefaultNormalizeTransitionEvent(spec.transitionEventFields ?? []);
|
|
1103
|
-
const statusesForState = spec.statusesForState ??
|
|
1243
|
+
const statusesForState = spec.statusesForState ??
|
|
1244
|
+
(hasCanonicalStatusProfile
|
|
1245
|
+
? makeDefaultStatusesForState(playerStates)
|
|
1246
|
+
: legacyStatusesForState);
|
|
1247
|
+
const classificationStatus = spec.classificationStatus ??
|
|
1248
|
+
(hasCanonicalStatusProfile
|
|
1249
|
+
? (event) => event.type
|
|
1250
|
+
: () => undefined);
|
|
1104
1251
|
const machineInput = spec.machineInput ?? ((options) => options);
|
|
1105
1252
|
const scriptCwd = spec.scriptCwd ??
|
|
1106
1253
|
((options) => {
|
|
@@ -1138,8 +1285,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1138
1285
|
// DR-029: the last event a public Boss boundary sent into the
|
|
1139
1286
|
// machine — classified, deterministic entry, or Boss reply — kept with
|
|
1140
1287
|
// its recorded payload so a failure-state retry action can replay the
|
|
1141
|
-
// event that drove the run into `failed`. Process-local: the
|
|
1142
|
-
//
|
|
1288
|
+
// event that drove the run into `failed`. Process-local: the durable
|
|
1289
|
+
// runtime snapshot does not persist it (PBRT-50).
|
|
1143
1290
|
let lastBossEvent;
|
|
1144
1291
|
// DR-029: process-local at-most-once `apply` execution — the accepted receipt
|
|
1145
1292
|
// recorded for each idempotency key, returned verbatim on a repeated
|
|
@@ -1159,6 +1306,56 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1159
1306
|
// Inspection callbacks enqueue a complete ordered batch synchronously;
|
|
1160
1307
|
// imperative boundaries await their queued work directly.
|
|
1161
1308
|
let emissionFailure;
|
|
1309
|
+
function selectPlayerResume(playerId) {
|
|
1310
|
+
const selected = session?.playerSessions
|
|
1311
|
+
? session.playerSessions.select(playerId)
|
|
1312
|
+
: playerResumeTokens.get(playerId) ?? false;
|
|
1313
|
+
if (selected !== false &&
|
|
1314
|
+
(typeof selected !== 'string' || selected.trim().length === 0)) {
|
|
1315
|
+
throw new TypeError(`player session store returned an invalid resume token for ${playerId}`);
|
|
1316
|
+
}
|
|
1317
|
+
return selected;
|
|
1318
|
+
}
|
|
1319
|
+
function updatePlayerResume(playerId, resumeToken) {
|
|
1320
|
+
if (session?.playerSessions) {
|
|
1321
|
+
session.playerSessions.update(playerId, resumeToken);
|
|
1322
|
+
}
|
|
1323
|
+
else if (resumeToken !== undefined && resumeToken.trim().length > 0) {
|
|
1324
|
+
playerResumeTokens.set(playerId, resumeToken);
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
playerResumeTokens.delete(playerId);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function snapshotPlayerResumeTokens() {
|
|
1331
|
+
const raw = session?.playerSessions
|
|
1332
|
+
? session.playerSessions.snapshot()
|
|
1333
|
+
: Object.fromEntries(playerResumeTokens);
|
|
1334
|
+
if (!isPlainObject(raw)) {
|
|
1335
|
+
throw new TypeError('player session store snapshot must be an object');
|
|
1336
|
+
}
|
|
1337
|
+
const detached = {};
|
|
1338
|
+
for (const [playerId, token] of Object.entries(raw)) {
|
|
1339
|
+
if (playerId.trim().length === 0) {
|
|
1340
|
+
throw new TypeError('player session store snapshot player ids must be non-empty');
|
|
1341
|
+
}
|
|
1342
|
+
if (typeof token !== 'string' || token.trim().length === 0) {
|
|
1343
|
+
throw new TypeError(`player session store snapshot token for ${playerId} must be a non-empty string`);
|
|
1344
|
+
}
|
|
1345
|
+
detached[playerId] = token;
|
|
1346
|
+
}
|
|
1347
|
+
return detached;
|
|
1348
|
+
}
|
|
1349
|
+
function restorePlayerResumeTokens(tokens) {
|
|
1350
|
+
if (session?.playerSessions) {
|
|
1351
|
+
session.playerSessions.restore(tokens);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
playerResumeTokens.clear();
|
|
1355
|
+
for (const [playerId, token] of Object.entries(tokens)) {
|
|
1356
|
+
playerResumeTokens.set(playerId, token);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1162
1359
|
function enqueueEmission(fn) {
|
|
1163
1360
|
const queued = emissionQueue.add(fn).then(() => undefined);
|
|
1164
1361
|
activeEmissionCalls.add(queued);
|
|
@@ -1315,9 +1512,18 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1315
1512
|
// State-entry telemetry/status must precede the call they describe.
|
|
1316
1513
|
await drainEmissions();
|
|
1317
1514
|
const turnId = activeTurnId;
|
|
1318
|
-
const callId = `player-${++playerCallSequence}`;
|
|
1319
1515
|
const stateId = input.stateId;
|
|
1320
|
-
|
|
1516
|
+
let resume;
|
|
1517
|
+
try {
|
|
1518
|
+
signal.throwIfAborted();
|
|
1519
|
+
resume = selectPlayerResume(playerId);
|
|
1520
|
+
}
|
|
1521
|
+
catch (error) {
|
|
1522
|
+
if (!signal.aborted)
|
|
1523
|
+
controlPlaneError ??= error;
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
const callId = `player-${++playerCallSequence}`;
|
|
1321
1527
|
const identity = {
|
|
1322
1528
|
purpose: 'captain',
|
|
1323
1529
|
...stateIdentity(stateId),
|
|
@@ -1382,12 +1588,22 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
1382
1588
|
}
|
|
1383
1589
|
throw error;
|
|
1384
1590
|
}
|
|
1385
|
-
|
|
1386
|
-
result.resumeToken
|
|
1387
|
-
|
|
1591
|
+
try {
|
|
1592
|
+
updatePlayerResume(playerId, typeof result.resumeToken === 'string' &&
|
|
1593
|
+
result.resumeToken.trim().length > 0
|
|
1594
|
+
? result.resumeToken
|
|
1595
|
+
: undefined);
|
|
1388
1596
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1597
|
+
catch (error) {
|
|
1598
|
+
if (!signal.aborted)
|
|
1599
|
+
controlPlaneError ??= error;
|
|
1600
|
+
try {
|
|
1601
|
+
await emitTrace('player.call.finished', { ...identity, status: 'error', error: normalizeError(error) }, position);
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
// The continuation-store failure remains authoritative.
|
|
1605
|
+
}
|
|
1606
|
+
throw error;
|
|
1391
1607
|
}
|
|
1392
1608
|
await emitTrace('player.call.finished', {
|
|
1393
1609
|
...identity,
|
|
@@ -2208,18 +2424,42 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2208
2424
|
initInFlight = undefined;
|
|
2209
2425
|
}
|
|
2210
2426
|
},
|
|
2211
|
-
// DR-014 §1 / PBRT-45: JSON-safe capture of a parked
|
|
2427
|
+
// DR-014 §1 / DR-031 §5 / PBRT-45: JSON-safe capture of a parked
|
|
2428
|
+
// session, including one already-started suspended nested call.
|
|
2212
2429
|
// Defined only at a safe capture point — initialized, not disposing
|
|
2213
|
-
// or disposed, no active public boundary,
|
|
2214
|
-
//
|
|
2430
|
+
// or disposed, no active public boundary, and the actor quiescent with
|
|
2431
|
+
// status `active`.
|
|
2215
2432
|
exportSnapshot() {
|
|
2216
2433
|
if (!actor || !session || disposed || disposalPromise !== undefined) {
|
|
2217
2434
|
return undefined;
|
|
2218
2435
|
}
|
|
2219
2436
|
if (activeSignal !== undefined)
|
|
2220
2437
|
return undefined;
|
|
2221
|
-
|
|
2438
|
+
const pendingCall = nestedBridge.getPendingCall();
|
|
2439
|
+
const bridgeSuspendedCall = nestedBridge.getSuspendedCall();
|
|
2440
|
+
if ((pendingCall === undefined) !== (bridgeSuspendedCall === undefined)) {
|
|
2222
2441
|
return undefined;
|
|
2442
|
+
}
|
|
2443
|
+
let suspendedCall;
|
|
2444
|
+
if (bridgeSuspendedCall !== undefined) {
|
|
2445
|
+
if (pendingCall?.callId !== bridgeSuspendedCall.callId ||
|
|
2446
|
+
pendingCall?.playbookId !== bridgeSuspendedCall.playbookId ||
|
|
2447
|
+
pendingCall?.childSessionId !== bridgeSuspendedCall.childSessionId) {
|
|
2448
|
+
return undefined;
|
|
2449
|
+
}
|
|
2450
|
+
if (!playbookCallTurnIds.has(bridgeSuspendedCall.callId)) {
|
|
2451
|
+
return undefined;
|
|
2452
|
+
}
|
|
2453
|
+
const turnId = playbookCallTurnIds.get(bridgeSuspendedCall.callId);
|
|
2454
|
+
if (bridgeSuspendedCall.turnId !== undefined &&
|
|
2455
|
+
bridgeSuspendedCall.turnId !== turnId) {
|
|
2456
|
+
return undefined;
|
|
2457
|
+
}
|
|
2458
|
+
suspendedCall = {
|
|
2459
|
+
...bridgeSuspendedCall,
|
|
2460
|
+
...(turnId === undefined ? {} : { turnId }),
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2223
2463
|
const state = currentState();
|
|
2224
2464
|
if (state.status !== 'active' || !state.quiescent)
|
|
2225
2465
|
return undefined;
|
|
@@ -2228,10 +2468,10 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2228
2468
|
.context;
|
|
2229
2469
|
const pending = pendingBossQuestionFromContext(context ?? {});
|
|
2230
2470
|
return {
|
|
2231
|
-
schemaVersion:
|
|
2471
|
+
schemaVersion: 2,
|
|
2232
2472
|
playbookId: session.playbookId,
|
|
2233
2473
|
machine: machineSnapshot,
|
|
2234
|
-
playerResumeTokens:
|
|
2474
|
+
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
2235
2475
|
sequences: {
|
|
2236
2476
|
trace: traceSequence,
|
|
2237
2477
|
turn: turnSequence,
|
|
@@ -2253,6 +2493,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2253
2493
|
sourceItem: pending.sourceItem,
|
|
2254
2494
|
},
|
|
2255
2495
|
],
|
|
2496
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2256
2497
|
};
|
|
2257
2498
|
},
|
|
2258
2499
|
// DR-014 §1 / PBRT-45: alternative to `init` that rehydrates an
|
|
@@ -2265,7 +2506,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2265
2506
|
throw new Error('createPlaybookRuntime.restore: already initialized');
|
|
2266
2507
|
}
|
|
2267
2508
|
const boundSession = snapshotPlaybookSession(nextSession);
|
|
2268
|
-
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId);
|
|
2509
|
+
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId, { allowSuspendedCall: true });
|
|
2510
|
+
const suspendedCall = boundSnapshot.schemaVersion === 2
|
|
2511
|
+
? boundSnapshot.suspendedCall
|
|
2512
|
+
: undefined;
|
|
2513
|
+
let priorExternalPlayerTokens;
|
|
2514
|
+
let externalStoreRestoreAttempted = false;
|
|
2269
2515
|
initialized = true;
|
|
2270
2516
|
let finishInitialization;
|
|
2271
2517
|
const initialization = new Promise((resolve) => {
|
|
@@ -2287,32 +2533,65 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2287
2533
|
// Every Captain call already consumed at least one trace number,
|
|
2288
2534
|
// so the global trace counter is a collision-safe id floor.
|
|
2289
2535
|
boundSnapshot.sequences.trace;
|
|
2290
|
-
// The
|
|
2291
|
-
//
|
|
2292
|
-
//
|
|
2293
|
-
//
|
|
2536
|
+
// The runtime snapshot carries no apply counter (PBRT-50); every
|
|
2537
|
+
// apply boundary consumed trace numbers, so the persisted trace
|
|
2538
|
+
// counter is a collision-safe id floor here too, keeping
|
|
2539
|
+
// `apply-<n>` call ids unique across restore.
|
|
2294
2540
|
applyCallSequence = boundSnapshot.sequences.trace;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2541
|
+
if (boundSession.playerSessions) {
|
|
2542
|
+
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
2543
|
+
externalStoreRestoreAttempted = true;
|
|
2544
|
+
}
|
|
2545
|
+
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
2546
|
+
nestedBridge.prepareRestore(suspendedCall);
|
|
2547
|
+
if (suspendedCall !== undefined) {
|
|
2548
|
+
playbookCallTurnIds.set(suspendedCall.callId, suspendedCall.turnId);
|
|
2298
2549
|
}
|
|
2299
2550
|
suppressInspectionEmissions = true;
|
|
2300
2551
|
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
2301
2552
|
actor.start();
|
|
2302
|
-
|
|
2553
|
+
if (controlPlaneError !== undefined)
|
|
2554
|
+
throw controlPlaneError;
|
|
2555
|
+
const restoredState = normalizePlaybookSnapshot(actor.getSnapshot(), suspendedCall === undefined
|
|
2556
|
+
? {}
|
|
2557
|
+
: {
|
|
2558
|
+
pendingCall: {
|
|
2559
|
+
callId: suspendedCall.callId,
|
|
2560
|
+
playbookId: suspendedCall.playbookId,
|
|
2561
|
+
childSessionId: suspendedCall.childSessionId,
|
|
2562
|
+
},
|
|
2563
|
+
});
|
|
2303
2564
|
if (restoredState.status !== 'active') {
|
|
2304
2565
|
throw new Error(`createPlaybookRuntime.restore: restored actor status is ${restoredState.status}, expected active`);
|
|
2305
2566
|
}
|
|
2306
|
-
|
|
2567
|
+
if (stableJson(restoredState, 'restored runtime state') !==
|
|
2568
|
+
stableJson(boundSnapshot.state, 'runtime snapshot state')) {
|
|
2569
|
+
throw new Error('createPlaybookRuntime.restore: restored actor state does not match snapshot state');
|
|
2570
|
+
}
|
|
2307
2571
|
priorState = restoredState;
|
|
2308
2572
|
await drainEmissions();
|
|
2573
|
+
suppressInspectionEmissions = false;
|
|
2574
|
+
// Final fallible step: after this publication the authoritative
|
|
2575
|
+
// child has rejoined ordinary resume/abort ownership, so no later
|
|
2576
|
+
// restore validation may trigger failed-start rollback.
|
|
2577
|
+
nestedBridge.confirmRestore();
|
|
2309
2578
|
})();
|
|
2310
2579
|
try {
|
|
2311
2580
|
await initTask;
|
|
2312
2581
|
}
|
|
2313
2582
|
catch (error) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2583
|
+
let failure = error;
|
|
2584
|
+
if (externalStoreRestoreAttempted &&
|
|
2585
|
+
priorExternalPlayerTokens !== undefined) {
|
|
2586
|
+
try {
|
|
2587
|
+
boundSession.playerSessions.restore(priorExternalPlayerTokens);
|
|
2588
|
+
}
|
|
2589
|
+
catch (rollbackError) {
|
|
2590
|
+
failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
2594
|
+
throw failure;
|
|
2316
2595
|
}
|
|
2317
2596
|
finally {
|
|
2318
2597
|
finishInitialization();
|
|
@@ -2662,7 +2941,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2662
2941
|
else {
|
|
2663
2942
|
// 2. Optional Captain-pane classification line: the bare FSM
|
|
2664
2943
|
// event type, emitted before the FSM advances.
|
|
2665
|
-
const statusLine =
|
|
2944
|
+
const statusLine = classificationStatus(event);
|
|
2666
2945
|
if (statusLine !== undefined) {
|
|
2667
2946
|
await runtimePorts.emitStatus(statusLine);
|
|
2668
2947
|
}
|
|
@@ -2843,7 +3122,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2843
3122
|
}
|
|
2844
3123
|
}
|
|
2845
3124
|
finally {
|
|
2846
|
-
|
|
3125
|
+
// A composing host owns the shared store for the complete root
|
|
3126
|
+
// engagement tree. Child disposal must not erase a token its
|
|
3127
|
+
// caller will resume. The private fallback remains runtime-owned.
|
|
3128
|
+
if (session?.playerSessions === undefined) {
|
|
3129
|
+
playerResumeTokens.clear();
|
|
3130
|
+
}
|
|
2847
3131
|
activePlayerIds.clear();
|
|
2848
3132
|
playbookCallTurnIds.clear();
|
|
2849
3133
|
activeEmissionCalls.clear();
|