@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
package/src/xstate-runtime.js
CHANGED
|
@@ -248,6 +248,26 @@ function capturedPort(descriptors, name) {
|
|
|
248
248
|
}
|
|
249
249
|
return value;
|
|
250
250
|
}
|
|
251
|
+
function capturedSessionStore(descriptors) {
|
|
252
|
+
const captured = capturedDataValue(descriptors, 'playerSessions', 'playbook session playerSessions');
|
|
253
|
+
if (!isRecord(captured)) {
|
|
254
|
+
throw new TypeError('playbook session playerSessions must be an object');
|
|
255
|
+
}
|
|
256
|
+
const storeDescriptors = Object.getOwnPropertyDescriptors(captured);
|
|
257
|
+
const method = (name) => {
|
|
258
|
+
const value = capturedDataValue(storeDescriptors, name, `playbook session playerSessions.${name}`);
|
|
259
|
+
if (typeof value !== 'function') {
|
|
260
|
+
throw new TypeError(`playbook session playerSessions.${name} must be a function`);
|
|
261
|
+
}
|
|
262
|
+
return value;
|
|
263
|
+
};
|
|
264
|
+
return Object.freeze({
|
|
265
|
+
select: method('select'),
|
|
266
|
+
update: method('update'),
|
|
267
|
+
snapshot: method('snapshot'),
|
|
268
|
+
restore: method('restore'),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
251
271
|
/** Validate session causality and detach its immutable identity from the host. */
|
|
252
272
|
export function snapshotPlaybookSession(session) {
|
|
253
273
|
if (!isRecord(session)) {
|
|
@@ -266,6 +286,7 @@ export function snapshotPlaybookSession(session) {
|
|
|
266
286
|
const capturedParentCallId = capturedDataValue(sessionDescriptors, 'parentCallId', 'playbook session parentCallId', false);
|
|
267
287
|
const hasParentSessionId = Object.prototype.hasOwnProperty.call(sessionDescriptors, 'parentSessionId');
|
|
268
288
|
const hasParentCallId = Object.prototype.hasOwnProperty.call(sessionDescriptors, 'parentCallId');
|
|
289
|
+
const hasPlayerSessions = Object.prototype.hasOwnProperty.call(sessionDescriptors, 'playerSessions');
|
|
269
290
|
let parentSessionId;
|
|
270
291
|
let parentCallId;
|
|
271
292
|
if (depth === 0) {
|
|
@@ -296,6 +317,9 @@ export function snapshotPlaybookSession(session) {
|
|
|
296
317
|
emitStatus: capturedPort(portDescriptors, 'emitStatus'),
|
|
297
318
|
emitTelemetry: capturedPort(portDescriptors, 'emitTelemetry'),
|
|
298
319
|
});
|
|
320
|
+
const playerSessions = hasPlayerSessions
|
|
321
|
+
? capturedSessionStore(sessionDescriptors)
|
|
322
|
+
: undefined;
|
|
299
323
|
return Object.freeze({
|
|
300
324
|
sessionId,
|
|
301
325
|
playbookId,
|
|
@@ -303,6 +327,7 @@ export function snapshotPlaybookSession(session) {
|
|
|
303
327
|
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
304
328
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
305
329
|
depth,
|
|
330
|
+
...(playerSessions === undefined ? {} : { playerSessions }),
|
|
306
331
|
ports,
|
|
307
332
|
});
|
|
308
333
|
}
|
|
@@ -501,58 +526,135 @@ const SNAPSHOT_SEQUENCE_KEYS = [
|
|
|
501
526
|
'playerCall',
|
|
502
527
|
'playbookCall',
|
|
503
528
|
];
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
529
|
+
function snapshotSuspendedCall(value, path = 'runtime snapshot suspendedCall') {
|
|
530
|
+
const captured = snapshotJsonValue(value, path);
|
|
531
|
+
if (!isRecord(captured)) {
|
|
532
|
+
throw new TypeError(`${path} must be an object`);
|
|
533
|
+
}
|
|
534
|
+
rejectUnknownKeys(captured, ['callId', 'stateId', 'playbookId', 'text', 'childSessionId', 'turnId'], path);
|
|
535
|
+
const call = {
|
|
536
|
+
callId: requireNonEmptyString(captured.callId, `${path}.callId`),
|
|
537
|
+
stateId: requireNonEmptyString(captured.stateId, `${path}.stateId`),
|
|
538
|
+
playbookId: requireNonEmptyString(captured.playbookId, `${path}.playbookId`),
|
|
539
|
+
text: requireNonEmptyString(captured.text, `${path}.text`),
|
|
540
|
+
childSessionId: requireNonEmptyString(captured.childSessionId, `${path}.childSessionId`),
|
|
541
|
+
};
|
|
542
|
+
if (own(captured, 'turnId')) {
|
|
543
|
+
if (!Number.isSafeInteger(captured.turnId) ||
|
|
544
|
+
captured.turnId <= 0) {
|
|
545
|
+
throw new TypeError(`${path}.turnId must be a positive integer`);
|
|
546
|
+
}
|
|
547
|
+
call.turnId = captured.turnId;
|
|
548
|
+
}
|
|
549
|
+
return Object.freeze(call);
|
|
550
|
+
}
|
|
551
|
+
// DR-014 §1 / DR-031 §5: validate and detach a host-supplied runtime
|
|
552
|
+
// snapshot before restore touches any state. A suspended schema-2 call is
|
|
553
|
+
// rejected unless the restore path explicitly promises to seed and claim it.
|
|
554
|
+
export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options = {}) {
|
|
555
|
+
const snapshot = snapshotJsonValue(value, 'runtime snapshot');
|
|
556
|
+
if (!isRecord(snapshot)) {
|
|
509
557
|
throw new TypeError('runtime snapshot must be an object');
|
|
510
558
|
}
|
|
511
|
-
|
|
512
|
-
|
|
559
|
+
const capturedOptions = snapshotJsonValue(options, 'runtime snapshot validation options');
|
|
560
|
+
if (!isRecord(capturedOptions)) {
|
|
561
|
+
throw new TypeError('runtime snapshot validation options must be an object');
|
|
562
|
+
}
|
|
563
|
+
rejectUnknownKeys(capturedOptions, ['allowSuspendedCall'], 'runtime snapshot validation options');
|
|
564
|
+
if (capturedOptions.allowSuspendedCall !== undefined &&
|
|
565
|
+
typeof capturedOptions.allowSuspendedCall !== 'boolean') {
|
|
566
|
+
throw new TypeError('runtime snapshot validation options.allowSuspendedCall must be boolean');
|
|
567
|
+
}
|
|
568
|
+
const allowSuspendedCall = capturedOptions.allowSuspendedCall ?? false;
|
|
569
|
+
if (snapshot.schemaVersion !== 1 && snapshot.schemaVersion !== 2) {
|
|
570
|
+
throw new TypeError(`runtime snapshot schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 1 or 2)`);
|
|
571
|
+
}
|
|
572
|
+
const schemaVersion = snapshot.schemaVersion;
|
|
573
|
+
rejectUnknownKeys(snapshot, [
|
|
574
|
+
'schemaVersion',
|
|
575
|
+
'playbookId',
|
|
576
|
+
'machine',
|
|
577
|
+
'playerResumeTokens',
|
|
578
|
+
'sequences',
|
|
579
|
+
'state',
|
|
580
|
+
'pendingBossQuestions',
|
|
581
|
+
'suspendedCall',
|
|
582
|
+
], 'runtime snapshot');
|
|
583
|
+
if (schemaVersion === 1 && own(snapshot, 'suspendedCall')) {
|
|
584
|
+
throw new TypeError('runtime snapshot schemaVersion 1 must not carry suspendedCall');
|
|
585
|
+
}
|
|
586
|
+
let suspendedCall;
|
|
587
|
+
if (schemaVersion === 2 && own(snapshot, 'suspendedCall')) {
|
|
588
|
+
suspendedCall = snapshotSuspendedCall(snapshot.suspendedCall);
|
|
589
|
+
if (!allowSuspendedCall) {
|
|
590
|
+
throw new TypeError('runtime snapshot suspendedCall requires a restore path that explicitly allows it');
|
|
591
|
+
}
|
|
513
592
|
}
|
|
514
|
-
const playbookId = requireNonEmptyString(
|
|
593
|
+
const playbookId = requireNonEmptyString(snapshot.playbookId, 'runtime snapshot playbookId');
|
|
515
594
|
if (playbookId !== expectedPlaybookId) {
|
|
516
595
|
throw new TypeError(`runtime snapshot playbookId ${playbookId} does not match runtime playbook ${expectedPlaybookId}`);
|
|
517
596
|
}
|
|
518
|
-
if (!isRecord(
|
|
597
|
+
if (!isRecord(snapshot.machine)) {
|
|
519
598
|
throw new TypeError('runtime snapshot machine must be an object');
|
|
520
599
|
}
|
|
521
|
-
const machine =
|
|
522
|
-
if (!isRecord(
|
|
600
|
+
const machine = snapshot.machine;
|
|
601
|
+
if (!isRecord(snapshot.playerResumeTokens)) {
|
|
523
602
|
throw new TypeError('runtime snapshot playerResumeTokens must be an object');
|
|
524
603
|
}
|
|
525
604
|
const playerResumeTokens = {};
|
|
526
|
-
for (const [playerId, token] of Object.entries(
|
|
605
|
+
for (const [playerId, token] of Object.entries(snapshot.playerResumeTokens)) {
|
|
527
606
|
defineEnumerableDataProperty(playerResumeTokens, playerId, requireNonEmptyString(token, `runtime snapshot playerResumeTokens.${playerId}`));
|
|
528
607
|
}
|
|
529
|
-
if (!isRecord(
|
|
608
|
+
if (!isRecord(snapshot.sequences)) {
|
|
530
609
|
throw new TypeError('runtime snapshot sequences must be an object');
|
|
531
610
|
}
|
|
611
|
+
rejectUnknownKeys(snapshot.sequences, [...SNAPSHOT_SEQUENCE_KEYS, 'captainCall'], 'runtime snapshot sequences');
|
|
532
612
|
const sequences = {};
|
|
533
613
|
for (const key of SNAPSHOT_SEQUENCE_KEYS) {
|
|
534
|
-
const sequence =
|
|
614
|
+
const sequence = snapshot.sequences[key];
|
|
535
615
|
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
536
616
|
throw new TypeError(`runtime snapshot sequences.${key} must be a non-negative integer`);
|
|
537
617
|
}
|
|
538
618
|
sequences[key] = sequence;
|
|
539
619
|
}
|
|
540
|
-
const captainCall =
|
|
620
|
+
const captainCall = snapshot.sequences.captainCall;
|
|
541
621
|
if (captainCall !== undefined) {
|
|
542
622
|
if (!Number.isSafeInteger(captainCall) || captainCall < 0) {
|
|
543
623
|
throw new TypeError('runtime snapshot sequences.captainCall must be a non-negative integer');
|
|
544
624
|
}
|
|
545
625
|
sequences.captainCall = captainCall;
|
|
546
626
|
}
|
|
547
|
-
validateState(
|
|
548
|
-
const state =
|
|
549
|
-
if (
|
|
627
|
+
validateState(snapshot.state, 'runtime snapshot state');
|
|
628
|
+
const state = snapshot.state;
|
|
629
|
+
if (state.tags.includes(SUSPENDED_TAG) && suspendedCall === undefined) {
|
|
630
|
+
throw new TypeError(`runtime snapshot state tagged ${SUSPENDED_TAG} requires schemaVersion 2 suspendedCall`);
|
|
631
|
+
}
|
|
632
|
+
if (suspendedCall) {
|
|
633
|
+
if (sequences.playbookCall === 0) {
|
|
634
|
+
throw new TypeError('runtime snapshot suspendedCall requires sequences.playbookCall greater than zero');
|
|
635
|
+
}
|
|
636
|
+
if (suspendedCall.turnId !== undefined &&
|
|
637
|
+
suspendedCall.turnId > sequences.turn) {
|
|
638
|
+
throw new TypeError('runtime snapshot suspendedCall.turnId must not exceed sequences.turn');
|
|
639
|
+
}
|
|
640
|
+
if (state.status !== 'active' || !state.quiescent) {
|
|
641
|
+
throw new TypeError('runtime snapshot suspendedCall requires an active quiescent state');
|
|
642
|
+
}
|
|
643
|
+
if (!state.tags.includes(SUSPENDED_TAG)) {
|
|
644
|
+
throw new TypeError(`runtime snapshot suspendedCall requires state tag ${SUSPENDED_TAG}`);
|
|
645
|
+
}
|
|
646
|
+
if (!state.activeStateIds.includes(suspendedCall.stateId)) {
|
|
647
|
+
throw new TypeError('runtime snapshot suspendedCall.stateId must be active in snapshot state');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (!Array.isArray(snapshot.pendingBossQuestions)) {
|
|
550
651
|
throw new TypeError('runtime snapshot pendingBossQuestions must be an array');
|
|
551
652
|
}
|
|
552
|
-
const pendingBossQuestions =
|
|
653
|
+
const pendingBossQuestions = snapshot.pendingBossQuestions.map((entry, index) => {
|
|
553
654
|
const path = `runtime snapshot pendingBossQuestions[${index}]`;
|
|
554
655
|
if (!isRecord(entry))
|
|
555
656
|
throw new TypeError(`${path} must be an object`);
|
|
657
|
+
rejectUnknownKeys(entry, ['questionId', 'player', 'question', 'sourceItem'], path);
|
|
556
658
|
const question = {
|
|
557
659
|
questionId: requireNonEmptyString(entry.questionId, `${path}.questionId`),
|
|
558
660
|
player: requireNonEmptyString(entry.player, `${path}.player`),
|
|
@@ -565,14 +667,21 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId) {
|
|
|
565
667
|
};
|
|
566
668
|
return Object.freeze(question);
|
|
567
669
|
});
|
|
568
|
-
|
|
569
|
-
schemaVersion: 1,
|
|
670
|
+
const fields = {
|
|
570
671
|
playbookId,
|
|
571
672
|
machine,
|
|
572
673
|
playerResumeTokens: Object.freeze(playerResumeTokens),
|
|
573
674
|
sequences: Object.freeze(sequences),
|
|
574
675
|
state,
|
|
575
676
|
pendingBossQuestions: Object.freeze(pendingBossQuestions),
|
|
677
|
+
};
|
|
678
|
+
if (schemaVersion === 1) {
|
|
679
|
+
return Object.freeze({ schemaVersion: 1, ...fields });
|
|
680
|
+
}
|
|
681
|
+
return Object.freeze({
|
|
682
|
+
schemaVersion: 2,
|
|
683
|
+
...fields,
|
|
684
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
576
685
|
});
|
|
577
686
|
}
|
|
578
687
|
export class NestedPlaybookCallError extends Error {
|
|
@@ -796,6 +905,7 @@ function outputOrThrow(result) {
|
|
|
796
905
|
}
|
|
797
906
|
export function createNestedPlaybookBridge(options) {
|
|
798
907
|
let current;
|
|
908
|
+
let restoreMode;
|
|
799
909
|
let disposed = false;
|
|
800
910
|
const usedCallIds = new Set();
|
|
801
911
|
const pendingListeners = new Set();
|
|
@@ -828,10 +938,29 @@ export function createNestedPlaybookBridge(options) {
|
|
|
828
938
|
childSessionId: active.childSessionId,
|
|
829
939
|
}
|
|
830
940
|
: undefined;
|
|
831
|
-
const
|
|
941
|
+
const suspendedIdentity = (active) => active?.phase === 'suspended' && active.childSessionId
|
|
942
|
+
? Object.freeze({
|
|
943
|
+
callId: active.callId,
|
|
944
|
+
stateId: active.input.stateId,
|
|
945
|
+
playbookId: active.input.playbookId,
|
|
946
|
+
text: active.input.text,
|
|
947
|
+
childSessionId: active.childSessionId,
|
|
948
|
+
...(active.turnId === undefined ? {} : { turnId: active.turnId }),
|
|
949
|
+
})
|
|
950
|
+
: undefined;
|
|
951
|
+
const failRestoreMode = (mode, error) => {
|
|
952
|
+
mode.state = 'failed';
|
|
953
|
+
mode.error = error;
|
|
954
|
+
reportControlPlaneError(error);
|
|
955
|
+
};
|
|
956
|
+
const detachAbortListener = (active) => {
|
|
832
957
|
if (active.abortListener) {
|
|
833
958
|
active.signal.removeEventListener('abort', active.abortListener);
|
|
959
|
+
active.abortListener = undefined;
|
|
834
960
|
}
|
|
961
|
+
};
|
|
962
|
+
const clear = (active) => {
|
|
963
|
+
detachAbortListener(active);
|
|
835
964
|
if (current === active)
|
|
836
965
|
current = undefined;
|
|
837
966
|
};
|
|
@@ -947,23 +1076,171 @@ export function createNestedPlaybookBridge(options) {
|
|
|
947
1076
|
throw error;
|
|
948
1077
|
}
|
|
949
1078
|
};
|
|
1079
|
+
const rollbackRestoredCall = (mode, error) => {
|
|
1080
|
+
const active = mode.active;
|
|
1081
|
+
mode.state = 'failed';
|
|
1082
|
+
mode.error = error;
|
|
1083
|
+
mode.active = undefined;
|
|
1084
|
+
if (!active)
|
|
1085
|
+
return undefined;
|
|
1086
|
+
active.phase = 'settling';
|
|
1087
|
+
active.restoreRolledBack = true;
|
|
1088
|
+
clear(active);
|
|
1089
|
+
usedCallIds.delete(active.callId);
|
|
1090
|
+
active.deferred.reject(error);
|
|
1091
|
+
return active;
|
|
1092
|
+
};
|
|
1093
|
+
const publishSuspendedCall = (active) => {
|
|
1094
|
+
if (active.phase !== 'suspended') {
|
|
1095
|
+
throw new Error(`playbook call ${active.callId} is not suspended`);
|
|
1096
|
+
}
|
|
1097
|
+
const abortListener = () => {
|
|
1098
|
+
if (active.phase !== 'suspended')
|
|
1099
|
+
return;
|
|
1100
|
+
const result = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason ?? new Error('Nested playbook invocation aborted'), true);
|
|
1101
|
+
void settlePending(active, result).catch((error) => {
|
|
1102
|
+
reportBackgroundError(error);
|
|
1103
|
+
});
|
|
1104
|
+
};
|
|
1105
|
+
active.abortListener = abortListener;
|
|
1106
|
+
active.signal.addEventListener('abort', abortListener, { once: true });
|
|
1107
|
+
const pendingCall = pendingIdentity(active);
|
|
1108
|
+
if (!pendingCall) {
|
|
1109
|
+
throw new Error('suspended call identity was not recorded');
|
|
1110
|
+
}
|
|
1111
|
+
for (const listener of pendingListeners) {
|
|
1112
|
+
try {
|
|
1113
|
+
listener(pendingCall);
|
|
1114
|
+
}
|
|
1115
|
+
catch (error) {
|
|
1116
|
+
reportBackgroundError(error);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (active.signal.aborted)
|
|
1120
|
+
abortListener();
|
|
1121
|
+
};
|
|
1122
|
+
const waitOnSuspendedCall = async (active) => {
|
|
1123
|
+
publishSuspendedCall(active);
|
|
1124
|
+
return await active.deferred.promise;
|
|
1125
|
+
};
|
|
950
1126
|
const actorLogic = fromPromise(async ({ input, signal: invocationSignal }) => {
|
|
951
1127
|
if (disposed) {
|
|
952
1128
|
rejectControlPlane(new Error('nested playbook bridge is disposed'));
|
|
953
1129
|
}
|
|
1130
|
+
const normalizedInput = (() => {
|
|
1131
|
+
try {
|
|
1132
|
+
return {
|
|
1133
|
+
stateId: requireNonEmptyString(input.stateId, 'playbook input stateId'),
|
|
1134
|
+
playbookId: requireNonEmptyString(input.playbookId, 'playbook input playbookId'),
|
|
1135
|
+
text: requireNonEmptyString(input.text, 'playbook input text'),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
const mode = restoreMode;
|
|
1140
|
+
if (mode) {
|
|
1141
|
+
if (mode.state === 'claimed') {
|
|
1142
|
+
rollbackRestoredCall(mode, error);
|
|
1143
|
+
reportControlPlaneError(error);
|
|
1144
|
+
}
|
|
1145
|
+
else
|
|
1146
|
+
failRestoreMode(mode, error);
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
1149
|
+
return rejectControlPlane(error);
|
|
1150
|
+
}
|
|
1151
|
+
})();
|
|
1152
|
+
const mode = restoreMode;
|
|
1153
|
+
if (mode) {
|
|
1154
|
+
if (mode.state !== 'armed') {
|
|
1155
|
+
const callId = mode.call?.callId ?? 'without a descriptor';
|
|
1156
|
+
const error = new Error(mode.state === 'claimed'
|
|
1157
|
+
? `restored playbook call ${callId} was claimed more than once`
|
|
1158
|
+
: `restored playbook call ${callId} is no longer claimable`);
|
|
1159
|
+
if (mode.state === 'claimed')
|
|
1160
|
+
rollbackRestoredCall(mode, error);
|
|
1161
|
+
else
|
|
1162
|
+
mode.error ??= error;
|
|
1163
|
+
reportControlPlaneError(error);
|
|
1164
|
+
throw error;
|
|
1165
|
+
}
|
|
1166
|
+
const seed = mode.call;
|
|
1167
|
+
if (!seed) {
|
|
1168
|
+
const error = new Error('restored machine invoked a nested playbook without a suspendedCall descriptor');
|
|
1169
|
+
failRestoreMode(mode, error);
|
|
1170
|
+
throw error;
|
|
1171
|
+
}
|
|
1172
|
+
for (const field of ['stateId', 'playbookId', 'text']) {
|
|
1173
|
+
if (normalizedInput[field] !== seed[field]) {
|
|
1174
|
+
const error = new Error(`restored playbook call ${seed.callId} ${field} does not match its persisted input`);
|
|
1175
|
+
failRestoreMode(mode, error);
|
|
1176
|
+
throw error;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (usedCallIds.has(seed.callId)) {
|
|
1180
|
+
const error = new Error(`restored duplicate playbook call id ${seed.callId}`);
|
|
1181
|
+
failRestoreMode(mode, error);
|
|
1182
|
+
throw error;
|
|
1183
|
+
}
|
|
1184
|
+
const controller = new AbortController();
|
|
1185
|
+
let callSignal;
|
|
1186
|
+
try {
|
|
1187
|
+
callSignal = combineAbortSignals(invocationSignal, options.getBoundarySignal?.(), controller.signal);
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
failRestoreMode(mode, error);
|
|
1191
|
+
throw error;
|
|
1192
|
+
}
|
|
1193
|
+
const active = {
|
|
1194
|
+
callId: seed.callId,
|
|
1195
|
+
input: normalizedInput,
|
|
1196
|
+
...(seed.turnId === undefined
|
|
1197
|
+
? {}
|
|
1198
|
+
: { turnId: seed.turnId }),
|
|
1199
|
+
deferred: deferred(),
|
|
1200
|
+
finished: deferred(),
|
|
1201
|
+
controller,
|
|
1202
|
+
signal: callSignal,
|
|
1203
|
+
phase: 'restoring',
|
|
1204
|
+
childSessionId: seed.childSessionId,
|
|
1205
|
+
};
|
|
1206
|
+
usedCallIds.add(active.callId);
|
|
1207
|
+
current = active;
|
|
1208
|
+
mode.state = 'claimed';
|
|
1209
|
+
mode.active = active;
|
|
1210
|
+
const restoreAbortListener = () => {
|
|
1211
|
+
if (restoreMode !== mode ||
|
|
1212
|
+
mode.state !== 'claimed' ||
|
|
1213
|
+
mode.active !== active ||
|
|
1214
|
+
active.phase !== 'restoring') {
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
rollbackRestoredCall(mode, active.signal.reason ??
|
|
1218
|
+
new Error('Restored nested playbook invocation aborted'));
|
|
1219
|
+
};
|
|
1220
|
+
active.abortListener = restoreAbortListener;
|
|
1221
|
+
active.signal.addEventListener('abort', restoreAbortListener, {
|
|
1222
|
+
once: true,
|
|
1223
|
+
});
|
|
1224
|
+
if (active.signal.aborted)
|
|
1225
|
+
restoreAbortListener();
|
|
1226
|
+
try {
|
|
1227
|
+
return await active.deferred.promise;
|
|
1228
|
+
}
|
|
1229
|
+
catch (error) {
|
|
1230
|
+
if (!active.restoreRolledBack)
|
|
1231
|
+
active.runError = error;
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
finally {
|
|
1235
|
+
active.finished.resolve(undefined);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
954
1238
|
if (current) {
|
|
955
1239
|
rejectControlPlane(new Error(`playbook call ${current.callId} is already outstanding`));
|
|
956
1240
|
}
|
|
957
|
-
const
|
|
1241
|
+
const callId = (() => {
|
|
958
1242
|
try {
|
|
959
|
-
return
|
|
960
|
-
{
|
|
961
|
-
stateId: requireNonEmptyString(input.stateId, 'playbook input stateId'),
|
|
962
|
-
playbookId: requireNonEmptyString(input.playbookId, 'playbook input playbookId'),
|
|
963
|
-
text: requireNonEmptyString(input.text, 'playbook input text'),
|
|
964
|
-
},
|
|
965
|
-
requireNonEmptyString(options.nextCallId(), 'allocated playbook call id'),
|
|
966
|
-
];
|
|
1243
|
+
return requireNonEmptyString(options.nextCallId(), 'allocated playbook call id');
|
|
967
1244
|
}
|
|
968
1245
|
catch (error) {
|
|
969
1246
|
return rejectControlPlane(error);
|
|
@@ -1094,32 +1371,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1094
1371
|
}
|
|
1095
1372
|
active.phase = 'suspended';
|
|
1096
1373
|
active.childSessionId = start.childSessionId;
|
|
1097
|
-
|
|
1098
|
-
if (active.phase !== 'suspended')
|
|
1099
|
-
return;
|
|
1100
|
-
const result = resultFromThrown(active.input.playbookId, active.childSessionId, active.signal.reason ??
|
|
1101
|
-
new Error('Nested playbook invocation aborted'), true);
|
|
1102
|
-
void settlePending(active, result).catch((error) => {
|
|
1103
|
-
reportBackgroundError(error);
|
|
1104
|
-
});
|
|
1105
|
-
};
|
|
1106
|
-
active.abortListener = abortListener;
|
|
1107
|
-
active.signal.addEventListener('abort', abortListener, { once: true });
|
|
1108
|
-
const pendingCall = pendingIdentity(active);
|
|
1109
|
-
if (!pendingCall) {
|
|
1110
|
-
throw new Error('suspended call identity was not recorded');
|
|
1111
|
-
}
|
|
1112
|
-
for (const listener of pendingListeners) {
|
|
1113
|
-
try {
|
|
1114
|
-
listener(pendingCall);
|
|
1115
|
-
}
|
|
1116
|
-
catch (error) {
|
|
1117
|
-
reportBackgroundError(error);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
if (active.signal.aborted)
|
|
1121
|
-
abortListener();
|
|
1122
|
-
return await active.deferred.promise;
|
|
1374
|
+
return await waitOnSuspendedCall(active);
|
|
1123
1375
|
}
|
|
1124
1376
|
catch (error) {
|
|
1125
1377
|
active.runError = error;
|
|
@@ -1130,6 +1382,16 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1130
1382
|
}
|
|
1131
1383
|
});
|
|
1132
1384
|
const abortPending = async (error = new Error('Nested playbook call aborted')) => {
|
|
1385
|
+
const mode = restoreMode;
|
|
1386
|
+
if (mode) {
|
|
1387
|
+
restoreMode = undefined;
|
|
1388
|
+
const restored = mode.state === 'claimed'
|
|
1389
|
+
? rollbackRestoredCall(mode, error)
|
|
1390
|
+
: undefined;
|
|
1391
|
+
if (restored)
|
|
1392
|
+
await restored.finished.promise;
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1133
1395
|
const active = current;
|
|
1134
1396
|
if (!active)
|
|
1135
1397
|
return;
|
|
@@ -1157,6 +1419,70 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1157
1419
|
return {
|
|
1158
1420
|
actorLogic,
|
|
1159
1421
|
getPendingCall: () => pendingIdentity(current),
|
|
1422
|
+
getSuspendedCall: () => suspendedIdentity(current),
|
|
1423
|
+
prepareRestore(call) {
|
|
1424
|
+
// Capture the complete host-owned descriptor before observing or
|
|
1425
|
+
// mutating bridge state, so a rejected preparation cannot leave state.
|
|
1426
|
+
const captured = call === undefined
|
|
1427
|
+
? undefined
|
|
1428
|
+
: snapshotSuspendedCall(call, 'restored playbook call');
|
|
1429
|
+
if (disposed) {
|
|
1430
|
+
rejectControlPlane(new Error('nested playbook bridge is disposed'));
|
|
1431
|
+
}
|
|
1432
|
+
if (current) {
|
|
1433
|
+
rejectControlPlane(new Error(`playbook call ${current.callId} is already outstanding`));
|
|
1434
|
+
}
|
|
1435
|
+
if (restoreMode) {
|
|
1436
|
+
rejectControlPlane(new Error('nested playbook bridge restore is already prepared'));
|
|
1437
|
+
}
|
|
1438
|
+
if (captured && usedCallIds.has(captured.callId)) {
|
|
1439
|
+
rejectControlPlane(new Error(`restored duplicate playbook call id ${captured.callId}`));
|
|
1440
|
+
}
|
|
1441
|
+
restoreMode = {
|
|
1442
|
+
...(captured === undefined ? {} : { call: captured }),
|
|
1443
|
+
state: 'armed',
|
|
1444
|
+
};
|
|
1445
|
+
},
|
|
1446
|
+
confirmRestore() {
|
|
1447
|
+
const mode = restoreMode;
|
|
1448
|
+
if (!mode) {
|
|
1449
|
+
throw new Error('nested playbook bridge restore is not prepared');
|
|
1450
|
+
}
|
|
1451
|
+
if (mode.state === 'failed') {
|
|
1452
|
+
restoreMode = undefined;
|
|
1453
|
+
throw mode.error;
|
|
1454
|
+
}
|
|
1455
|
+
if (mode.call === undefined) {
|
|
1456
|
+
restoreMode = undefined;
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
if (mode.state !== 'claimed' || !mode.active) {
|
|
1460
|
+
const error = new Error(`restored playbook call ${mode.call.callId} was not claimed by actor startup`);
|
|
1461
|
+
restoreMode = undefined;
|
|
1462
|
+
reportControlPlaneError(error);
|
|
1463
|
+
throw error;
|
|
1464
|
+
}
|
|
1465
|
+
const active = mode.active;
|
|
1466
|
+
if (active.signal.aborted) {
|
|
1467
|
+
const error = active.signal.reason ??
|
|
1468
|
+
new Error('Restored nested playbook invocation aborted');
|
|
1469
|
+
rollbackRestoredCall(mode, error);
|
|
1470
|
+
restoreMode = undefined;
|
|
1471
|
+
throw error;
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
detachAbortListener(active);
|
|
1475
|
+
active.phase = 'suspended';
|
|
1476
|
+
restoreMode = undefined;
|
|
1477
|
+
publishSuspendedCall(active);
|
|
1478
|
+
}
|
|
1479
|
+
catch (error) {
|
|
1480
|
+
rollbackRestoredCall(mode, error);
|
|
1481
|
+
restoreMode = undefined;
|
|
1482
|
+
reportControlPlaneError(error);
|
|
1483
|
+
throw error;
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1160
1486
|
subscribePendingCall(listener) {
|
|
1161
1487
|
if (disposed)
|
|
1162
1488
|
return () => undefined;
|
|
@@ -1212,6 +1538,7 @@ export function createNestedPlaybookBridge(options) {
|
|
|
1212
1538
|
}
|
|
1213
1539
|
}
|
|
1214
1540
|
finally {
|
|
1541
|
+
restoreMode = undefined;
|
|
1215
1542
|
pendingListeners.clear();
|
|
1216
1543
|
}
|
|
1217
1544
|
},
|