@sublang/playbook 8.0.0 → 9.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 +3 -3
- package/docs/cli.md +15 -15
- package/docs/configuration.md +13 -8
- package/docs/embedding.md +7 -2
- package/package.json +1 -1
- package/reference/sdlc/captain.playbook/captain.playbook.js +14 -3
- package/reference/sdlc/captain.playbook/captain.playbook.ts +18 -4
- package/reference/sdlc/code.playbook/code.fsm.d.ts +4 -1
- package/reference/sdlc/code.playbook/code.fsm.js +11 -4
- package/reference/sdlc/code.playbook/code.fsm.ts +12 -4
- package/reference/sdlc/code.playbook/code.playbook.js +14 -3
- package/reference/sdlc/code.playbook/code.playbook.ts +13 -3
- package/reference/sdlc/code.playbook/playbook-captain.js +44 -10
- package/reference/sdlc/code.playbook/playbook-captain.ts +47 -10
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.d.ts +2 -0
- package/reference/sdlc/decide.playbook/decide.playbook.js +299 -117
- package/reference/sdlc/decide.playbook/decide.playbook.ts +395 -131
- package/reference/sdlc/review.playbook/review.playbook.js +14 -3
- package/reference/sdlc/review.playbook/review.playbook.ts +13 -3
- package/slc/gears2fsm.md +19 -2
- package/slc/link.md +184 -42
- package/src/runtime.d.ts +1 -0
- package/src/runtime.ts +1 -0
- package/src/xstate-playbook-runtime.d.ts +13 -3
- package/src/xstate-playbook-runtime.js +732 -251
- package/src/xstate-playbook-runtime.ts +873 -280
- package/src/xstate-runtime.d.ts +17 -7
- package/src/xstate-runtime.js +135 -57
- package/src/xstate-runtime.ts +243 -84
package/src/xstate-runtime.ts
CHANGED
|
@@ -68,11 +68,45 @@ function withAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// slc/link.md §Abort: cancellation is causal identity with the applicable
|
|
72
|
+
// signal's reason; an `AbortError`-named rejection that is not that exact
|
|
73
|
+
// reason is a control-plane failure to surface, never an abort to swallow.
|
|
71
74
|
function isAbortReason(error: unknown, signal: AbortSignal): boolean {
|
|
72
|
-
return (
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
return signal.aborted && Object.is(error, signal.reason);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Immutable cancellation provenance for one runtime operation. The captured
|
|
80
|
+
* signal identities do not change when a mutable runtime advances to another
|
|
81
|
+
* public boundary, while each signal's eventual reason remains observable.
|
|
82
|
+
*/
|
|
83
|
+
interface AbortReasonClassifier {
|
|
84
|
+
isAbortReason(error: unknown): boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function createAbortReasonClassifier(
|
|
88
|
+
...sources: readonly (
|
|
89
|
+
| AbortSignal
|
|
90
|
+
| AbortReasonClassifier
|
|
91
|
+
| undefined
|
|
92
|
+
)[]
|
|
93
|
+
): AbortReasonClassifier {
|
|
94
|
+
const captured = Object.freeze(
|
|
95
|
+
sources.filter(
|
|
96
|
+
(
|
|
97
|
+
source,
|
|
98
|
+
): source is AbortSignal | AbortReasonClassifier =>
|
|
99
|
+
source !== undefined,
|
|
100
|
+
),
|
|
75
101
|
);
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
isAbortReason: (error: unknown): boolean =>
|
|
104
|
+
captured.some((source) =>
|
|
105
|
+
source instanceof AbortSignal
|
|
106
|
+
? isAbortReason(error, source)
|
|
107
|
+
: source.isAbortReason(error),
|
|
108
|
+
),
|
|
109
|
+
});
|
|
76
110
|
}
|
|
77
111
|
|
|
78
112
|
const NEVER_ABORTED_SIGNAL = new AbortController().signal;
|
|
@@ -119,7 +153,10 @@ export function registerPlaybookAbortCleanup(
|
|
|
119
153
|
void cleanup.catch(() => undefined);
|
|
120
154
|
}
|
|
121
155
|
|
|
122
|
-
async function drainPlaybookAbortCleanups(
|
|
156
|
+
async function drainPlaybookAbortCleanups(
|
|
157
|
+
signal: AbortSignal,
|
|
158
|
+
aborts: AbortReasonClassifier,
|
|
159
|
+
): Promise<void> {
|
|
123
160
|
const failures: unknown[] = [];
|
|
124
161
|
while (true) {
|
|
125
162
|
const pending = abortCleanups.get(signal);
|
|
@@ -128,7 +165,12 @@ async function drainPlaybookAbortCleanups(signal: AbortSignal): Promise<void> {
|
|
|
128
165
|
pending.clear();
|
|
129
166
|
const outcomes = await Promise.allSettled(batch);
|
|
130
167
|
for (const outcome of outcomes) {
|
|
131
|
-
if (
|
|
168
|
+
if (
|
|
169
|
+
outcome.status === 'rejected' &&
|
|
170
|
+
!aborts.isAbortReason(outcome.reason)
|
|
171
|
+
) {
|
|
172
|
+
failures.push(outcome.reason);
|
|
173
|
+
}
|
|
132
174
|
}
|
|
133
175
|
}
|
|
134
176
|
abortCleanups.delete(signal);
|
|
@@ -671,7 +713,10 @@ function normalizeStateValue(
|
|
|
671
713
|
|
|
672
714
|
export interface PlaybookStateMetadata {
|
|
673
715
|
stateId: string;
|
|
674
|
-
|
|
716
|
+
// Optional by contract: a state whose source declares no description
|
|
717
|
+
// carries none, and no id is ever promoted into one
|
|
718
|
+
// (slc/link.md §Snapshot normalization).
|
|
719
|
+
description?: string;
|
|
675
720
|
}
|
|
676
721
|
|
|
677
722
|
interface MachineSnapshotLike {
|
|
@@ -718,17 +763,31 @@ export function activePlaybookStateMetadata(
|
|
|
718
763
|
meta.playbook.stateId,
|
|
719
764
|
`${nodeId}.meta.playbook.stateId`,
|
|
720
765
|
);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
766
|
+
// Description is optional: a state may declare none and stay fully
|
|
767
|
+
// usable, merely carrying no `stateDescription` downstream. A declared
|
|
768
|
+
// description must still be a nonempty string.
|
|
769
|
+
const description =
|
|
770
|
+
meta.playbook.description === undefined
|
|
771
|
+
? undefined
|
|
772
|
+
: requireNonEmptyString(
|
|
773
|
+
meta.playbook.description,
|
|
774
|
+
`${nodeId}.meta.playbook.description`,
|
|
775
|
+
);
|
|
725
776
|
const previous = byStateId.get(stateId);
|
|
726
|
-
if (
|
|
777
|
+
if (
|
|
778
|
+
previous?.description !== undefined &&
|
|
779
|
+
description !== undefined &&
|
|
780
|
+
previous.description !== description
|
|
781
|
+
) {
|
|
727
782
|
throw new TypeError(
|
|
728
783
|
`active state id ${stateId} has conflicting descriptions`,
|
|
729
784
|
);
|
|
730
785
|
}
|
|
731
|
-
|
|
786
|
+
const effective = description ?? previous?.description;
|
|
787
|
+
byStateId.set(stateId, {
|
|
788
|
+
stateId,
|
|
789
|
+
...(effective === undefined ? {} : { description: effective }),
|
|
790
|
+
});
|
|
732
791
|
}
|
|
733
792
|
return [...byStateId.values()].sort((left, right) =>
|
|
734
793
|
left.stateId.localeCompare(right.stateId),
|
|
@@ -1106,12 +1165,29 @@ export interface NestedPlaybookBridgeOptions {
|
|
|
1106
1165
|
request: PlaybookCallRequest,
|
|
1107
1166
|
signal: AbortSignal,
|
|
1108
1167
|
): Promise<PlaybookCallStart>;
|
|
1109
|
-
emitStarted(
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1168
|
+
emitStarted(
|
|
1169
|
+
event: PlaybookCallStarted,
|
|
1170
|
+
aborts?: AbortReasonClassifier,
|
|
1171
|
+
): Promise<void>;
|
|
1172
|
+
emitFinished(
|
|
1173
|
+
event: PlaybookCallFinished,
|
|
1174
|
+
aborts?: AbortReasonClassifier,
|
|
1175
|
+
): Promise<void>;
|
|
1176
|
+
drain(aborts?: AbortReasonClassifier): Promise<void>;
|
|
1177
|
+
bindResumeSignal?(
|
|
1178
|
+
signal: AbortSignal,
|
|
1179
|
+
aborts?: AbortReasonClassifier,
|
|
1180
|
+
): void;
|
|
1181
|
+
/** Bind provenance to the root transition caused by this child result. */
|
|
1182
|
+
bindActorSettlement?(aborts: AbortReasonClassifier): void;
|
|
1183
|
+
onControlPlaneError?(
|
|
1184
|
+
error: unknown,
|
|
1185
|
+
aborts?: AbortReasonClassifier,
|
|
1186
|
+
): void;
|
|
1187
|
+
onBackgroundError?(
|
|
1188
|
+
error: unknown,
|
|
1189
|
+
aborts?: AbortReasonClassifier,
|
|
1190
|
+
): void;
|
|
1115
1191
|
}
|
|
1116
1192
|
|
|
1117
1193
|
export class NestedPlaybookCallError extends Error {
|
|
@@ -1135,6 +1211,7 @@ interface ActiveCall {
|
|
|
1135
1211
|
readonly finished: Deferred<void>;
|
|
1136
1212
|
readonly controller: AbortController;
|
|
1137
1213
|
readonly signal: AbortSignal;
|
|
1214
|
+
readonly aborts: AbortReasonClassifier;
|
|
1138
1215
|
phase: 'starting' | 'restoring' | 'suspended' | 'settling';
|
|
1139
1216
|
childSessionId?: string;
|
|
1140
1217
|
abortListener?: () => void;
|
|
@@ -1509,21 +1586,29 @@ export function createNestedPlaybookBridge<
|
|
|
1509
1586
|
(pendingCall: PlaybookPendingCall) => void
|
|
1510
1587
|
>();
|
|
1511
1588
|
|
|
1512
|
-
const reportBackgroundError = (
|
|
1589
|
+
const reportBackgroundError = (
|
|
1590
|
+
error: unknown,
|
|
1591
|
+
aborts?: AbortReasonClassifier,
|
|
1592
|
+
): void => {
|
|
1593
|
+
if (aborts?.isAbortReason(error)) return;
|
|
1513
1594
|
try {
|
|
1514
|
-
options.onBackgroundError?.(error);
|
|
1595
|
+
options.onBackgroundError?.(error, aborts);
|
|
1515
1596
|
} catch {
|
|
1516
1597
|
// Background observers are a terminal sink and cannot own cleanup.
|
|
1517
1598
|
}
|
|
1518
1599
|
};
|
|
1519
1600
|
|
|
1520
|
-
const reportControlPlaneError = (
|
|
1601
|
+
const reportControlPlaneError = (
|
|
1602
|
+
error: unknown,
|
|
1603
|
+
aborts?: AbortReasonClassifier,
|
|
1604
|
+
): void => {
|
|
1605
|
+
if (aborts?.isAbortReason(error)) return;
|
|
1521
1606
|
try {
|
|
1522
|
-
options.onControlPlaneError?.(error);
|
|
1607
|
+
options.onControlPlaneError?.(error, aborts);
|
|
1523
1608
|
} catch (callbackError) {
|
|
1524
1609
|
// Observability callbacks must never prevent terminal cleanup of the
|
|
1525
1610
|
// invocation they are observing.
|
|
1526
|
-
reportBackgroundError(callbackError);
|
|
1611
|
+
reportBackgroundError(callbackError, aborts);
|
|
1527
1612
|
}
|
|
1528
1613
|
};
|
|
1529
1614
|
|
|
@@ -1578,18 +1663,31 @@ export function createNestedPlaybookBridge<
|
|
|
1578
1663
|
if (current === active) current = undefined;
|
|
1579
1664
|
};
|
|
1580
1665
|
|
|
1666
|
+
// A failure causally identical to an applicable abort reason is the
|
|
1667
|
+
// cancellation's own evidence, never a control-plane error.
|
|
1668
|
+
const reportNonAbortControlError = (
|
|
1669
|
+
error: unknown,
|
|
1670
|
+
aborts: AbortReasonClassifier,
|
|
1671
|
+
): void => {
|
|
1672
|
+
reportControlPlaneError(error, aborts);
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1581
1675
|
const emitFinish = async (
|
|
1582
1676
|
active: ActiveCall,
|
|
1583
1677
|
result: PlaybookCallResult,
|
|
1678
|
+
aborts: AbortReasonClassifier,
|
|
1584
1679
|
): Promise<void> => {
|
|
1585
|
-
await options.emitFinished(
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1680
|
+
await options.emitFinished(
|
|
1681
|
+
{
|
|
1682
|
+
callId: active.callId,
|
|
1683
|
+
stateId: active.input.stateId,
|
|
1684
|
+
playbookId: active.input.playbookId,
|
|
1685
|
+
text: active.input.text,
|
|
1686
|
+
result,
|
|
1687
|
+
},
|
|
1688
|
+
aborts,
|
|
1689
|
+
);
|
|
1690
|
+
await options.drain(aborts);
|
|
1593
1691
|
};
|
|
1594
1692
|
|
|
1595
1693
|
const finishImmediate = async (
|
|
@@ -1598,20 +1696,25 @@ export function createNestedPlaybookBridge<
|
|
|
1598
1696
|
controlError?: unknown,
|
|
1599
1697
|
resultAfterAbortCleanup?: () => PlaybookCallResult,
|
|
1600
1698
|
): Promise<JsonValue | undefined> => {
|
|
1699
|
+
const aborts = active.aborts;
|
|
1601
1700
|
let effectiveResult = result;
|
|
1602
1701
|
let cleanupControlError: unknown;
|
|
1603
1702
|
if (result.status === 'aborted' || active.signal.aborted) {
|
|
1604
1703
|
try {
|
|
1605
|
-
await drainPlaybookAbortCleanups(active.signal);
|
|
1704
|
+
await drainPlaybookAbortCleanups(active.signal, aborts);
|
|
1606
1705
|
} catch (error) {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1706
|
+
// A cleanup rejection identical to an applicable abort reason is
|
|
1707
|
+
// the cancellation's own evidence — no latch, no result override.
|
|
1708
|
+
if (!aborts.isAbortReason(error)) {
|
|
1709
|
+
cleanupControlError = error;
|
|
1710
|
+
reportControlPlaneError(error, aborts);
|
|
1711
|
+
effectiveResult = resultFromThrown(
|
|
1712
|
+
active.input.playbookId,
|
|
1713
|
+
active.childSessionId,
|
|
1714
|
+
error,
|
|
1715
|
+
false,
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1615
1718
|
}
|
|
1616
1719
|
if (cleanupControlError === undefined && resultAfterAbortCleanup) {
|
|
1617
1720
|
effectiveResult = resultAfterAbortCleanup();
|
|
@@ -1619,15 +1722,16 @@ export function createNestedPlaybookBridge<
|
|
|
1619
1722
|
}
|
|
1620
1723
|
let finishControlError: unknown;
|
|
1621
1724
|
try {
|
|
1622
|
-
await emitFinish(active, effectiveResult);
|
|
1725
|
+
await emitFinish(active, effectiveResult, aborts);
|
|
1623
1726
|
} catch (error) {
|
|
1624
|
-
|
|
1727
|
+
reportNonAbortControlError(error, aborts);
|
|
1625
1728
|
finishControlError = error;
|
|
1626
1729
|
} finally {
|
|
1627
1730
|
// An immediate call can never be resumed. Even when its finish
|
|
1628
1731
|
// emission fails, do not leave a permanently unresumable call in the
|
|
1629
1732
|
// bridge and prevent disposal or a later invocation.
|
|
1630
1733
|
clear(active);
|
|
1734
|
+
options.bindActorSettlement?.(aborts);
|
|
1631
1735
|
}
|
|
1632
1736
|
if (controlError !== undefined) throw controlError;
|
|
1633
1737
|
if (cleanupControlError !== undefined) throw cleanupControlError;
|
|
@@ -1639,6 +1743,7 @@ export function createNestedPlaybookBridge<
|
|
|
1639
1743
|
active: ActiveCall,
|
|
1640
1744
|
result: PlaybookCallResult,
|
|
1641
1745
|
controlError?: unknown,
|
|
1746
|
+
aborts: AbortReasonClassifier = active.aborts,
|
|
1642
1747
|
): Promise<void> => {
|
|
1643
1748
|
if (active.phase === 'settling' && active.settlement) {
|
|
1644
1749
|
await active.settlement;
|
|
@@ -1656,37 +1761,45 @@ export function createNestedPlaybookBridge<
|
|
|
1656
1761
|
effectiveResult = resultFromThrown(
|
|
1657
1762
|
active.input.playbookId,
|
|
1658
1763
|
active.childSessionId,
|
|
1659
|
-
active.signal.reason
|
|
1660
|
-
new Error('Nested playbook invocation aborted'),
|
|
1764
|
+
active.signal.reason,
|
|
1661
1765
|
true,
|
|
1662
1766
|
);
|
|
1663
1767
|
}
|
|
1664
1768
|
try {
|
|
1665
|
-
await drainPlaybookAbortCleanups(active.signal);
|
|
1769
|
+
await drainPlaybookAbortCleanups(active.signal, aborts);
|
|
1666
1770
|
} catch (cleanupError) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1771
|
+
// A cleanup rejection identical to an applicable abort reason is
|
|
1772
|
+
// the cancellation's own evidence — no latch, no result override.
|
|
1773
|
+
if (!aborts.isAbortReason(cleanupError)) {
|
|
1774
|
+
cleanupControlError = cleanupError;
|
|
1775
|
+
reportControlPlaneError(cleanupError, aborts);
|
|
1776
|
+
effectiveResult = resultFromThrown(
|
|
1777
|
+
active.input.playbookId,
|
|
1778
|
+
active.childSessionId,
|
|
1779
|
+
cleanupError,
|
|
1780
|
+
false,
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1675
1783
|
}
|
|
1676
1784
|
}
|
|
1677
1785
|
try {
|
|
1678
|
-
await emitFinish(active, effectiveResult);
|
|
1786
|
+
await emitFinish(active, effectiveResult, aborts);
|
|
1679
1787
|
} catch (error) {
|
|
1680
1788
|
// A finish event is the durable return boundary. If it cannot be
|
|
1681
1789
|
// emitted and drained, the child result must not remain retryable:
|
|
1682
1790
|
// clear the identity and fail the promise actor so its parent takes
|
|
1683
|
-
// onError instead of observing a phantom suspended child.
|
|
1684
|
-
|
|
1791
|
+
// onError instead of observing a phantom suspended child. A finish
|
|
1792
|
+
// rejection that is an applicable abort reason — the invocation's
|
|
1793
|
+
// or the settling resume's — evidences cancellation, not a
|
|
1794
|
+
// control-plane failure (slc/link.md §Abort).
|
|
1795
|
+
reportControlPlaneError(error, aborts);
|
|
1685
1796
|
clear(active);
|
|
1797
|
+
options.bindActorSettlement?.(aborts);
|
|
1686
1798
|
active.deferred.reject(error);
|
|
1687
1799
|
throw error;
|
|
1688
1800
|
}
|
|
1689
1801
|
clear(active);
|
|
1802
|
+
options.bindActorSettlement?.(aborts);
|
|
1690
1803
|
if (controlError !== undefined) {
|
|
1691
1804
|
active.deferred.reject(controlError);
|
|
1692
1805
|
} else if (cleanupControlError !== undefined) {
|
|
@@ -1720,6 +1833,7 @@ export function createNestedPlaybookBridge<
|
|
|
1720
1833
|
active.restoreRolledBack = true;
|
|
1721
1834
|
clear(active);
|
|
1722
1835
|
usedCallIds.delete(active.callId);
|
|
1836
|
+
options.bindActorSettlement?.(active.aborts);
|
|
1723
1837
|
active.deferred.reject(error);
|
|
1724
1838
|
return active;
|
|
1725
1839
|
};
|
|
@@ -1733,11 +1847,11 @@ export function createNestedPlaybookBridge<
|
|
|
1733
1847
|
const result = resultFromThrown(
|
|
1734
1848
|
active.input.playbookId,
|
|
1735
1849
|
active.childSessionId,
|
|
1736
|
-
active.signal.reason
|
|
1850
|
+
active.signal.reason,
|
|
1737
1851
|
true,
|
|
1738
1852
|
);
|
|
1739
1853
|
void settlePending(active, result).catch((error: unknown) => {
|
|
1740
|
-
reportBackgroundError(error);
|
|
1854
|
+
reportBackgroundError(error, active.aborts);
|
|
1741
1855
|
});
|
|
1742
1856
|
};
|
|
1743
1857
|
active.abortListener = abortListener;
|
|
@@ -1750,7 +1864,7 @@ export function createNestedPlaybookBridge<
|
|
|
1750
1864
|
try {
|
|
1751
1865
|
listener(pendingCall);
|
|
1752
1866
|
} catch (error) {
|
|
1753
|
-
reportBackgroundError(error);
|
|
1867
|
+
reportBackgroundError(error, active.aborts);
|
|
1754
1868
|
}
|
|
1755
1869
|
}
|
|
1756
1870
|
if (active.signal.aborted) abortListener();
|
|
@@ -1834,10 +1948,17 @@ export function createNestedPlaybookBridge<
|
|
|
1834
1948
|
}
|
|
1835
1949
|
const controller = new AbortController();
|
|
1836
1950
|
let callSignal: AbortSignal;
|
|
1951
|
+
let callAborts: AbortReasonClassifier;
|
|
1837
1952
|
try {
|
|
1953
|
+
const boundarySignal = options.getBoundarySignal?.();
|
|
1838
1954
|
callSignal = combineAbortSignals(
|
|
1839
1955
|
invocationSignal,
|
|
1840
|
-
|
|
1956
|
+
boundarySignal,
|
|
1957
|
+
controller.signal,
|
|
1958
|
+
);
|
|
1959
|
+
callAborts = createAbortReasonClassifier(
|
|
1960
|
+
invocationSignal,
|
|
1961
|
+
boundarySignal,
|
|
1841
1962
|
controller.signal,
|
|
1842
1963
|
);
|
|
1843
1964
|
} catch (error) {
|
|
@@ -1854,6 +1975,7 @@ export function createNestedPlaybookBridge<
|
|
|
1854
1975
|
finished: deferred<void>(),
|
|
1855
1976
|
controller,
|
|
1856
1977
|
signal: callSignal,
|
|
1978
|
+
aborts: callAborts,
|
|
1857
1979
|
phase: 'restoring',
|
|
1858
1980
|
childSessionId: seed.childSessionId,
|
|
1859
1981
|
};
|
|
@@ -1872,8 +1994,7 @@ export function createNestedPlaybookBridge<
|
|
|
1872
1994
|
}
|
|
1873
1995
|
rollbackRestoredCall(
|
|
1874
1996
|
mode,
|
|
1875
|
-
active.signal.reason
|
|
1876
|
-
new Error('Restored nested playbook invocation aborted'),
|
|
1997
|
+
active.signal.reason,
|
|
1877
1998
|
);
|
|
1878
1999
|
};
|
|
1879
2000
|
active.abortListener = restoreAbortListener;
|
|
@@ -1915,10 +2036,17 @@ export function createNestedPlaybookBridge<
|
|
|
1915
2036
|
usedCallIds.add(callId);
|
|
1916
2037
|
const controller = new AbortController();
|
|
1917
2038
|
let callSignal: AbortSignal;
|
|
2039
|
+
let callAborts: AbortReasonClassifier;
|
|
1918
2040
|
try {
|
|
2041
|
+
const boundarySignal = options.getBoundarySignal?.();
|
|
1919
2042
|
callSignal = combineAbortSignals(
|
|
1920
2043
|
invocationSignal,
|
|
1921
|
-
|
|
2044
|
+
boundarySignal,
|
|
2045
|
+
controller.signal,
|
|
2046
|
+
);
|
|
2047
|
+
callAborts = createAbortReasonClassifier(
|
|
2048
|
+
invocationSignal,
|
|
2049
|
+
boundarySignal,
|
|
1922
2050
|
controller.signal,
|
|
1923
2051
|
);
|
|
1924
2052
|
} catch (error) {
|
|
@@ -1931,6 +2059,7 @@ export function createNestedPlaybookBridge<
|
|
|
1931
2059
|
finished: deferred<void>(),
|
|
1932
2060
|
controller,
|
|
1933
2061
|
signal: callSignal,
|
|
2062
|
+
aborts: callAborts,
|
|
1934
2063
|
phase: 'starting',
|
|
1935
2064
|
};
|
|
1936
2065
|
current = active;
|
|
@@ -1940,40 +2069,56 @@ export function createNestedPlaybookBridge<
|
|
|
1940
2069
|
// for their entering state. Yield through the runtime's global queue so
|
|
1941
2070
|
// that transition/status telemetry is enqueued before call.started.
|
|
1942
2071
|
try {
|
|
1943
|
-
await options.drain();
|
|
2072
|
+
await options.drain(active.aborts);
|
|
1944
2073
|
} catch (error) {
|
|
1945
|
-
|
|
2074
|
+
reportNonAbortControlError(error, active.aborts);
|
|
1946
2075
|
clear(active);
|
|
1947
2076
|
throw error;
|
|
1948
2077
|
}
|
|
1949
2078
|
try {
|
|
1950
|
-
await options.emitStarted(
|
|
2079
|
+
await options.emitStarted(
|
|
2080
|
+
{ callId, ...normalizedInput },
|
|
2081
|
+
active.aborts,
|
|
2082
|
+
);
|
|
1951
2083
|
} catch (error) {
|
|
1952
|
-
|
|
2084
|
+
// A start-sink rejection identical to the applicable abort
|
|
2085
|
+
// reason is the cancellation itself: the pair finishes
|
|
2086
|
+
// `aborted` and nothing is reported (slc/link.md §Abort).
|
|
2087
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
2088
|
+
? undefined
|
|
2089
|
+
: error;
|
|
2090
|
+
if (controlError !== undefined) {
|
|
2091
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
2092
|
+
}
|
|
1953
2093
|
return await finishImmediate(
|
|
1954
2094
|
active,
|
|
1955
2095
|
resultFromThrown(
|
|
1956
2096
|
normalizedInput.playbookId,
|
|
1957
2097
|
undefined,
|
|
1958
2098
|
error,
|
|
1959
|
-
|
|
2099
|
+
controlError === undefined,
|
|
1960
2100
|
),
|
|
1961
|
-
|
|
2101
|
+
controlError,
|
|
1962
2102
|
);
|
|
1963
2103
|
}
|
|
1964
2104
|
try {
|
|
1965
|
-
await options.drain();
|
|
2105
|
+
await options.drain(active.aborts);
|
|
1966
2106
|
} catch (error) {
|
|
1967
|
-
|
|
2107
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
2108
|
+
? undefined
|
|
2109
|
+
: error;
|
|
2110
|
+
if (controlError !== undefined) {
|
|
2111
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
2112
|
+
}
|
|
1968
2113
|
return await finishImmediate(
|
|
1969
2114
|
active,
|
|
1970
2115
|
resultFromThrown(
|
|
1971
2116
|
normalizedInput.playbookId,
|
|
1972
2117
|
undefined,
|
|
1973
2118
|
error,
|
|
1974
|
-
|
|
2119
|
+
controlError === undefined,
|
|
1975
2120
|
),
|
|
1976
|
-
|
|
2121
|
+
controlError,
|
|
1977
2122
|
);
|
|
1978
2123
|
}
|
|
1979
2124
|
|
|
@@ -2002,7 +2147,7 @@ export function createNestedPlaybookBridge<
|
|
|
2002
2147
|
const openingCleanup = starting.then(
|
|
2003
2148
|
() => undefined,
|
|
2004
2149
|
(error: unknown) => {
|
|
2005
|
-
if (isAbortReason(error
|
|
2150
|
+
if (active.aborts.isAbortReason(error)) return;
|
|
2006
2151
|
throw error;
|
|
2007
2152
|
},
|
|
2008
2153
|
);
|
|
@@ -2019,19 +2164,23 @@ export function createNestedPlaybookBridge<
|
|
|
2019
2164
|
}
|
|
2020
2165
|
rawStart = await withAbort(starting, active.signal);
|
|
2021
2166
|
} catch (error) {
|
|
2022
|
-
const controlError = active.
|
|
2023
|
-
|
|
2167
|
+
const controlError = active.aborts.isAbortReason(error)
|
|
2168
|
+
? undefined
|
|
2169
|
+
: error;
|
|
2170
|
+
if (controlError !== undefined) {
|
|
2171
|
+
reportControlPlaneError(controlError, active.aborts);
|
|
2172
|
+
}
|
|
2024
2173
|
const result = resultFromThrown(
|
|
2025
2174
|
normalizedInput.playbookId,
|
|
2026
2175
|
undefined,
|
|
2027
2176
|
error,
|
|
2028
|
-
active.signal.aborted,
|
|
2177
|
+
controlError === undefined && active.signal.aborted,
|
|
2029
2178
|
);
|
|
2030
2179
|
return await finishImmediate(
|
|
2031
2180
|
active,
|
|
2032
2181
|
result,
|
|
2033
2182
|
controlError,
|
|
2034
|
-
active.signal.aborted
|
|
2183
|
+
controlError === undefined && active.signal.aborted
|
|
2035
2184
|
? () =>
|
|
2036
2185
|
resultFromThrown(
|
|
2037
2186
|
normalizedInput.playbookId,
|
|
@@ -2208,9 +2357,7 @@ export function createNestedPlaybookBridge<
|
|
|
2208
2357
|
}
|
|
2209
2358
|
const active = mode.active;
|
|
2210
2359
|
if (active.signal.aborted) {
|
|
2211
|
-
const error =
|
|
2212
|
-
active.signal.reason ??
|
|
2213
|
-
new Error('Restored nested playbook invocation aborted');
|
|
2360
|
+
const error = active.signal.reason;
|
|
2214
2361
|
rollbackRestoredCall(mode, error);
|
|
2215
2362
|
restoreMode = undefined;
|
|
2216
2363
|
throw error;
|
|
@@ -2235,7 +2382,7 @@ export function createNestedPlaybookBridge<
|
|
|
2235
2382
|
try {
|
|
2236
2383
|
listener(pendingCall);
|
|
2237
2384
|
} catch (error) {
|
|
2238
|
-
reportBackgroundError(error);
|
|
2385
|
+
reportBackgroundError(error, current?.aborts);
|
|
2239
2386
|
}
|
|
2240
2387
|
}
|
|
2241
2388
|
return () => pendingListeners.delete(listener);
|
|
@@ -2277,8 +2424,20 @@ export function createNestedPlaybookBridge<
|
|
|
2277
2424
|
}
|
|
2278
2425
|
throw error;
|
|
2279
2426
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2427
|
+
// A resume whose signal is already aborted delivers nothing: the
|
|
2428
|
+
// validated child result is not consumed, no finish is emitted, and
|
|
2429
|
+
// the pending call survives for a later resume with a fresh signal
|
|
2430
|
+
// (slc/link.md §Nested playbook bridge). Identity and validation
|
|
2431
|
+
// control errors above still win — they are the caller's defects.
|
|
2432
|
+
if (signal.aborted) {
|
|
2433
|
+
throw signal.reason;
|
|
2434
|
+
}
|
|
2435
|
+
const resumeAborts = createAbortReasonClassifier(
|
|
2436
|
+
active.aborts,
|
|
2437
|
+
signal,
|
|
2438
|
+
);
|
|
2439
|
+
options.bindResumeSignal?.(signal, resumeAborts);
|
|
2440
|
+
await settlePending(active, validatedResult, undefined, resumeAborts);
|
|
2282
2441
|
},
|
|
2283
2442
|
abortPending,
|
|
2284
2443
|
async dispose() {
|