@sublang/playbook 5.0.0 → 6.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 +11 -7
- package/docs/cli.md +38 -35
- package/docs/configuration.md +58 -15
- package/docs/embedding.md +24 -16
- package/package.json +40 -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/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.js +93 -15
- package/reference/sdlc/code.playbook/playbook-captain.ts +115 -19
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +33 -22
- 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} +471 -362
- package/reference/sdlc/{discuss.playbook/discuss.playbook.ts → decide.playbook/decide.playbook.ts} +575 -443
- 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 +48 -2
- package/slc/text2gears.md +22 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +9 -2
- package/src/xstate-playbook-runtime.js +255 -21
- package/src/xstate-playbook-runtime.ts +333 -28
- package/src/xstate-runtime.js +25 -0
- package/src/xstate-runtime.ts +51 -0
- 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) => {
|
|
@@ -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,
|
|
@@ -2231,7 +2447,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2231
2447
|
schemaVersion: 1,
|
|
2232
2448
|
playbookId: session.playbookId,
|
|
2233
2449
|
machine: machineSnapshot,
|
|
2234
|
-
playerResumeTokens:
|
|
2450
|
+
playerResumeTokens: snapshotPlayerResumeTokens(),
|
|
2235
2451
|
sequences: {
|
|
2236
2452
|
trace: traceSequence,
|
|
2237
2453
|
turn: turnSequence,
|
|
@@ -2266,6 +2482,8 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2266
2482
|
}
|
|
2267
2483
|
const boundSession = snapshotPlaybookSession(nextSession);
|
|
2268
2484
|
const boundSnapshot = assertPlaybookRuntimeSnapshot(snapshot, boundSession.playbookId);
|
|
2485
|
+
let priorExternalPlayerTokens;
|
|
2486
|
+
let externalStoreRestoreAttempted = false;
|
|
2269
2487
|
initialized = true;
|
|
2270
2488
|
let finishInitialization;
|
|
2271
2489
|
const initialization = new Promise((resolve) => {
|
|
@@ -2292,10 +2510,11 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2292
2510
|
// the persisted trace counter is a collision-safe id floor here
|
|
2293
2511
|
// too, keeping `apply-<n>` call ids unique across restore.
|
|
2294
2512
|
applyCallSequence = boundSnapshot.sequences.trace;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2513
|
+
if (boundSession.playerSessions) {
|
|
2514
|
+
priorExternalPlayerTokens = snapshotPlayerResumeTokens();
|
|
2515
|
+
externalStoreRestoreAttempted = true;
|
|
2298
2516
|
}
|
|
2517
|
+
restorePlayerResumeTokens(boundSnapshot.playerResumeTokens);
|
|
2299
2518
|
suppressInspectionEmissions = true;
|
|
2300
2519
|
actor = buildActor(runtimePorts, boundSnapshot.machine);
|
|
2301
2520
|
actor.start();
|
|
@@ -2311,8 +2530,18 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2311
2530
|
await initTask;
|
|
2312
2531
|
}
|
|
2313
2532
|
catch (error) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2533
|
+
let failure = error;
|
|
2534
|
+
if (externalStoreRestoreAttempted &&
|
|
2535
|
+
priorExternalPlayerTokens !== undefined) {
|
|
2536
|
+
try {
|
|
2537
|
+
boundSession.playerSessions.restore(priorExternalPlayerTokens);
|
|
2538
|
+
}
|
|
2539
|
+
catch (rollbackError) {
|
|
2540
|
+
failure = new AggregateError([error, rollbackError], 'createPlaybookRuntime.restore and player continuation rollback failed');
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
await cleanupFailedStart(failure, { emitDisposal: false });
|
|
2544
|
+
throw failure;
|
|
2316
2545
|
}
|
|
2317
2546
|
finally {
|
|
2318
2547
|
finishInitialization();
|
|
@@ -2662,7 +2891,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2662
2891
|
else {
|
|
2663
2892
|
// 2. Optional Captain-pane classification line: the bare FSM
|
|
2664
2893
|
// event type, emitted before the FSM advances.
|
|
2665
|
-
const statusLine =
|
|
2894
|
+
const statusLine = classificationStatus(event);
|
|
2666
2895
|
if (statusLine !== undefined) {
|
|
2667
2896
|
await runtimePorts.emitStatus(statusLine);
|
|
2668
2897
|
}
|
|
@@ -2843,7 +3072,12 @@ export function createXStatePlaybookRuntime(machine, spec) {
|
|
|
2843
3072
|
}
|
|
2844
3073
|
}
|
|
2845
3074
|
finally {
|
|
2846
|
-
|
|
3075
|
+
// A composing host owns the shared store for the complete root
|
|
3076
|
+
// engagement tree. Child disposal must not erase a token its
|
|
3077
|
+
// caller will resume. The private fallback remains runtime-owned.
|
|
3078
|
+
if (session?.playerSessions === undefined) {
|
|
3079
|
+
playerResumeTokens.clear();
|
|
3080
|
+
}
|
|
2847
3081
|
activePlayerIds.clear();
|
|
2848
3082
|
playbookCallTurnIds.clear();
|
|
2849
3083
|
activeEmissionCalls.clear();
|