@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.
Files changed (30) hide show
  1. package/README.md +3 -3
  2. package/docs/cli.md +15 -15
  3. package/docs/configuration.md +13 -8
  4. package/docs/embedding.md +7 -2
  5. package/package.json +1 -1
  6. package/reference/sdlc/captain.playbook/captain.playbook.js +14 -3
  7. package/reference/sdlc/captain.playbook/captain.playbook.ts +18 -4
  8. package/reference/sdlc/code.playbook/code.fsm.d.ts +4 -1
  9. package/reference/sdlc/code.playbook/code.fsm.js +11 -4
  10. package/reference/sdlc/code.playbook/code.fsm.ts +12 -4
  11. package/reference/sdlc/code.playbook/code.playbook.js +14 -3
  12. package/reference/sdlc/code.playbook/code.playbook.ts +13 -3
  13. package/reference/sdlc/code.playbook/playbook-captain.js +44 -10
  14. package/reference/sdlc/code.playbook/playbook-captain.ts +47 -10
  15. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
  16. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +2 -0
  17. package/reference/sdlc/decide.playbook/decide.playbook.js +299 -117
  18. package/reference/sdlc/decide.playbook/decide.playbook.ts +395 -131
  19. package/reference/sdlc/review.playbook/review.playbook.js +14 -3
  20. package/reference/sdlc/review.playbook/review.playbook.ts +13 -3
  21. package/slc/gears2fsm.md +19 -2
  22. package/slc/link.md +184 -42
  23. package/src/runtime.d.ts +1 -0
  24. package/src/runtime.ts +1 -0
  25. package/src/xstate-playbook-runtime.d.ts +13 -3
  26. package/src/xstate-playbook-runtime.js +732 -251
  27. package/src/xstate-playbook-runtime.ts +873 -280
  28. package/src/xstate-runtime.d.ts +17 -7
  29. package/src/xstate-runtime.js +135 -57
  30. package/src/xstate-runtime.ts +243 -84
@@ -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
- signal.aborted &&
74
- (error === signal.reason || normalizeError(error).name === 'AbortError')
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(signal: AbortSignal): Promise<void> {
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 (outcome.status === 'rejected') failures.push(outcome.reason);
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
- description: string;
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
- const description = requireNonEmptyString(
722
- meta.playbook.description,
723
- `${nodeId}.meta.playbook.description`,
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 (previous && previous.description !== description) {
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
- byStateId.set(stateId, { stateId, description });
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(event: PlaybookCallStarted): Promise<void>;
1110
- emitFinished(event: PlaybookCallFinished): Promise<void>;
1111
- drain(): Promise<void>;
1112
- bindResumeSignal?(signal: AbortSignal): void;
1113
- onControlPlaneError?(error: unknown): void;
1114
- onBackgroundError?(error: unknown): void;
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 = (error: unknown): void => {
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 = (error: unknown): void => {
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
- callId: active.callId,
1587
- stateId: active.input.stateId,
1588
- playbookId: active.input.playbookId,
1589
- text: active.input.text,
1590
- result,
1591
- });
1592
- await options.drain();
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
- cleanupControlError = error;
1608
- reportControlPlaneError(error);
1609
- effectiveResult = resultFromThrown(
1610
- active.input.playbookId,
1611
- active.childSessionId,
1612
- error,
1613
- false,
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
- reportControlPlaneError(error);
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
- cleanupControlError = cleanupError;
1668
- reportControlPlaneError(cleanupError);
1669
- effectiveResult = resultFromThrown(
1670
- active.input.playbookId,
1671
- active.childSessionId,
1672
- cleanupError,
1673
- false,
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
- reportControlPlaneError(error);
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 ?? new Error('Nested playbook invocation aborted'),
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
- options.getBoundarySignal?.(),
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
- options.getBoundarySignal?.(),
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
- reportControlPlaneError(error);
2074
+ reportNonAbortControlError(error, active.aborts);
1946
2075
  clear(active);
1947
2076
  throw error;
1948
2077
  }
1949
2078
  try {
1950
- await options.emitStarted({ callId, ...normalizedInput });
2079
+ await options.emitStarted(
2080
+ { callId, ...normalizedInput },
2081
+ active.aborts,
2082
+ );
1951
2083
  } catch (error) {
1952
- reportControlPlaneError(error);
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
- false,
2099
+ controlError === undefined,
1960
2100
  ),
1961
- error,
2101
+ controlError,
1962
2102
  );
1963
2103
  }
1964
2104
  try {
1965
- await options.drain();
2105
+ await options.drain(active.aborts);
1966
2106
  } catch (error) {
1967
- reportControlPlaneError(error);
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
- false,
2119
+ controlError === undefined,
1975
2120
  ),
1976
- error,
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, active.signal)) return;
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.signal.aborted ? undefined : error;
2023
- if (controlError !== undefined) reportControlPlaneError(controlError);
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
- options.bindResumeSignal?.(signal);
2281
- await settlePending(active, validatedResult);
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() {