@sublang/playbook 6.0.0 → 8.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 +28 -11
- package/docs/cli.md +158 -68
- package/docs/configuration.md +246 -108
- package/docs/embedding.md +71 -25
- package/package.json +6 -3
- package/reference/sdlc/captain.playbook/captain.playbook.js +3 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +3 -3
- package/reference/sdlc/code.md +1 -1
- package/reference/sdlc/code.playbook/bin/interactive-session.js +816 -0
- package/reference/sdlc/code.playbook/bin/launch-config.js +1900 -0
- package/reference/sdlc/code.playbook/bin/playbook.js +573 -535
- package/reference/sdlc/code.playbook/bin/provision.js +84 -38
- package/reference/sdlc/code.playbook/bin/run.js +1164 -991
- package/reference/sdlc/code.playbook/bin/session-store.js +1961 -0
- package/reference/sdlc/code.playbook/code.fsm.d.ts +5 -5
- package/reference/sdlc/code.playbook/code.fsm.introspect.js +2 -2
- package/reference/sdlc/code.playbook/code.fsm.introspect.ts +2 -2
- package/reference/sdlc/code.playbook/code.fsm.js +7 -11
- package/reference/sdlc/code.playbook/code.fsm.ts +9 -17
- package/reference/sdlc/code.playbook/code.gears.md +1 -1
- package/reference/sdlc/code.playbook/code.playbook.d.ts +2 -1
- package/reference/sdlc/code.playbook/code.playbook.js +12 -13
- package/reference/sdlc/code.playbook/code.playbook.ts +22 -15
- package/reference/sdlc/code.playbook/code.registry.d.ts +5 -13
- package/reference/sdlc/code.playbook/code.registry.js +3 -10
- package/reference/sdlc/code.playbook/code.registry.ts +7 -32
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +101 -9
- package/reference/sdlc/code.playbook/playbook-captain.js +1690 -213
- package/reference/sdlc/code.playbook/playbook-captain.ts +2492 -253
- package/reference/sdlc/code.playbook/playbook.config.template.yaml +44 -62
- package/reference/sdlc/decide.md +4 -4
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +9 -9
- package/reference/sdlc/decide.playbook/decide.fsm.js +21 -14
- package/reference/sdlc/decide.playbook/decide.fsm.ts +27 -23
- package/reference/sdlc/decide.playbook/decide.gears.md +3 -5
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +9 -13
- package/reference/sdlc/decide.playbook/decide.playbook.js +244 -143
- package/reference/sdlc/decide.playbook/decide.playbook.ts +326 -171
- package/reference/sdlc/decide.playbook/decide.registry.d.ts +5 -13
- package/reference/sdlc/decide.playbook/decide.registry.js +3 -9
- package/reference/sdlc/decide.playbook/decide.registry.ts +7 -31
- package/reference/sdlc/review.md +4 -5
- package/reference/sdlc/review.playbook/review.fsm.d.ts +9 -11
- package/reference/sdlc/review.playbook/review.fsm.js +30 -24
- package/reference/sdlc/review.playbook/review.fsm.ts +39 -35
- package/reference/sdlc/review.playbook/review.gears.md +6 -5
- package/reference/sdlc/review.playbook/review.playbook.d.ts +2 -1
- package/reference/sdlc/review.playbook/review.playbook.js +16 -21
- package/reference/sdlc/review.playbook/review.playbook.ts +26 -26
- package/reference/sdlc/review.playbook/review.registry.d.ts +5 -13
- package/reference/sdlc/review.playbook/review.registry.js +3 -16
- package/reference/sdlc/review.playbook/review.registry.ts +7 -38
- package/slc/gears2fsm.md +27 -23
- package/slc/link.md +140 -97
- package/slc/text2gears.md +19 -18
- package/src/runtime.d.ts +24 -8
- package/src/runtime.ts +29 -13
- package/src/xstate-playbook-runtime.d.ts +21 -17
- package/src/xstate-playbook-runtime.js +301 -159
- package/src/xstate-playbook-runtime.ts +405 -186
- package/src/xstate-runtime.d.ts +19 -2
- package/src/xstate-runtime.js +403 -62
- package/src/xstate-runtime.ts +566 -78
package/src/xstate-runtime.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
PlaybookSession,
|
|
23
23
|
PlaybookState,
|
|
24
24
|
PlaybookStateValue,
|
|
25
|
+
PlaybookSuspendedCall,
|
|
25
26
|
PlayerResult,
|
|
26
27
|
} from './runtime.js';
|
|
27
28
|
|
|
@@ -385,6 +386,50 @@ function capturedSessionStore(
|
|
|
385
386
|
});
|
|
386
387
|
}
|
|
387
388
|
|
|
389
|
+
function capturedRoleBindings(
|
|
390
|
+
descriptors: PropertyDescriptorMap,
|
|
391
|
+
): NonNullable<PlaybookSession['roleBindings']> {
|
|
392
|
+
const captured = snapshotJsonValue(
|
|
393
|
+
capturedDataValue(
|
|
394
|
+
descriptors,
|
|
395
|
+
'roleBindings',
|
|
396
|
+
'playbook session roleBindings',
|
|
397
|
+
),
|
|
398
|
+
'playbook session roleBindings',
|
|
399
|
+
);
|
|
400
|
+
if (!isRecord(captured)) {
|
|
401
|
+
throw new TypeError('playbook session roleBindings must be an object');
|
|
402
|
+
}
|
|
403
|
+
const bindings: Record<
|
|
404
|
+
string,
|
|
405
|
+
{ readonly playerId: string; readonly promptIdentity: string }
|
|
406
|
+
> = {};
|
|
407
|
+
for (const [roleId, value] of Object.entries(captured)) {
|
|
408
|
+
requireNonEmptyString(roleId, 'playbook session roleBindings role id');
|
|
409
|
+
if (!isRecord(value)) {
|
|
410
|
+
throw new TypeError(
|
|
411
|
+
`playbook session roleBindings.${roleId} must be an object`,
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
rejectUnknownKeys(
|
|
415
|
+
value,
|
|
416
|
+
['playerId', 'promptIdentity'],
|
|
417
|
+
`playbook session roleBindings.${roleId}`,
|
|
418
|
+
);
|
|
419
|
+
defineEnumerableDataProperty(bindings, roleId, Object.freeze({
|
|
420
|
+
playerId: requireNonEmptyString(
|
|
421
|
+
value.playerId,
|
|
422
|
+
`playbook session roleBindings.${roleId}.playerId`,
|
|
423
|
+
),
|
|
424
|
+
promptIdentity: requireNonEmptyString(
|
|
425
|
+
value.promptIdentity,
|
|
426
|
+
`playbook session roleBindings.${roleId}.promptIdentity`,
|
|
427
|
+
),
|
|
428
|
+
}));
|
|
429
|
+
}
|
|
430
|
+
return Object.freeze(bindings);
|
|
431
|
+
}
|
|
432
|
+
|
|
388
433
|
/** Validate session causality and detach its immutable identity from the host. */
|
|
389
434
|
export function snapshotPlaybookSession(
|
|
390
435
|
session: PlaybookSession,
|
|
@@ -452,6 +497,10 @@ export function snapshotPlaybookSession(
|
|
|
452
497
|
sessionDescriptors,
|
|
453
498
|
'playerSessions',
|
|
454
499
|
);
|
|
500
|
+
const hasRoleBindings = Object.prototype.hasOwnProperty.call(
|
|
501
|
+
sessionDescriptors,
|
|
502
|
+
'roleBindings',
|
|
503
|
+
);
|
|
455
504
|
let parentSessionId: string | undefined;
|
|
456
505
|
let parentCallId: string | undefined;
|
|
457
506
|
if (depth === 0) {
|
|
@@ -500,6 +549,9 @@ export function snapshotPlaybookSession(
|
|
|
500
549
|
const playerSessions = hasPlayerSessions
|
|
501
550
|
? capturedSessionStore(sessionDescriptors)
|
|
502
551
|
: undefined;
|
|
552
|
+
const roleBindings = hasRoleBindings
|
|
553
|
+
? capturedRoleBindings(sessionDescriptors)
|
|
554
|
+
: undefined;
|
|
503
555
|
return Object.freeze({
|
|
504
556
|
sessionId,
|
|
505
557
|
playbookId,
|
|
@@ -507,6 +559,7 @@ export function snapshotPlaybookSession(
|
|
|
507
559
|
...(parentSessionId === undefined ? {} : { parentSessionId }),
|
|
508
560
|
...(parentCallId === undefined ? {} : { parentCallId }),
|
|
509
561
|
depth,
|
|
562
|
+
...(roleBindings === undefined ? {} : { roleBindings }),
|
|
510
563
|
...(playerSessions === undefined ? {} : { playerSessions }),
|
|
511
564
|
ports,
|
|
512
565
|
});
|
|
@@ -757,23 +810,117 @@ const SNAPSHOT_SEQUENCE_KEYS = [
|
|
|
757
810
|
'playbookCall',
|
|
758
811
|
] as const;
|
|
759
812
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
813
|
+
export interface PlaybookRuntimeSnapshotValidationOptions {
|
|
814
|
+
/**
|
|
815
|
+
* Opt in only when the restore path will prepare and confirm the suspended
|
|
816
|
+
* call transaction. The default is fail-closed so a legacy restore cannot
|
|
817
|
+
* reopen or ignore it.
|
|
818
|
+
*/
|
|
819
|
+
allowSuspendedCall?: boolean;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function snapshotSuspendedCall(
|
|
823
|
+
value: unknown,
|
|
824
|
+
path = 'runtime snapshot suspendedCall',
|
|
825
|
+
): PlaybookSuspendedCall {
|
|
826
|
+
const captured = snapshotJsonValue(value, path);
|
|
827
|
+
if (!isRecord(captured)) {
|
|
828
|
+
throw new TypeError(`${path} must be an object`);
|
|
829
|
+
}
|
|
830
|
+
rejectUnknownKeys(
|
|
831
|
+
captured,
|
|
832
|
+
['callId', 'stateId', 'playbookId', 'text', 'childSessionId', 'turnId'],
|
|
833
|
+
path,
|
|
834
|
+
);
|
|
835
|
+
const call: PlaybookSuspendedCall = {
|
|
836
|
+
callId: requireNonEmptyString(captured.callId, `${path}.callId`),
|
|
837
|
+
stateId: requireNonEmptyString(captured.stateId, `${path}.stateId`),
|
|
838
|
+
playbookId: requireNonEmptyString(
|
|
839
|
+
captured.playbookId,
|
|
840
|
+
`${path}.playbookId`,
|
|
841
|
+
),
|
|
842
|
+
text: requireNonEmptyString(captured.text, `${path}.text`),
|
|
843
|
+
childSessionId: requireNonEmptyString(
|
|
844
|
+
captured.childSessionId,
|
|
845
|
+
`${path}.childSessionId`,
|
|
846
|
+
),
|
|
847
|
+
};
|
|
848
|
+
if (own(captured, 'turnId')) {
|
|
849
|
+
if (
|
|
850
|
+
!Number.isSafeInteger(captured.turnId) ||
|
|
851
|
+
(captured.turnId as number) <= 0
|
|
852
|
+
) {
|
|
853
|
+
throw new TypeError(`${path}.turnId must be a positive integer`);
|
|
854
|
+
}
|
|
855
|
+
call.turnId = captured.turnId as number;
|
|
856
|
+
}
|
|
857
|
+
return Object.freeze(call);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// DR-014 §1 / DR-031 §5 / DR-032: validate and detach a host-supplied
|
|
861
|
+
// schema-3 runtime snapshot before restore touches any state. A suspended
|
|
862
|
+
// call is rejected unless the restore path explicitly promises to seed and
|
|
863
|
+
// claim it; older schemas are rejected rather than guessing role identity.
|
|
763
864
|
export function assertPlaybookRuntimeSnapshot(
|
|
764
865
|
value: unknown,
|
|
765
866
|
expectedPlaybookId: string,
|
|
867
|
+
options: PlaybookRuntimeSnapshotValidationOptions = {},
|
|
766
868
|
): PlaybookRuntimeSnapshot {
|
|
767
|
-
|
|
869
|
+
const snapshot = snapshotJsonValue(value, 'runtime snapshot');
|
|
870
|
+
if (!isRecord(snapshot)) {
|
|
768
871
|
throw new TypeError('runtime snapshot must be an object');
|
|
769
872
|
}
|
|
770
|
-
|
|
873
|
+
const capturedOptions = snapshotJsonValue(
|
|
874
|
+
options,
|
|
875
|
+
'runtime snapshot validation options',
|
|
876
|
+
);
|
|
877
|
+
if (!isRecord(capturedOptions)) {
|
|
878
|
+
throw new TypeError('runtime snapshot validation options must be an object');
|
|
879
|
+
}
|
|
880
|
+
rejectUnknownKeys(
|
|
881
|
+
capturedOptions,
|
|
882
|
+
['allowSuspendedCall'],
|
|
883
|
+
'runtime snapshot validation options',
|
|
884
|
+
);
|
|
885
|
+
if (
|
|
886
|
+
capturedOptions.allowSuspendedCall !== undefined &&
|
|
887
|
+
typeof capturedOptions.allowSuspendedCall !== 'boolean'
|
|
888
|
+
) {
|
|
771
889
|
throw new TypeError(
|
|
772
|
-
|
|
890
|
+
'runtime snapshot validation options.allowSuspendedCall must be boolean',
|
|
773
891
|
);
|
|
774
892
|
}
|
|
893
|
+
const allowSuspendedCall = capturedOptions.allowSuspendedCall ?? false;
|
|
894
|
+
if (snapshot.schemaVersion !== 3) {
|
|
895
|
+
throw new TypeError(
|
|
896
|
+
`runtime snapshot schemaVersion ${String(snapshot.schemaVersion)} is not supported (expected 3)`,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
rejectUnknownKeys(
|
|
900
|
+
snapshot,
|
|
901
|
+
[
|
|
902
|
+
'schemaVersion',
|
|
903
|
+
'playbookId',
|
|
904
|
+
'machine',
|
|
905
|
+
'roleResumeTokens',
|
|
906
|
+
'sequences',
|
|
907
|
+
'state',
|
|
908
|
+
'pendingBossQuestions',
|
|
909
|
+
'suspendedCall',
|
|
910
|
+
],
|
|
911
|
+
'runtime snapshot',
|
|
912
|
+
);
|
|
913
|
+
let suspendedCall: PlaybookSuspendedCall | undefined;
|
|
914
|
+
if (own(snapshot, 'suspendedCall')) {
|
|
915
|
+
suspendedCall = snapshotSuspendedCall(snapshot.suspendedCall);
|
|
916
|
+
if (!allowSuspendedCall) {
|
|
917
|
+
throw new TypeError(
|
|
918
|
+
'runtime snapshot suspendedCall requires a restore path that explicitly allows it',
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
775
922
|
const playbookId = requireNonEmptyString(
|
|
776
|
-
|
|
923
|
+
snapshot.playbookId,
|
|
777
924
|
'runtime snapshot playbookId',
|
|
778
925
|
);
|
|
779
926
|
if (playbookId !== expectedPlaybookId) {
|
|
@@ -781,32 +928,37 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
781
928
|
`runtime snapshot playbookId ${playbookId} does not match runtime playbook ${expectedPlaybookId}`,
|
|
782
929
|
);
|
|
783
930
|
}
|
|
784
|
-
if (!isRecord(
|
|
931
|
+
if (!isRecord(snapshot.machine)) {
|
|
785
932
|
throw new TypeError('runtime snapshot machine must be an object');
|
|
786
933
|
}
|
|
787
|
-
const machine =
|
|
788
|
-
if (!isRecord(
|
|
934
|
+
const machine = snapshot.machine;
|
|
935
|
+
if (!isRecord(snapshot.roleResumeTokens)) {
|
|
789
936
|
throw new TypeError(
|
|
790
|
-
'runtime snapshot
|
|
937
|
+
'runtime snapshot roleResumeTokens must be an object',
|
|
791
938
|
);
|
|
792
939
|
}
|
|
793
|
-
const
|
|
794
|
-
for (const [
|
|
940
|
+
const roleResumeTokens: Record<string, string> = {};
|
|
941
|
+
for (const [roleId, token] of Object.entries(snapshot.roleResumeTokens)) {
|
|
795
942
|
defineEnumerableDataProperty(
|
|
796
|
-
|
|
797
|
-
|
|
943
|
+
roleResumeTokens,
|
|
944
|
+
requireNonEmptyString(roleId, 'runtime snapshot roleResumeTokens role id'),
|
|
798
945
|
requireNonEmptyString(
|
|
799
946
|
token,
|
|
800
|
-
`runtime snapshot
|
|
947
|
+
`runtime snapshot roleResumeTokens.${roleId}`,
|
|
801
948
|
),
|
|
802
949
|
);
|
|
803
950
|
}
|
|
804
|
-
if (!isRecord(
|
|
951
|
+
if (!isRecord(snapshot.sequences)) {
|
|
805
952
|
throw new TypeError('runtime snapshot sequences must be an object');
|
|
806
953
|
}
|
|
954
|
+
rejectUnknownKeys(
|
|
955
|
+
snapshot.sequences,
|
|
956
|
+
[...SNAPSHOT_SEQUENCE_KEYS, 'captainCall'],
|
|
957
|
+
'runtime snapshot sequences',
|
|
958
|
+
);
|
|
807
959
|
const sequences = {} as PlaybookRuntimeSnapshot['sequences'];
|
|
808
960
|
for (const key of SNAPSHOT_SEQUENCE_KEYS) {
|
|
809
|
-
const sequence =
|
|
961
|
+
const sequence = snapshot.sequences[key];
|
|
810
962
|
if (!Number.isSafeInteger(sequence) || (sequence as number) < 0) {
|
|
811
963
|
throw new TypeError(
|
|
812
964
|
`runtime snapshot sequences.${key} must be a non-negative integer`,
|
|
@@ -814,7 +966,7 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
814
966
|
}
|
|
815
967
|
sequences[key] = sequence as number;
|
|
816
968
|
}
|
|
817
|
-
const captainCall =
|
|
969
|
+
const captainCall = snapshot.sequences.captainCall;
|
|
818
970
|
if (captainCall !== undefined) {
|
|
819
971
|
if (!Number.isSafeInteger(captainCall) || (captainCall as number) < 0) {
|
|
820
972
|
throw new TypeError(
|
|
@@ -823,26 +975,84 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
823
975
|
}
|
|
824
976
|
sequences.captainCall = captainCall as number;
|
|
825
977
|
}
|
|
826
|
-
validateState(
|
|
827
|
-
const state =
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
978
|
+
validateState(snapshot.state, 'runtime snapshot state');
|
|
979
|
+
const state = snapshot.state as unknown as PlaybookState;
|
|
980
|
+
if (state.tags.includes(SUSPENDED_TAG) && suspendedCall === undefined) {
|
|
981
|
+
throw new TypeError(
|
|
982
|
+
`runtime snapshot state tagged ${SUSPENDED_TAG} requires suspendedCall`,
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (suspendedCall) {
|
|
986
|
+
if (sequences.playbookCall === 0) {
|
|
987
|
+
throw new TypeError(
|
|
988
|
+
'runtime snapshot suspendedCall requires sequences.playbookCall greater than zero',
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (
|
|
992
|
+
suspendedCall.turnId !== undefined &&
|
|
993
|
+
suspendedCall.turnId > sequences.turn
|
|
994
|
+
) {
|
|
995
|
+
throw new TypeError(
|
|
996
|
+
'runtime snapshot suspendedCall.turnId must not exceed sequences.turn',
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (state.status !== 'active' || !state.quiescent) {
|
|
1000
|
+
throw new TypeError(
|
|
1001
|
+
'runtime snapshot suspendedCall requires an active quiescent state',
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (!state.tags.includes(SUSPENDED_TAG)) {
|
|
1005
|
+
throw new TypeError(
|
|
1006
|
+
`runtime snapshot suspendedCall requires state tag ${SUSPENDED_TAG}`,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
if (!state.activeStateIds.includes(suspendedCall.stateId)) {
|
|
1010
|
+
throw new TypeError(
|
|
1011
|
+
'runtime snapshot suspendedCall.stateId must be active in snapshot state',
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (!Array.isArray(snapshot.pendingBossQuestions)) {
|
|
832
1016
|
throw new TypeError(
|
|
833
1017
|
'runtime snapshot pendingBossQuestions must be an array',
|
|
834
1018
|
);
|
|
835
1019
|
}
|
|
836
|
-
const pendingBossQuestions =
|
|
1020
|
+
const pendingBossQuestions = snapshot.pendingBossQuestions.map(
|
|
837
1021
|
(entry, index) => {
|
|
838
1022
|
const path = `runtime snapshot pendingBossQuestions[${index}]`;
|
|
839
1023
|
if (!isRecord(entry)) throw new TypeError(`${path} must be an object`);
|
|
1024
|
+
rejectUnknownKeys(
|
|
1025
|
+
entry,
|
|
1026
|
+
['questionId', 'asker', 'question', 'sourceItem'],
|
|
1027
|
+
path,
|
|
1028
|
+
);
|
|
1029
|
+
if (!isRecord(entry.asker)) {
|
|
1030
|
+
throw new TypeError(`${path}.asker must be an object`);
|
|
1031
|
+
}
|
|
1032
|
+
let asker: PlaybookPendingBossQuestion['asker'];
|
|
1033
|
+
if (entry.asker.kind === 'captain') {
|
|
1034
|
+
rejectUnknownKeys(entry.asker, ['kind'], `${path}.asker`);
|
|
1035
|
+
asker = Object.freeze({ kind: 'captain' });
|
|
1036
|
+
} else if (entry.asker.kind === 'role') {
|
|
1037
|
+
rejectUnknownKeys(entry.asker, ['kind', 'roleId'], `${path}.asker`);
|
|
1038
|
+
asker = Object.freeze({
|
|
1039
|
+
kind: 'role',
|
|
1040
|
+
roleId: requireNonEmptyString(
|
|
1041
|
+
entry.asker.roleId,
|
|
1042
|
+
`${path}.asker.roleId`,
|
|
1043
|
+
),
|
|
1044
|
+
});
|
|
1045
|
+
} else {
|
|
1046
|
+
throw new TypeError(
|
|
1047
|
+
`${path}.asker.kind must be "captain" or "role"`,
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
840
1050
|
const question: PlaybookPendingBossQuestion = {
|
|
841
1051
|
questionId: requireNonEmptyString(
|
|
842
1052
|
entry.questionId,
|
|
843
1053
|
`${path}.questionId`,
|
|
844
1054
|
),
|
|
845
|
-
|
|
1055
|
+
asker,
|
|
846
1056
|
question: requireNonEmptyString(entry.question, `${path}.question`),
|
|
847
1057
|
...(entry.sourceItem === undefined
|
|
848
1058
|
? {}
|
|
@@ -856,14 +1066,18 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
856
1066
|
return Object.freeze(question);
|
|
857
1067
|
},
|
|
858
1068
|
);
|
|
859
|
-
|
|
860
|
-
schemaVersion: 1,
|
|
1069
|
+
const fields = {
|
|
861
1070
|
playbookId,
|
|
862
1071
|
machine,
|
|
863
|
-
|
|
1072
|
+
roleResumeTokens: Object.freeze(roleResumeTokens),
|
|
864
1073
|
sequences: Object.freeze(sequences),
|
|
865
1074
|
state,
|
|
866
1075
|
pendingBossQuestions: Object.freeze(pendingBossQuestions),
|
|
1076
|
+
};
|
|
1077
|
+
return Object.freeze({
|
|
1078
|
+
schemaVersion: 3,
|
|
1079
|
+
...fields,
|
|
1080
|
+
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
867
1081
|
});
|
|
868
1082
|
}
|
|
869
1083
|
|
|
@@ -916,15 +1130,24 @@ export class NestedPlaybookCallError extends Error {
|
|
|
916
1130
|
interface ActiveCall {
|
|
917
1131
|
readonly callId: string;
|
|
918
1132
|
readonly input: NestedPlaybookInput;
|
|
1133
|
+
readonly turnId?: number;
|
|
919
1134
|
readonly deferred: Deferred<JsonValue | undefined>;
|
|
920
1135
|
readonly finished: Deferred<void>;
|
|
921
1136
|
readonly controller: AbortController;
|
|
922
1137
|
readonly signal: AbortSignal;
|
|
923
|
-
phase: 'starting' | 'suspended' | 'settling';
|
|
1138
|
+
phase: 'starting' | 'restoring' | 'suspended' | 'settling';
|
|
924
1139
|
childSessionId?: string;
|
|
925
1140
|
abortListener?: () => void;
|
|
926
1141
|
settlement?: Promise<void>;
|
|
927
1142
|
runError?: unknown;
|
|
1143
|
+
restoreRolledBack?: boolean;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
interface NestedPlaybookRestoreMode {
|
|
1147
|
+
readonly call?: PlaybookSuspendedCall;
|
|
1148
|
+
state: 'armed' | 'claimed' | 'failed';
|
|
1149
|
+
active?: ActiveCall;
|
|
1150
|
+
error?: unknown;
|
|
928
1151
|
}
|
|
929
1152
|
|
|
930
1153
|
export interface PendingCallObserver {
|
|
@@ -938,6 +1161,15 @@ export interface NestedPlaybookBridge<
|
|
|
938
1161
|
TInput extends NestedPlaybookInput = NestedPlaybookInput,
|
|
939
1162
|
> extends PendingCallObserver {
|
|
940
1163
|
actorLogic: PromiseActorLogic<JsonValue | undefined, TInput>;
|
|
1164
|
+
/** Arm fail-closed actor startup for a snapshot with zero or one nested call. */
|
|
1165
|
+
prepareRestore(call?: PlaybookSuspendedCall): void;
|
|
1166
|
+
/**
|
|
1167
|
+
* Commit restore startup after the persisted machine recreated exactly the
|
|
1168
|
+
* expected zero or one nested invocation.
|
|
1169
|
+
*/
|
|
1170
|
+
confirmRestore(): void;
|
|
1171
|
+
/** Complete durable identity; undefined until a normal or restored call suspends. */
|
|
1172
|
+
getSuspendedCall(): PlaybookSuspendedCall | undefined;
|
|
941
1173
|
resume(input: {
|
|
942
1174
|
callId: string;
|
|
943
1175
|
result: PlaybookCallResult;
|
|
@@ -1061,6 +1293,9 @@ export function validatePlayerResult(
|
|
|
1061
1293
|
);
|
|
1062
1294
|
validateRunStatus(result.status, `${path}.status`);
|
|
1063
1295
|
validateOptionalString(result, 'resumeToken', path);
|
|
1296
|
+
if (result.resumeToken !== undefined) {
|
|
1297
|
+
requireNonEmptyString(result.resumeToken, `${path}.resumeToken`);
|
|
1298
|
+
}
|
|
1064
1299
|
validateOptionalString(result, 'finalText', path);
|
|
1065
1300
|
validateOptionalString(result, 'error', path);
|
|
1066
1301
|
return result as unknown as PlayerResult;
|
|
@@ -1267,6 +1502,7 @@ export function createNestedPlaybookBridge<
|
|
|
1267
1502
|
TInput extends NestedPlaybookInput = NestedPlaybookInput,
|
|
1268
1503
|
>(options: NestedPlaybookBridgeOptions): NestedPlaybookBridge<TInput> {
|
|
1269
1504
|
let current: ActiveCall | undefined;
|
|
1505
|
+
let restoreMode: NestedPlaybookRestoreMode | undefined;
|
|
1270
1506
|
let disposed = false;
|
|
1271
1507
|
const usedCallIds = new Set<string>();
|
|
1272
1508
|
const pendingListeners = new Set<
|
|
@@ -1307,10 +1543,38 @@ export function createNestedPlaybookBridge<
|
|
|
1307
1543
|
}
|
|
1308
1544
|
: undefined;
|
|
1309
1545
|
|
|
1310
|
-
const
|
|
1546
|
+
const suspendedIdentity = (
|
|
1547
|
+
active: ActiveCall | undefined,
|
|
1548
|
+
): PlaybookSuspendedCall | undefined =>
|
|
1549
|
+
active?.phase === 'suspended' && active.childSessionId
|
|
1550
|
+
? Object.freeze({
|
|
1551
|
+
callId: active.callId,
|
|
1552
|
+
stateId: active.input.stateId,
|
|
1553
|
+
playbookId: active.input.playbookId,
|
|
1554
|
+
text: active.input.text,
|
|
1555
|
+
childSessionId: active.childSessionId,
|
|
1556
|
+
...(active.turnId === undefined ? {} : { turnId: active.turnId }),
|
|
1557
|
+
})
|
|
1558
|
+
: undefined;
|
|
1559
|
+
|
|
1560
|
+
const failRestoreMode = (
|
|
1561
|
+
mode: NestedPlaybookRestoreMode,
|
|
1562
|
+
error: unknown,
|
|
1563
|
+
): void => {
|
|
1564
|
+
mode.state = 'failed';
|
|
1565
|
+
mode.error = error;
|
|
1566
|
+
reportControlPlaneError(error);
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
const detachAbortListener = (active: ActiveCall): void => {
|
|
1311
1570
|
if (active.abortListener) {
|
|
1312
1571
|
active.signal.removeEventListener('abort', active.abortListener);
|
|
1572
|
+
active.abortListener = undefined;
|
|
1313
1573
|
}
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const clear = (active: ActiveCall): void => {
|
|
1577
|
+
detachAbortListener(active);
|
|
1314
1578
|
if (current === active) current = undefined;
|
|
1315
1579
|
};
|
|
1316
1580
|
|
|
@@ -1443,35 +1707,202 @@ export function createNestedPlaybookBridge<
|
|
|
1443
1707
|
}
|
|
1444
1708
|
};
|
|
1445
1709
|
|
|
1710
|
+
const rollbackRestoredCall = (
|
|
1711
|
+
mode: NestedPlaybookRestoreMode,
|
|
1712
|
+
error: unknown,
|
|
1713
|
+
): ActiveCall | undefined => {
|
|
1714
|
+
const active = mode.active;
|
|
1715
|
+
mode.state = 'failed';
|
|
1716
|
+
mode.error = error;
|
|
1717
|
+
mode.active = undefined;
|
|
1718
|
+
if (!active) return undefined;
|
|
1719
|
+
active.phase = 'settling';
|
|
1720
|
+
active.restoreRolledBack = true;
|
|
1721
|
+
clear(active);
|
|
1722
|
+
usedCallIds.delete(active.callId);
|
|
1723
|
+
active.deferred.reject(error);
|
|
1724
|
+
return active;
|
|
1725
|
+
};
|
|
1726
|
+
|
|
1727
|
+
const publishSuspendedCall = (active: ActiveCall): void => {
|
|
1728
|
+
if (active.phase !== 'suspended') {
|
|
1729
|
+
throw new Error(`playbook call ${active.callId} is not suspended`);
|
|
1730
|
+
}
|
|
1731
|
+
const abortListener = (): void => {
|
|
1732
|
+
if (active.phase !== 'suspended') return;
|
|
1733
|
+
const result = resultFromThrown(
|
|
1734
|
+
active.input.playbookId,
|
|
1735
|
+
active.childSessionId,
|
|
1736
|
+
active.signal.reason ?? new Error('Nested playbook invocation aborted'),
|
|
1737
|
+
true,
|
|
1738
|
+
);
|
|
1739
|
+
void settlePending(active, result).catch((error: unknown) => {
|
|
1740
|
+
reportBackgroundError(error);
|
|
1741
|
+
});
|
|
1742
|
+
};
|
|
1743
|
+
active.abortListener = abortListener;
|
|
1744
|
+
active.signal.addEventListener('abort', abortListener, { once: true });
|
|
1745
|
+
const pendingCall = pendingIdentity(active);
|
|
1746
|
+
if (!pendingCall) {
|
|
1747
|
+
throw new Error('suspended call identity was not recorded');
|
|
1748
|
+
}
|
|
1749
|
+
for (const listener of pendingListeners) {
|
|
1750
|
+
try {
|
|
1751
|
+
listener(pendingCall);
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
reportBackgroundError(error);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (active.signal.aborted) abortListener();
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
const waitOnSuspendedCall = async (
|
|
1760
|
+
active: ActiveCall,
|
|
1761
|
+
): Promise<JsonValue | undefined> => {
|
|
1762
|
+
publishSuspendedCall(active);
|
|
1763
|
+
return await active.deferred.promise;
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1446
1766
|
const actorLogic = fromPromise<JsonValue | undefined, TInput>(
|
|
1447
1767
|
async ({ input, signal: invocationSignal }) => {
|
|
1448
1768
|
if (disposed) {
|
|
1449
1769
|
rejectControlPlane(new Error('nested playbook bridge is disposed'));
|
|
1450
1770
|
}
|
|
1771
|
+
const normalizedInput = (() => {
|
|
1772
|
+
try {
|
|
1773
|
+
return {
|
|
1774
|
+
stateId: requireNonEmptyString(
|
|
1775
|
+
input.stateId,
|
|
1776
|
+
'playbook input stateId',
|
|
1777
|
+
),
|
|
1778
|
+
playbookId: requireNonEmptyString(
|
|
1779
|
+
input.playbookId,
|
|
1780
|
+
'playbook input playbookId',
|
|
1781
|
+
),
|
|
1782
|
+
text: requireNonEmptyString(input.text, 'playbook input text'),
|
|
1783
|
+
};
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
const mode = restoreMode;
|
|
1786
|
+
if (mode) {
|
|
1787
|
+
if (mode.state === 'claimed') {
|
|
1788
|
+
rollbackRestoredCall(mode, error);
|
|
1789
|
+
reportControlPlaneError(error);
|
|
1790
|
+
} else failRestoreMode(mode, error);
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
return rejectControlPlane(error);
|
|
1794
|
+
}
|
|
1795
|
+
})();
|
|
1796
|
+
|
|
1797
|
+
const mode = restoreMode;
|
|
1798
|
+
if (mode) {
|
|
1799
|
+
if (mode.state !== 'armed') {
|
|
1800
|
+
const callId = mode.call?.callId ?? 'without a descriptor';
|
|
1801
|
+
const error = new Error(
|
|
1802
|
+
mode.state === 'claimed'
|
|
1803
|
+
? `restored playbook call ${callId} was claimed more than once`
|
|
1804
|
+
: `restored playbook call ${callId} is no longer claimable`,
|
|
1805
|
+
);
|
|
1806
|
+
if (mode.state === 'claimed') rollbackRestoredCall(mode, error);
|
|
1807
|
+
else mode.error ??= error;
|
|
1808
|
+
reportControlPlaneError(error);
|
|
1809
|
+
throw error;
|
|
1810
|
+
}
|
|
1811
|
+
const seed = mode.call;
|
|
1812
|
+
if (!seed) {
|
|
1813
|
+
const error = new Error(
|
|
1814
|
+
'restored machine invoked a nested playbook without a suspendedCall descriptor',
|
|
1815
|
+
);
|
|
1816
|
+
failRestoreMode(mode, error);
|
|
1817
|
+
throw error;
|
|
1818
|
+
}
|
|
1819
|
+
for (const field of ['stateId', 'playbookId', 'text'] as const) {
|
|
1820
|
+
if (normalizedInput[field] !== seed[field]) {
|
|
1821
|
+
const error = new Error(
|
|
1822
|
+
`restored playbook call ${seed.callId} ${field} does not match its persisted input`,
|
|
1823
|
+
);
|
|
1824
|
+
failRestoreMode(mode, error);
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (usedCallIds.has(seed.callId)) {
|
|
1829
|
+
const error = new Error(
|
|
1830
|
+
`restored duplicate playbook call id ${seed.callId}`,
|
|
1831
|
+
);
|
|
1832
|
+
failRestoreMode(mode, error);
|
|
1833
|
+
throw error;
|
|
1834
|
+
}
|
|
1835
|
+
const controller = new AbortController();
|
|
1836
|
+
let callSignal: AbortSignal;
|
|
1837
|
+
try {
|
|
1838
|
+
callSignal = combineAbortSignals(
|
|
1839
|
+
invocationSignal,
|
|
1840
|
+
options.getBoundarySignal?.(),
|
|
1841
|
+
controller.signal,
|
|
1842
|
+
);
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
failRestoreMode(mode, error);
|
|
1845
|
+
throw error;
|
|
1846
|
+
}
|
|
1847
|
+
const active: ActiveCall = {
|
|
1848
|
+
callId: seed.callId,
|
|
1849
|
+
input: normalizedInput,
|
|
1850
|
+
...(seed.turnId === undefined
|
|
1851
|
+
? {}
|
|
1852
|
+
: { turnId: seed.turnId }),
|
|
1853
|
+
deferred: deferred<JsonValue | undefined>(),
|
|
1854
|
+
finished: deferred<void>(),
|
|
1855
|
+
controller,
|
|
1856
|
+
signal: callSignal,
|
|
1857
|
+
phase: 'restoring',
|
|
1858
|
+
childSessionId: seed.childSessionId,
|
|
1859
|
+
};
|
|
1860
|
+
usedCallIds.add(active.callId);
|
|
1861
|
+
current = active;
|
|
1862
|
+
mode.state = 'claimed';
|
|
1863
|
+
mode.active = active;
|
|
1864
|
+
const restoreAbortListener = (): void => {
|
|
1865
|
+
if (
|
|
1866
|
+
restoreMode !== mode ||
|
|
1867
|
+
mode.state !== 'claimed' ||
|
|
1868
|
+
mode.active !== active ||
|
|
1869
|
+
active.phase !== 'restoring'
|
|
1870
|
+
) {
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
rollbackRestoredCall(
|
|
1874
|
+
mode,
|
|
1875
|
+
active.signal.reason ??
|
|
1876
|
+
new Error('Restored nested playbook invocation aborted'),
|
|
1877
|
+
);
|
|
1878
|
+
};
|
|
1879
|
+
active.abortListener = restoreAbortListener;
|
|
1880
|
+
active.signal.addEventListener('abort', restoreAbortListener, {
|
|
1881
|
+
once: true,
|
|
1882
|
+
});
|
|
1883
|
+
if (active.signal.aborted) restoreAbortListener();
|
|
1884
|
+
try {
|
|
1885
|
+
return await active.deferred.promise;
|
|
1886
|
+
} catch (error) {
|
|
1887
|
+
if (!active.restoreRolledBack) active.runError = error;
|
|
1888
|
+
throw error;
|
|
1889
|
+
} finally {
|
|
1890
|
+
active.finished.resolve(undefined);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1451
1894
|
if (current) {
|
|
1452
1895
|
rejectControlPlane(
|
|
1453
1896
|
new Error(`playbook call ${current.callId} is already outstanding`),
|
|
1454
1897
|
);
|
|
1455
1898
|
}
|
|
1456
|
-
|
|
1899
|
+
|
|
1900
|
+
const callId = (() => {
|
|
1457
1901
|
try {
|
|
1458
|
-
return
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
'playbook input stateId',
|
|
1463
|
-
),
|
|
1464
|
-
playbookId: requireNonEmptyString(
|
|
1465
|
-
input.playbookId,
|
|
1466
|
-
'playbook input playbookId',
|
|
1467
|
-
),
|
|
1468
|
-
text: requireNonEmptyString(input.text, 'playbook input text'),
|
|
1469
|
-
},
|
|
1470
|
-
requireNonEmptyString(
|
|
1471
|
-
options.nextCallId(),
|
|
1472
|
-
'allocated playbook call id',
|
|
1473
|
-
),
|
|
1474
|
-
] as const;
|
|
1902
|
+
return requireNonEmptyString(
|
|
1903
|
+
options.nextCallId(),
|
|
1904
|
+
'allocated playbook call id',
|
|
1905
|
+
);
|
|
1475
1906
|
} catch (error) {
|
|
1476
1907
|
return rejectControlPlane(error);
|
|
1477
1908
|
}
|
|
@@ -1665,34 +2096,7 @@ export function createNestedPlaybookBridge<
|
|
|
1665
2096
|
|
|
1666
2097
|
active.phase = 'suspended';
|
|
1667
2098
|
active.childSessionId = start.childSessionId;
|
|
1668
|
-
|
|
1669
|
-
if (active.phase !== 'suspended') return;
|
|
1670
|
-
const result = resultFromThrown(
|
|
1671
|
-
active.input.playbookId,
|
|
1672
|
-
active.childSessionId,
|
|
1673
|
-
active.signal.reason ??
|
|
1674
|
-
new Error('Nested playbook invocation aborted'),
|
|
1675
|
-
true,
|
|
1676
|
-
);
|
|
1677
|
-
void settlePending(active, result).catch((error: unknown) => {
|
|
1678
|
-
reportBackgroundError(error);
|
|
1679
|
-
});
|
|
1680
|
-
};
|
|
1681
|
-
active.abortListener = abortListener;
|
|
1682
|
-
active.signal.addEventListener('abort', abortListener, { once: true });
|
|
1683
|
-
const pendingCall = pendingIdentity(active);
|
|
1684
|
-
if (!pendingCall) {
|
|
1685
|
-
throw new Error('suspended call identity was not recorded');
|
|
1686
|
-
}
|
|
1687
|
-
for (const listener of pendingListeners) {
|
|
1688
|
-
try {
|
|
1689
|
-
listener(pendingCall);
|
|
1690
|
-
} catch (error) {
|
|
1691
|
-
reportBackgroundError(error);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
if (active.signal.aborted) abortListener();
|
|
1695
|
-
return await active.deferred.promise;
|
|
2099
|
+
return await waitOnSuspendedCall(active);
|
|
1696
2100
|
} catch (error) {
|
|
1697
2101
|
active.runError = error;
|
|
1698
2102
|
throw error;
|
|
@@ -1705,6 +2109,16 @@ export function createNestedPlaybookBridge<
|
|
|
1705
2109
|
const abortPending = async (
|
|
1706
2110
|
error: unknown = new Error('Nested playbook call aborted'),
|
|
1707
2111
|
): Promise<void> => {
|
|
2112
|
+
const mode = restoreMode;
|
|
2113
|
+
if (mode) {
|
|
2114
|
+
restoreMode = undefined;
|
|
2115
|
+
const restored =
|
|
2116
|
+
mode.state === 'claimed'
|
|
2117
|
+
? rollbackRestoredCall(mode, error)
|
|
2118
|
+
: undefined;
|
|
2119
|
+
if (restored) await restored.finished.promise;
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
1708
2122
|
const active = current;
|
|
1709
2123
|
if (!active) return;
|
|
1710
2124
|
if (!active.controller.signal.aborted) active.controller.abort(error);
|
|
@@ -1740,6 +2154,79 @@ export function createNestedPlaybookBridge<
|
|
|
1740
2154
|
return {
|
|
1741
2155
|
actorLogic,
|
|
1742
2156
|
getPendingCall: () => pendingIdentity(current),
|
|
2157
|
+
getSuspendedCall: () => suspendedIdentity(current),
|
|
2158
|
+
prepareRestore(call) {
|
|
2159
|
+
// Capture the complete host-owned descriptor before observing or
|
|
2160
|
+
// mutating bridge state, so a rejected preparation cannot leave state.
|
|
2161
|
+
const captured =
|
|
2162
|
+
call === undefined
|
|
2163
|
+
? undefined
|
|
2164
|
+
: snapshotSuspendedCall(call, 'restored playbook call');
|
|
2165
|
+
if (disposed) {
|
|
2166
|
+
rejectControlPlane(new Error('nested playbook bridge is disposed'));
|
|
2167
|
+
}
|
|
2168
|
+
if (current) {
|
|
2169
|
+
rejectControlPlane(
|
|
2170
|
+
new Error(`playbook call ${current.callId} is already outstanding`),
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
if (restoreMode) {
|
|
2174
|
+
rejectControlPlane(
|
|
2175
|
+
new Error('nested playbook bridge restore is already prepared'),
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
if (captured && usedCallIds.has(captured.callId)) {
|
|
2179
|
+
rejectControlPlane(
|
|
2180
|
+
new Error(`restored duplicate playbook call id ${captured.callId}`),
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
restoreMode = {
|
|
2184
|
+
...(captured === undefined ? {} : { call: captured }),
|
|
2185
|
+
state: 'armed',
|
|
2186
|
+
};
|
|
2187
|
+
},
|
|
2188
|
+
confirmRestore() {
|
|
2189
|
+
const mode = restoreMode;
|
|
2190
|
+
if (!mode) {
|
|
2191
|
+
throw new Error('nested playbook bridge restore is not prepared');
|
|
2192
|
+
}
|
|
2193
|
+
if (mode.state === 'failed') {
|
|
2194
|
+
restoreMode = undefined;
|
|
2195
|
+
throw mode.error;
|
|
2196
|
+
}
|
|
2197
|
+
if (mode.call === undefined) {
|
|
2198
|
+
restoreMode = undefined;
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
if (mode.state !== 'claimed' || !mode.active) {
|
|
2202
|
+
const error = new Error(
|
|
2203
|
+
`restored playbook call ${mode.call.callId} was not claimed by actor startup`,
|
|
2204
|
+
);
|
|
2205
|
+
restoreMode = undefined;
|
|
2206
|
+
reportControlPlaneError(error);
|
|
2207
|
+
throw error;
|
|
2208
|
+
}
|
|
2209
|
+
const active = mode.active;
|
|
2210
|
+
if (active.signal.aborted) {
|
|
2211
|
+
const error =
|
|
2212
|
+
active.signal.reason ??
|
|
2213
|
+
new Error('Restored nested playbook invocation aborted');
|
|
2214
|
+
rollbackRestoredCall(mode, error);
|
|
2215
|
+
restoreMode = undefined;
|
|
2216
|
+
throw error;
|
|
2217
|
+
}
|
|
2218
|
+
try {
|
|
2219
|
+
detachAbortListener(active);
|
|
2220
|
+
active.phase = 'suspended';
|
|
2221
|
+
restoreMode = undefined;
|
|
2222
|
+
publishSuspendedCall(active);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
rollbackRestoredCall(mode, error);
|
|
2225
|
+
restoreMode = undefined;
|
|
2226
|
+
reportControlPlaneError(error);
|
|
2227
|
+
throw error;
|
|
2228
|
+
}
|
|
2229
|
+
},
|
|
1743
2230
|
subscribePendingCall(listener) {
|
|
1744
2231
|
if (disposed) return () => undefined;
|
|
1745
2232
|
pendingListeners.add(listener);
|
|
@@ -1807,6 +2294,7 @@ export function createNestedPlaybookBridge<
|
|
|
1807
2294
|
throw active.runError;
|
|
1808
2295
|
}
|
|
1809
2296
|
} finally {
|
|
2297
|
+
restoreMode = undefined;
|
|
1810
2298
|
pendingListeners.clear();
|
|
1811
2299
|
}
|
|
1812
2300
|
},
|