@bastani/atomic 0.9.3-alpha.1 → 0.9.3-alpha.3

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 (175) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/builtin/cursor/CHANGELOG.md +21 -0
  3. package/dist/builtin/cursor/README.md +2 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/cursor/src/cursor-models-raw.json +2 -9
  6. package/dist/builtin/cursor/src/model-mapper.ts +14 -3
  7. package/dist/builtin/cursor/src/proto/protobuf-codec-base64.ts +22 -0
  8. package/dist/builtin/cursor/src/proto/protobuf-codec-request.ts +53 -13
  9. package/dist/builtin/cursor/src/proto/protobuf-codec-wire.ts +24 -7
  10. package/dist/builtin/cursor/src/proto/protobuf-codec.ts +3 -2
  11. package/dist/builtin/cursor/src/stream.ts +5 -11
  12. package/dist/builtin/cursor/src/transport-types.ts +3 -0
  13. package/dist/builtin/cursor/src/transport.ts +1 -0
  14. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  15. package/dist/builtin/intercom/package.json +1 -1
  16. package/dist/builtin/mcp/CHANGELOG.md +6 -0
  17. package/dist/builtin/mcp/package.json +1 -1
  18. package/dist/builtin/subagents/CHANGELOG.md +15 -0
  19. package/dist/builtin/subagents/package.json +1 -1
  20. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -0
  21. package/dist/builtin/subagents/src/extension/index.ts +6 -3
  22. package/dist/builtin/subagents/src/extension/schemas.ts +0 -5
  23. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +1 -4
  24. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +15 -1
  25. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +35 -1
  26. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -2
  27. package/dist/builtin/subagents/src/shared/types-async.ts +1 -0
  28. package/dist/builtin/subagents/src/slash/prompt-template-bridge.ts +27 -5
  29. package/dist/builtin/subagents/src/tui/render-layout.ts +27 -4
  30. package/dist/builtin/subagents/src/tui/render-result-animation.ts +22 -31
  31. package/dist/builtin/subagents/src/tui/render-result-compact.ts +6 -6
  32. package/dist/builtin/subagents/src/tui/render-result.ts +20 -19
  33. package/dist/builtin/subagents/src/tui/render-status-progress.ts +3 -3
  34. package/dist/builtin/subagents/src/tui/render-widget.ts +46 -7
  35. package/dist/builtin/subagents/src/tui/render.ts +2 -2
  36. package/dist/builtin/web-access/CHANGELOG.md +6 -0
  37. package/dist/builtin/web-access/package.json +1 -1
  38. package/dist/builtin/workflows/CHANGELOG.md +49 -0
  39. package/dist/builtin/workflows/README.md +1 -1
  40. package/dist/builtin/workflows/package.json +1 -1
  41. package/dist/builtin/workflows/src/authoring.d.ts +1 -1
  42. package/dist/builtin/workflows/src/durable/backend.ts +343 -0
  43. package/dist/builtin/workflows/src/durable/child-primitive.ts +79 -0
  44. package/dist/builtin/workflows/src/durable/dbos-backend.ts +421 -0
  45. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +171 -0
  46. package/dist/builtin/workflows/src/durable/factory.ts +96 -0
  47. package/dist/builtin/workflows/src/durable/file-backend.ts +433 -0
  48. package/dist/builtin/workflows/src/durable/index.ts +73 -0
  49. package/dist/builtin/workflows/src/durable/resume-catalog.ts +217 -0
  50. package/dist/builtin/workflows/src/durable/resume-runtime.ts +299 -0
  51. package/dist/builtin/workflows/src/durable/scoped-backend.ts +171 -0
  52. package/dist/builtin/workflows/src/durable/stage-primitive.ts +284 -0
  53. package/dist/builtin/workflows/src/durable/tool-primitive.ts +180 -0
  54. package/dist/builtin/workflows/src/durable/types.ts +168 -0
  55. package/dist/builtin/workflows/src/durable/ui-primitive.ts +96 -0
  56. package/dist/builtin/workflows/src/engine/options.ts +3 -0
  57. package/dist/builtin/workflows/src/engine/primitives/parallel.ts +2 -2
  58. package/dist/builtin/workflows/src/engine/primitives/task.ts +4 -4
  59. package/dist/builtin/workflows/src/engine/primitives/ui.ts +22 -8
  60. package/dist/builtin/workflows/src/engine/primitives/workflow.ts +8 -0
  61. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +69 -0
  62. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +31 -0
  63. package/dist/builtin/workflows/src/engine/run.ts +148 -6
  64. package/dist/builtin/workflows/src/engine/runtime.ts +8 -2
  65. package/dist/builtin/workflows/src/extension/extension-factory.ts +6 -12
  66. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +5 -1
  67. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  68. package/dist/builtin/workflows/src/extension/runtime.ts +48 -9
  69. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +143 -4
  70. package/dist/builtin/workflows/src/runs/background/quit.ts +61 -0
  71. package/dist/builtin/workflows/src/runs/background/status.ts +1 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +74 -33
  74. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +20 -1
  75. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +8 -7
  76. package/dist/builtin/workflows/src/runs/foreground/executor-stage-replay.ts +1 -0
  77. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +1 -1
  78. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +19 -2
  79. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +4 -0
  80. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +10 -10
  81. package/dist/builtin/workflows/src/runs/foreground/stage-runner-options.ts +5 -1
  82. package/dist/builtin/workflows/src/runs/foreground/stage-runner-send-user-message.ts +25 -0
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +3 -0
  84. package/dist/builtin/workflows/src/shared/authoring-contract-stage.d.ts +16 -0
  85. package/dist/builtin/workflows/src/shared/authoring-contract-stage.ts +20 -0
  86. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +23 -1
  87. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +30 -1
  88. package/dist/builtin/workflows/src/shared/store-public-types.ts +6 -2
  89. package/dist/builtin/workflows/src/shared/store-run-methods.ts +12 -6
  90. package/dist/builtin/workflows/src/shared/types.ts +55 -0
  91. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +1 -1
  92. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +41 -0
  93. package/dist/builtin/workflows/src/tui/graph-view-input.ts +82 -24
  94. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-state.ts +22 -2
  96. package/dist/builtin/workflows/src/tui/graph-view-types.ts +4 -5
  97. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -11
  98. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +9 -3
  99. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +11 -2
  100. package/dist/builtin/workflows/src/tui/stage-chat-view-live-events.ts +35 -0
  101. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +51 -17
  102. package/dist/builtin/workflows/src/tui/stage-chat-view-status.ts +36 -0
  103. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +5 -1
  104. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -1
  105. package/dist/builtin/workflows/src/tui/status-list.ts +14 -2
  106. package/dist/builtin/workflows/src/tui/widget.ts +23 -8
  107. package/dist/builtin/workflows/src/tui/workflow-attach-pane-types.ts +5 -4
  108. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +8 -8
  109. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +151 -0
  110. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  111. package/dist/core/extensions/loader-virtual-modules.js +47 -30
  112. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  113. package/dist/core/messages.d.ts +1 -0
  114. package/dist/core/messages.d.ts.map +1 -1
  115. package/dist/core/messages.js +46 -1
  116. package/dist/core/messages.js.map +1 -1
  117. package/dist/core/sdk.d.ts.map +1 -1
  118. package/dist/core/sdk.js +12 -0
  119. package/dist/core/sdk.js.map +1 -1
  120. package/dist/core/session-manager-core.d.ts +15 -7
  121. package/dist/core/session-manager-core.d.ts.map +1 -1
  122. package/dist/core/session-manager-core.js +20 -9
  123. package/dist/core/session-manager-core.js.map +1 -1
  124. package/dist/core/session-manager-entries.d.ts +2 -2
  125. package/dist/core/session-manager-entries.d.ts.map +1 -1
  126. package/dist/core/session-manager-entries.js +9 -3
  127. package/dist/core/session-manager-entries.js.map +1 -1
  128. package/dist/core/session-manager-history.d.ts.map +1 -1
  129. package/dist/core/session-manager-history.js +2 -1
  130. package/dist/core/session-manager-history.js.map +1 -1
  131. package/dist/core/session-manager-list.d.ts +3 -3
  132. package/dist/core/session-manager-list.d.ts.map +1 -1
  133. package/dist/core/session-manager-list.js +27 -8
  134. package/dist/core/session-manager-list.js.map +1 -1
  135. package/dist/core/session-manager-storage.d.ts +3 -1
  136. package/dist/core/session-manager-storage.d.ts.map +1 -1
  137. package/dist/core/session-manager-storage.js +55 -12
  138. package/dist/core/session-manager-storage.js.map +1 -1
  139. package/dist/core/session-manager-tool-dependencies.d.ts +10 -0
  140. package/dist/core/session-manager-tool-dependencies.d.ts.map +1 -0
  141. package/dist/core/session-manager-tool-dependencies.js +133 -0
  142. package/dist/core/session-manager-tool-dependencies.js.map +1 -0
  143. package/dist/core/session-manager-types.d.ts +22 -0
  144. package/dist/core/session-manager-types.d.ts.map +1 -1
  145. package/dist/core/session-manager-types.js.map +1 -1
  146. package/dist/core/session-manager.d.ts +2 -2
  147. package/dist/core/session-manager.d.ts.map +1 -1
  148. package/dist/core/session-manager.js +1 -1
  149. package/dist/core/session-manager.js.map +1 -1
  150. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts +1 -0
  151. package/dist/modes/interactive/components/chat-session-host-runtime.d.ts.map +1 -1
  152. package/dist/modes/interactive/components/chat-session-host-runtime.js +12 -0
  153. package/dist/modes/interactive/components/chat-session-host-runtime.js.map +1 -1
  154. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts +4 -0
  155. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.d.ts.map +1 -0
  156. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js +131 -0
  157. package/dist/modes/interactive/components/chat-session-host-terminal-cleanup.js.map +1 -0
  158. package/dist/modes/interactive/components/chat-session-host.d.ts +2 -0
  159. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/chat-session-host.js +7 -1
  161. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  162. package/dist/modes/interactive/components/chat-transcript.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/chat-transcript.js +15 -4
  164. package/dist/modes/interactive/components/chat-transcript.js.map +1 -1
  165. package/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  166. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  167. package/dist/modes/interactive/components/tool-execution.js +26 -0
  168. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  169. package/docs/compaction.md +2 -0
  170. package/docs/models.md +1 -1
  171. package/docs/providers.md +2 -1
  172. package/docs/session-format.md +6 -0
  173. package/docs/sessions.md +6 -0
  174. package/docs/workflows.md +105 -3
  175. package/package.json +4 -3
@@ -0,0 +1,96 @@
1
+ /** Durable `ctx.ui` wrapper with collision-resistant prompt identities. */
2
+
3
+ import type {
4
+ WorkflowCustomUiFactory,
5
+ WorkflowCustomUiOptions,
6
+ WorkflowUIContext,
7
+ } from "../shared/authoring-contract-ui.js";
8
+ import type { WorkflowSerializableValue } from "../shared/types.js";
9
+ import type { DurableWorkflowBackend } from "./backend.js";
10
+ import { durableHash } from "./backend.js";
11
+ import type { DurableUiCheckpoint, UiPromptKind } from "./types.js";
12
+ import { recordCheckpointDurably } from "./tool-primitive.js";
13
+
14
+ export interface DurableUiDeps {
15
+ readonly workflowId: string;
16
+ readonly backend: DurableWorkflowBackend;
17
+ readonly nextCheckpointId: () => string;
18
+ }
19
+
20
+ export function wrapUiWithDurable(base: WorkflowUIContext, deps: DurableUiDeps): WorkflowUIContext {
21
+ const ordinals = new Map<string, number>();
22
+
23
+ const nextIdentity = (kind: UiPromptKind, message: string, details?: WorkflowSerializableValue): { key: string; hash: string } => {
24
+ const baseKey = durableHash({ kind, message, details: details ?? null });
25
+ const ordinal = (ordinals.get(baseKey) ?? 0) + 1;
26
+ ordinals.set(baseKey, ordinal);
27
+ const identity = { kind, message, details: details ?? null, ordinal };
28
+ return { key: JSON.stringify(identity), hash: durableHash(identity) };
29
+ };
30
+
31
+ const record = async (kind: UiPromptKind, identity: { key: string; hash: string }, response: WorkflowSerializableValue): Promise<void> => {
32
+ const checkpoint: DurableUiCheckpoint = {
33
+ kind: "ui",
34
+ workflowId: deps.workflowId,
35
+ checkpointId: `ui:${identity.hash}`,
36
+ promptKind: kind,
37
+ message: identity.key,
38
+ promptHash: identity.hash,
39
+ response,
40
+ completedAt: Date.now(),
41
+ };
42
+ await recordCheckpointDurably(deps.backend, checkpoint);
43
+ };
44
+
45
+ const cached = (identity: { readonly hash: string }): WorkflowSerializableValue | undefined => deps.backend.getUiResponse(deps.workflowId, identity.hash);
46
+
47
+ const cachedCustom = (identity: { readonly hash: string }): { readonly found: boolean; readonly response?: WorkflowSerializableValue } => {
48
+ const hit = deps.backend.listCheckpoints(deps.workflowId)
49
+ .find((checkpoint) => checkpoint.kind === "ui" && checkpoint.promptHash === identity.hash);
50
+ return hit?.kind === "ui" ? { found: true, response: hit.response } : { found: false };
51
+ };
52
+
53
+ return {
54
+ async input(promptText: string): Promise<string> {
55
+ const identity = nextIdentity("input", promptText);
56
+ const hit = cached(identity);
57
+ if (typeof hit === "string") return hit;
58
+ const response = await base.input(promptText);
59
+ await record("input", identity, response);
60
+ return response;
61
+ },
62
+ async confirm(message: string): Promise<boolean> {
63
+ const identity = nextIdentity("confirm", message);
64
+ const hit = cached(identity);
65
+ if (typeof hit === "boolean") return hit;
66
+ const response = await base.confirm(message);
67
+ await record("confirm", identity, response);
68
+ return response;
69
+ },
70
+ async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
71
+ const identity = nextIdentity("select", message, [...options]);
72
+ const hit = cached(identity);
73
+ if (typeof hit === "string") return hit as T;
74
+ const response = await base.select<T>(message, options);
75
+ await record("select", identity, response);
76
+ return response;
77
+ },
78
+ async editor(initial?: string): Promise<string> {
79
+ const identity = nextIdentity("editor", initial ?? "", initial ?? null);
80
+ const hit = cached(identity);
81
+ if (typeof hit === "string") return hit;
82
+ const response = await base.editor(initial);
83
+ await record("editor", identity, response);
84
+ return response;
85
+ },
86
+ async custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
87
+ const replayIdentity = options?.replayIdentity ?? factory?.name ?? "custom";
88
+ const identity = nextIdentity("custom", replayIdentity, { replayIdentity });
89
+ const hit = cachedCustom(identity);
90
+ if (hit.found) return hit.response as T;
91
+ const response = await base.custom<T>(factory, options);
92
+ await record("custom", identity, response as WorkflowSerializableValue);
93
+ return response;
94
+ },
95
+ };
96
+ }
@@ -9,6 +9,7 @@ export type EngineStageRuntimeOptions = Pick<
9
9
  | "persistence"
10
10
  | "onStageStart"
11
11
  | "onStageEnd"
12
+ | "onStageSession"
12
13
  | "confirmStageReadiness"
13
14
  | "usePromptNodesForUi"
14
15
  >;
@@ -37,4 +38,6 @@ export type EngineChildRunOptions = Pick<
37
38
  | "stageControlRegistry"
38
39
  | "onStageStart"
39
40
  | "onStageEnd"
41
+ | "onStageSession"
42
+ | "durableBackend"
40
43
  >;
@@ -33,11 +33,11 @@ export function createParallelPrimitive(input: {
33
33
  taskWithSharedDefaults(taskOptionsFromStep(step, prompt, taskPrevious(step)), options),
34
34
  parallelScope,
35
35
  );
36
- }, (error) => {
36
+ }, async (error) => {
37
37
  if (!failFastEnabled) return;
38
38
  parallelScope.failed = true;
39
39
  parallelScope.firstFailure = error;
40
- for (const stage of parallelScope.activeStages.values()) stage.skip();
40
+ await Promise.all([...parallelScope.activeStages.values()].map((stage) => stage.skip()));
41
41
  }, {
42
42
  beforeDequeue: input.runtime.exit.throwIfWorkflowExitSelected,
43
43
  beforeMap: input.runtime.exit.throwIfWorkflowExitSelected,
@@ -49,10 +49,10 @@ function createTaskPrimitive(runtime: EngineRuntime): WorkflowTaskPrimitive {
49
49
  ...(stageFailFastScope !== undefined ? { failFastScope: stageFailFastScope } : {}),
50
50
  });
51
51
  const stage = stageHandle.context;
52
- const rawOutput = await stage.prompt(
53
- applyTaskContext(`${taskReadInstruction(resolvedTaskOptions)}${taskPrompt(resolvedTaskOptions)}`, taskPrevious(resolvedTaskOptions)),
54
- taskPromptOptions(resolvedTaskOptions),
55
- );
52
+ const promptText = resolvedTaskOptions.resumeFromSessionFile !== undefined
53
+ ? "Continue"
54
+ : applyTaskContext(`${taskReadInstruction(resolvedTaskOptions)}${taskPrompt(resolvedTaskOptions)}`, taskPrevious(resolvedTaskOptions));
55
+ const rawOutput = await stage.prompt(promptText, taskPromptOptions(resolvedTaskOptions));
56
56
  const structured = typeof rawOutput === "string" ? undefined : rawOutput;
57
57
  const text = truncateTaskOutput(structuredTaskOutputText(rawOutput), resolvedTaskOptions.maxOutput);
58
58
  const sessionId = (() => {
@@ -5,37 +5,51 @@ import type {
5
5
  } from "../../shared/types.js";
6
6
  import type { RunOpts } from "../../runs/foreground/executor-types.js";
7
7
  import { makeHeadlessUnavailableUIContext, normalizeUIContext } from "../../runs/foreground/executor-hil.js";
8
+ import type { WorkflowUIContext as AuthoringWorkflowUIContext } from "../../shared/authoring-contract-ui.js";
9
+ import { wrapUiWithDurable, type DurableUiDeps } from "../../durable/ui-primitive.js";
8
10
 
9
- export function buildExitGatedUiContext(input: {
11
+ export interface BuildExitGatedUiContextInput {
10
12
  readonly opts: RunOpts;
11
- readonly baseFromPromptNodes: () => WorkflowUIContext;
13
+ readonly baseFromPromptNodes: () => AuthoringWorkflowUIContext;
12
14
  readonly throwIfWorkflowExitSelected: () => void;
13
- }): WorkflowUIContext {
15
+ /**
16
+ * Optional durable UI deps. When provided, completed ctx.ui responses are
17
+ * cached durably and replayed on resume instead of re-asking the user.
18
+ *
19
+ * cross-ref: issue #1498 — durable ctx.ui response/pending prompt state.
20
+ */
21
+ readonly durableUi?: DurableUiDeps;
22
+ }
23
+
24
+ export function buildExitGatedUiContext(input: BuildExitGatedUiContextInput): WorkflowUIContext {
14
25
  const base = input.opts.usePromptNodesForUi === true
15
26
  ? input.baseFromPromptNodes()
16
27
  : input.opts.executionMode === "non_interactive" && input.opts.ui === undefined
17
28
  ? makeHeadlessUnavailableUIContext()
18
29
  : normalizeUIContext(input.opts.ui);
30
+ // Wrap the resolved base UI with durable caching when deps are supplied.
31
+ // The durable wrapper is transparent when no cached response exists.
32
+ const durableBase = input.durableUi !== undefined ? wrapUiWithDurable(base, input.durableUi) : base;
19
33
  return {
20
34
  async input(promptText: string): Promise<string> {
21
35
  input.throwIfWorkflowExitSelected();
22
- return await base.input(promptText);
36
+ return await durableBase.input(promptText);
23
37
  },
24
38
  async confirm(message: string): Promise<boolean> {
25
39
  input.throwIfWorkflowExitSelected();
26
- return await base.confirm(message);
40
+ return await durableBase.confirm(message);
27
41
  },
28
42
  async select<T extends string>(message: string, options: readonly T[]): Promise<T> {
29
43
  input.throwIfWorkflowExitSelected();
30
- return await base.select(message, options);
44
+ return await durableBase.select(message, options);
31
45
  },
32
46
  async editor(initial?: string): Promise<string> {
33
47
  input.throwIfWorkflowExitSelected();
34
- return await base.editor(initial);
48
+ return await durableBase.editor(initial);
35
49
  },
36
50
  async custom<T>(factory: WorkflowCustomUiFactory<T>, options?: WorkflowCustomUiOptions): Promise<T> {
37
51
  input.throwIfWorkflowExitSelected();
38
- return await base.custom(factory, options);
52
+ return await durableBase.custom(factory, options);
39
53
  },
40
54
  };
41
55
  }
@@ -17,11 +17,18 @@ import {
17
17
  workflowChildReplaySnapshot,
18
18
  workflowDefinitionRequirementMessage,
19
19
  } from "../../runs/foreground/executor-child-helpers.js";
20
+ import type { DurableScope } from "../../durable/scoped-backend.js";
20
21
 
21
22
  export function createChildWorkflowRunner(input: {
22
23
  readonly runtime: EngineRuntime;
23
24
  readonly resolveWorkflowCwd: () => string;
24
25
  readonly nextWorkflowBoundaryReplayKey: (name: string) => string;
26
+ /**
27
+ * Consume the durable scope published by the durable child primitive for the
28
+ * current invocation, if any. Routes child internal side-effect checkpoints
29
+ * under the root workflow.
30
+ */
31
+ readonly consumeDurableScope?: () => DurableScope | undefined;
25
32
  readonly runWorkflow: <
26
33
  TInputs extends WorkflowInputValues,
27
34
  TRunInputs extends WorkflowInputValues = TInputs,
@@ -100,6 +107,7 @@ export function createChildWorkflowRunner(input: {
100
107
  },
101
108
  signal: childController.signal,
102
109
  deferWorkflowStart: false,
110
+ ...(input.consumeDurableScope !== undefined ? { durableScope: input.consumeDurableScope() } : {}),
103
111
  });
104
112
  boundary.observeChildRun(childRunPromise);
105
113
  const childRun = await childRunPromise;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Durable terminal-status finalization for workflow runs.
3
+ *
4
+ * Extracted from `run()` to keep the engine entrypoint under the file-length
5
+ * gate. Persists the final durable status (cancelled/blocked/skipped/failed/
6
+ * killed) for cross-session resume discovery when the run did not complete
7
+ * normally (normal completion is handled in the run try-block).
8
+ *
9
+ * cross-ref: issue #1498.
10
+ */
11
+
12
+ import type { RunSnapshot } from "../shared/store-types.js";
13
+ import type { WorkflowPersistencePort } from "../shared/types.js";
14
+ import type { DurableWorkflowBackend } from "../durable/backend.js";
15
+ import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
16
+ import type { DurableWorkflowStatus } from "../durable/types.js";
17
+
18
+ export interface DurableTerminalFinalizeInput {
19
+ readonly runId: string;
20
+ readonly runSnapshot: RunSnapshot;
21
+ readonly isRoot: boolean;
22
+ readonly durableBackend: DurableWorkflowBackend;
23
+ readonly persistence?: WorkflowPersistencePort;
24
+ }
25
+
26
+ /**
27
+ * Map and persist the terminal durable status for a root workflow run when the
28
+ * run did not complete normally. Safe to call from a `finally` block: flush
29
+ * failures are logged but never rethrown so they do not mask the original
30
+ * failure/exit status.
31
+ */
32
+ export async function finalizeDurableTerminalStatus(input: DurableTerminalFinalizeInput): Promise<void> {
33
+ if (!input.isRoot) return;
34
+ const status = input.runSnapshot.status;
35
+ const isExitTerminal = input.runSnapshot.exited === true && status !== "running";
36
+ if (status !== "failed" && status !== "killed" && !isExitTerminal) return;
37
+
38
+ const durableStatus = toDurableStatus(status);
39
+ if (durableStatus !== undefined) {
40
+ input.durableBackend.setWorkflowStatus(input.runId, durableStatus, undefined, input.runSnapshot.resumable);
41
+ }
42
+ try {
43
+ await input.durableBackend.flush?.();
44
+ } catch (flushErr) {
45
+ const msg = flushErr instanceof Error ? flushErr.message : String(flushErr);
46
+ console.warn(`atomic-workflows: durable terminal status flush failed: ${msg}`);
47
+ }
48
+ if (input.persistence !== undefined && input.durableBackend.persistent) {
49
+ const cacheEntry = input.durableBackend.toCacheEntry(input.runId);
50
+ if (cacheEntry) persistDurableCacheEntry(input.persistence, cacheEntry);
51
+ }
52
+ }
53
+
54
+ function toDurableStatus(status: RunSnapshot["status"]): DurableWorkflowStatus | undefined {
55
+ switch (status) {
56
+ case "completed":
57
+ case "skipped":
58
+ return "completed";
59
+ case "cancelled":
60
+ case "killed":
61
+ return "cancelled";
62
+ case "failed":
63
+ return "failed";
64
+ case "blocked":
65
+ return "blocked";
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
@@ -0,0 +1,31 @@
1
+ import type { StageSnapshot } from "../shared/store-types.js";
2
+ import type { WorkflowPersistencePort } from "../shared/types.js";
3
+ import type { DurableWorkflowBackend } from "../durable/backend.js";
4
+ import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
5
+ import { recordStageSessionCheckpoint, type DurableStageDeps } from "../durable/stage-primitive.js";
6
+
7
+ export interface DurableStageSessionRecorderInput {
8
+ readonly runId: string;
9
+ readonly deps: DurableStageDeps;
10
+ readonly backend: DurableWorkflowBackend;
11
+ readonly persistence?: WorkflowPersistencePort;
12
+ readonly onStageSession?: (runId: string, snapshot: StageSnapshot) => unknown;
13
+ }
14
+
15
+ export function createDurableStageSessionRecorder(
16
+ input: DurableStageSessionRecorderInput,
17
+ ): (stageRunId: string, snapshot: StageSnapshot) => void {
18
+ return (stageRunId, snapshot) => {
19
+ if (stageRunId === input.runId) {
20
+ void recordStageSessionCheckpoint(input.deps, snapshot).then((recorded) => {
21
+ if (!recorded || !input.persistence || !input.backend.persistent) return;
22
+ const cacheEntry = input.backend.toCacheEntry(input.runId);
23
+ if (cacheEntry) persistDurableCacheEntry(input.persistence, cacheEntry);
24
+ }).catch((error) => {
25
+ const message = error instanceof Error ? error.message : String(error);
26
+ console.warn(`atomic-workflows: durable stage session checkpoint failed: ${message}`);
27
+ });
28
+ }
29
+ void input.onStageSession?.(stageRunId, snapshot);
30
+ };
31
+ }
@@ -4,6 +4,7 @@ import type {
4
4
  WorkflowInputValues,
5
5
  WorkflowOutputValues,
6
6
  WorkflowRunContext,
7
+ WorkflowSerializableValue,
7
8
  } from "../shared/types.js";
8
9
  import type { WorkflowFailure } from "../shared/workflow-failures.js";
9
10
  import { classifyWorkflowFailure } from "../shared/workflow-failures.js";
@@ -17,6 +18,8 @@ import { buildExitGatedUiContext } from "./primitives/ui.js";
17
18
  import { createWorkflowExitManager } from "./primitives/exit.js";
18
19
  import { createWorkflowTaskRunners } from "./primitives/task.js";
19
20
  import { createChildWorkflowRunner } from "./primitives/workflow.js";
21
+ import { createChainPrimitive } from "./primitives/chain.js";
22
+ import { createParallelPrimitive } from "./primitives/parallel.js";
20
23
  import { createRunLimiter } from "../runs/shared/concurrency.js";
21
24
  import { stageControlRegistry as defaultStageControlRegistry } from "../runs/foreground/stage-control-registry.js";
22
25
  import type { RunOpts, RunResult } from "../runs/foreground/executor-types.js";
@@ -37,6 +40,16 @@ import {
37
40
  } from "../runs/foreground/executor-lifecycle.js";
38
41
  import { assertWorkflowRunOutputs, normalizeWorkflowRunOutput } from "../runs/foreground/executor-outputs.js";
39
42
  import { isWorkflowDefinition, workflowDefinitionRequirementMessage } from "../runs/foreground/executor-child-helpers.js";
43
+ import { getDurableBackend } from "../durable/factory.js";
44
+ import { createToolPrimitive, createCheckpointIdGenerator } from "../durable/tool-primitive.js";
45
+ import { persistDurableCacheEntry } from "../durable/resume-catalog.js";
46
+ import { createDurableStagePrimitive, createDurableTaskPrimitive, recordStageCheckpoint, createStageReplayKeyGenerator, recordCachedStageWithTracker } from "../durable/stage-primitive.js";
47
+ import { createDurableChildWorkflowPrimitive } from "../durable/child-primitive.js";
48
+ import { ScopedDurableBackend, type DurableScope } from "../durable/scoped-backend.js";
49
+ import { finalizeDurableTerminalStatus } from "./run-durable-finalize.js";
50
+ import { createDurableStageSessionRecorder } from "./run-durable-stage-session.js";
51
+ import type { DurableWorkflowBackend } from "../durable/backend.js";
52
+ import type { StageSnapshot } from "../shared/store-types.js";
40
53
 
41
54
  function nextEventLoopTurn(): Promise<void> {
42
55
  return new Promise((resolve) => setTimeout(resolve, 0));
@@ -176,6 +189,38 @@ export async function run<
176
189
  classifyExecutorFailure,
177
190
  drainWorkflowExitCleanups: exit.drainWorkflowExitCleanups,
178
191
  });
192
+ // Durable workflow backend — registers this run and wires ctx.tool/ui/stage.
193
+ // Declared early so the stage-end recorder can attach to stageOptions.
194
+ // cross-ref: issue #1498 — DBOS-backed cross-session resumability.
195
+ // Child runs with a durable scope route their internal side-effect
196
+ // checkpoints under the root workflow so interrupted children do not
197
+ // re-execute completed side effects on parent resume.
198
+ const rootBackend: DurableWorkflowBackend = opts.durableBackend ?? getDurableBackend();
199
+ const durableBackend: DurableWorkflowBackend = opts.durableScope !== undefined
200
+ ? new ScopedDurableBackend(rootBackend, opts.durableScope)
201
+ : rootBackend;
202
+ const checkpointIdGenerator = createCheckpointIdGenerator();
203
+ const stageReplayKeyGenerator = createStageReplayKeyGenerator(runId);
204
+ const completedStageReplayKeys = new Map<string, string>();
205
+ const durableStageDeps = {
206
+ workflowId: runId,
207
+ backend: durableBackend,
208
+ nextCheckpointId: checkpointIdGenerator,
209
+ nextReplayKey: stageReplayKeyGenerator,
210
+ replayKeyForCompletedStage: (stage: StageSnapshot) => completedStageReplayKeys.get(stage.id),
211
+ };
212
+ const userOnStageEnd = opts.onStageEnd;
213
+ const durableOnStageEnd = async (stageRunId: string, snapshot: StageSnapshot): Promise<void> => {
214
+ if (stageRunId === runId && snapshot.status === "completed") {
215
+ await recordStageCheckpoint(durableStageDeps, snapshot);
216
+ if (opts.persistence && durableBackend.persistent) {
217
+ const cacheEntry = durableBackend.toCacheEntry(runId);
218
+ if (cacheEntry) persistDurableCacheEntry(opts.persistence, cacheEntry);
219
+ }
220
+ }
221
+ await userOnStageEnd?.(stageRunId, snapshot);
222
+ };
223
+ const durableOnStageSession = createDurableStageSessionRecorder({ runId, deps: durableStageDeps, backend: durableBackend, persistence: opts.persistence, onStageSession: opts.onStageSession });
179
224
  const stageOptions: EngineStageRuntimeOptions = {
180
225
  continuation: opts.continuation,
181
226
  models: opts.models,
@@ -183,7 +228,8 @@ export async function run<
183
228
  defaultSessionDir: opts.defaultSessionDir,
184
229
  persistence: opts.persistence,
185
230
  onStageStart: opts.onStageStart,
186
- onStageEnd: opts.onStageEnd,
231
+ onStageEnd: durableOnStageEnd,
232
+ onStageSession: durableOnStageSession,
187
233
  confirmStageReadiness: opts.confirmStageReadiness,
188
234
  usePromptNodesForUi: opts.usePromptNodesForUi,
189
235
  };
@@ -210,6 +256,8 @@ export async function run<
210
256
  stageControlRegistry: opts.stageControlRegistry,
211
257
  onStageStart: opts.onStageStart,
212
258
  onStageEnd: opts.onStageEnd,
259
+ onStageSession: opts.onStageSession,
260
+ durableBackend,
213
261
  };
214
262
  const runtime = new EngineRuntime({
215
263
  runId,
@@ -238,14 +286,77 @@ export async function run<
238
286
  workflowBoundaryReplayCounts.set(name, next);
239
287
  return `workflow:${name}:${next}`;
240
288
  };
289
+ // Durable child workflow replay keys use a SEPARATE counter so that cache
290
+ // hits (which do not invoke the inner workflow runner) do not desync the
291
+ // ordinal sequence. Without this, repeated ctx.workflow(child) calls would
292
+ // shift replay keys on resume and re-execute completed children.
293
+ // cross-ref: issue #1498.
294
+ const durableChildReplayCounts = new Map<string, number>();
295
+ const nextDurableChildReplayKey = (name: string): string => {
296
+ const next = (durableChildReplayCounts.get(name) ?? 0) + 1;
297
+ durableChildReplayCounts.set(name, next);
298
+ return `workflow:${name}:${next}`;
299
+ };
241
300
  const taskRunners = createWorkflowTaskRunners({ runtime });
301
+ // Durable scope holder: the durable child primitive publishes the scope for
302
+ // the next child invocation; the child runner consumes it when launching the
303
+ // run. This routes child internal side-effect checkpoints under the root.
304
+ let pendingChildDurableScope: DurableScope | undefined;
242
305
  const workflow = createChildWorkflowRunner({
243
306
  runtime,
244
307
  resolveWorkflowCwd,
245
308
  nextWorkflowBoundaryReplayKey,
309
+ consumeDurableScope: () => {
310
+ const scope = pendingChildDurableScope;
311
+ pendingChildDurableScope = undefined;
312
+ return scope;
313
+ },
246
314
  runWorkflow: run,
247
315
  });
248
316
 
317
+ // Durable workflow registration — register this run for cross-session discovery.
318
+ if (opts.continuation === undefined && opts.parentRun === undefined) {
319
+ // New root run: register it durably so a future session can discover it.
320
+ durableBackend.registerWorkflow({
321
+ workflowId: runId,
322
+ name: def.name,
323
+ inputs: resolvedInputs as Record<string, import("../shared/types.js").WorkflowSerializableValue>,
324
+ createdAt: runSnapshot.startedAt,
325
+ status: "running",
326
+ rootWorkflowId: runId,
327
+ resumable: true,
328
+ ...(opts.persistence !== undefined ? { sessionFile: undefined } : {}),
329
+ });
330
+ } else if (opts.parentRun === undefined) {
331
+ // Resuming a root workflow: mark it as running again in the backend.
332
+ durableBackend.setWorkflowStatus(runId, "running");
333
+ }
334
+ const tool = createToolPrimitive({
335
+ workflowId: runId,
336
+ backend: durableBackend,
337
+ nextCheckpointId: checkpointIdGenerator,
338
+ throwIfCancelled: () => {
339
+ if (ownController.signal.aborted) {
340
+ throw new Error("atomic-workflows: workflow cancelled");
341
+ }
342
+ },
343
+ signal: ownController.signal,
344
+ });
345
+
346
+ // Durable ctx.ui wrapper — caches completed user responses so a resumed workflow does not re-ask answered prompts.
347
+ const durableUiDeps = { workflowId: runId, backend: durableBackend, nextCheckpointId: checkpointIdGenerator };
348
+ const recordCachedStage = (name: string, replayKey: string, output: WorkflowSerializableValue): void =>
349
+ recordCachedStageWithTracker(activeStore, tracker, runId, name, replayKey, output, completedStageReplayKeys);
350
+ const durableTask = createDurableTaskPrimitive({
351
+ workflowId: runId, backend: durableBackend,
352
+ nextReplayKey: (stageName) => stageReplayKeyGenerator(stageName), task: taskRunners.task,
353
+ recordCachedTask: (name, replayKey, output) => recordCachedStage(name, replayKey, output),
354
+ });
355
+ const durableWorkflow = createDurableChildWorkflowPrimitive({
356
+ workflowId: runId, rootWorkflowId: opts.parentRun?.rootRunId ?? runId, backend: durableBackend,
357
+ nextReplayKey: nextDurableChildReplayKey, setChildDurableScope: (scope) => { pendingChildDurableScope = scope; },
358
+ recordCachedStage, workflow,
359
+ });
249
360
  const ctx: WorkflowRunContext<TInputs> = {
250
361
  inputs: resolvedInputs as TInputs,
251
362
  get cwd() { return resolveWorkflowCwd(); },
@@ -253,6 +364,7 @@ export async function run<
253
364
  ui: buildExitGatedUiContext({
254
365
  opts,
255
366
  throwIfWorkflowExitSelected: exit.throwIfWorkflowExitSelected,
367
+ durableUi: durableUiDeps,
256
368
  baseFromPromptNodes: () => buildPromptNodeUiAdapter({
257
369
  runId,
258
370
  activeStore,
@@ -267,11 +379,23 @@ export async function run<
267
379
  classifyExecutorFailure,
268
380
  }),
269
381
  }),
270
- stage: runtime.stage,
271
- task: taskRunners.task,
272
- chain: taskRunners.chain,
273
- parallel: taskRunners.parallel,
274
- workflow,
382
+ stage: createDurableStagePrimitive({
383
+ workflowId: runId,
384
+ backend: durableBackend,
385
+ nextReplayKey: (stageName) => stageReplayKeyGenerator(stageName),
386
+ recordCachedStage,
387
+ stage: (name, options, replayKey) => {
388
+ const stage = runtime.stage(name, options);
389
+ const stageId = activeStore.runs().find((r) => r.id === runId)?.stages.at(-1)?.id;
390
+ if (stageId !== undefined) completedStageReplayKeys.set(stageId, replayKey);
391
+ return stage;
392
+ },
393
+ }),
394
+ task: durableTask,
395
+ chain: createChainPrimitive({ runtime, task: durableTask }),
396
+ parallel: createParallelPrimitive({ runtime, task: durableTask }),
397
+ workflow: durableWorkflow,
398
+ tool,
275
399
  };
276
400
 
277
401
  try {
@@ -298,8 +422,15 @@ export async function run<
298
422
  const result = normalizeWorkflowRunOutput(def.name, rawResult);
299
423
  assertWorkflowRunOutputs(def.name, result, def.outputs);
300
424
  assertWorkflowCreatedStage(runSnapshot);
425
+ await durableBackend.flush?.();
301
426
  const recorded = activeStore.recordRunEnd(runId, "completed", result);
302
427
  appendRunEndWhenRecorded(opts.persistence, recorded, { runId, status: "completed", result, ts: Date.now() });
428
+ durableBackend.setWorkflowStatus(runId, "completed");
429
+ await durableBackend.flush?.();
430
+ if (opts.persistence && durableBackend.persistent) {
431
+ const cacheEntry = durableBackend.toCacheEntry(runId);
432
+ if (cacheEntry) persistDurableCacheEntry(opts.persistence, cacheEntry);
433
+ }
303
434
  return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: "completed", result }, opts.onRunEnd);
304
435
  } catch (err) {
305
436
  const selectedExit = findWorkflowExitSignal(err, exitScope) ?? findWorkflowExitSignal(ownController.signal.reason, exitScope);
@@ -351,6 +482,17 @@ export async function run<
351
482
  });
352
483
  return reconcileTerminalRunResult(runId, runSnapshot, activeStore, { status: "failed", error: metadata.errorMessage }, opts.onRunEnd);
353
484
  } finally {
485
+ // Persist final durable status for cross-session resume discovery.
486
+ // Covers ctx.exit terminal states and failure/kill paths; normal
487
+ // completion is handled in the try block.
488
+ // cross-ref: issue #1498
489
+ await finalizeDurableTerminalStatus({
490
+ runId,
491
+ runSnapshot,
492
+ isRoot: opts.parentRun === undefined,
493
+ durableBackend,
494
+ persistence: opts.persistence,
495
+ });
354
496
  opts.cancellation?.unregister(runId);
355
497
  }
356
498
  }
@@ -148,12 +148,18 @@ export class EngineRuntime {
148
148
  const allow = options?.mcp?.allow ?? null;
149
149
  const deny = options?.mcp?.deny ?? null;
150
150
  const hasScope = allow !== null || deny !== null;
151
+ let depth = 0;
151
152
  return {
152
153
  apply: () => {
153
- if (this.childRunOptions.mcp && hasScope) this.childRunOptions.mcp.setScope(stageId, allow, deny);
154
+ if (!this.childRunOptions.mcp || !hasScope) return;
155
+ if (depth === 0) this.childRunOptions.mcp.setScope(stageId, allow, deny);
156
+ depth += 1;
154
157
  },
155
158
  clear: () => {
156
- if (this.childRunOptions.mcp && hasScope) this.childRunOptions.mcp.clearScope(stageId);
159
+ if (!this.childRunOptions.mcp || !hasScope) return;
160
+ if (depth === 0) return;
161
+ depth -= 1;
162
+ if (depth === 0) this.childRunOptions.mcp.clearScope(stageId);
157
163
  },
158
164
  };
159
165
  }
@@ -1,5 +1,4 @@
1
- import { killRun } from "../runs/background/status.js";
2
- import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
1
+ import { quitRun } from "../runs/background/quit.js";
3
2
  import { store } from "../shared/store.js";
4
3
  import { subscribeIntercomControl } from "../intercom/result-intercom.js";
5
4
  import { buildIntercomCallbacks } from "../intercom/intercom-routing.js";
@@ -7,9 +6,8 @@ import { installStoreWidget, installToolExecutionHooks } from "../tui/store-widg
7
6
  import { buildGraphOverlayAdapter } from "../tui/overlay-adapter.js";
8
7
  import type { GraphOverlayPort } from "../tui/overlay-adapter.js";
9
8
  import { registerInlineFormRenderer } from "../tui/inline-form-overlay.js";
10
- import { registerChatSurfaceRenderer, emitChatSurface } from "../tui/chat-surface-message.js";
9
+ import { registerChatSurfaceRenderer } from "../tui/chat-surface-message.js";
11
10
  import { deriveGraphTheme } from "../tui/graph-theme.js";
12
- import type { WorkflowPersistencePort } from "../shared/types.js";
13
11
  import { renderRunBanner, renderRunSummary, type RunEndPayload, type RunStartPayload } from "./renderers.js";
14
12
  import { buildRuntimeAdapters } from "./wiring.js";
15
13
  import type { ExtensionAPI, PiCommandContext } from "./public-types.js";
@@ -36,15 +34,11 @@ function registerWorkflowMessageRenderers(pi: ExtensionAPI): void {
36
34
 
37
35
  function buildWorkflowOverlay(
38
36
  pi: ExtensionAPI,
39
- getPersistence: () => WorkflowPersistencePort | undefined,
40
37
  ): GraphOverlayPort {
41
38
  return buildGraphOverlayAdapter(pi, store, {
42
- onKillRun: (runId) => {
43
- const run = store.runs().find((r) => r.id === runId);
44
- const result = killRun(runId, { cancellation: cancellationRegistry, persistence: getPersistence() });
45
- if (run && result.ok) {
46
- emitChatSurface(pi, { kind: "killed", run, previousStatus: result.previousStatus });
47
- }
39
+ onQuitRun: (runId) => {
40
+ quitRun(runId, { store });
41
+ pi.ui?.notify?.(`Workflow quit; resume with /workflow resume.`, "info");
48
42
  },
49
43
  });
50
44
  }
@@ -83,7 +77,7 @@ function registerIntercomControl(
83
77
  function factory(pi: ExtensionAPI): void {
84
78
  const adapters = buildRuntimeAdapters(pi);
85
79
  const runtimeState = createWorkflowExtensionRuntimeState(pi, adapters);
86
- const overlay = buildWorkflowOverlay(pi, () => runtimeState.persistenceRef.current);
80
+ const overlay = buildWorkflowOverlay(pi);
87
81
  const workflowCommands = new Map<string, WorkflowCommandHandler>();
88
82
  const storeWidgetRef: { current: (() => void) | null } = { current: null };
89
83
  const intercomControlRef: { current: (() => void) | null } = { current: null };
@@ -1,4 +1,5 @@
1
1
  import { killAllRuns } from "../runs/background/status.js";
2
+ import { quitAllRuns } from "../runs/background/quit.js";
2
3
  import { cancellationRegistry } from "../runs/background/cancellation-registry.js";
3
4
  import { stageControlRegistry } from "../runs/foreground/stage-control-registry.js";
4
5
  import { store } from "../shared/store.js";
@@ -99,7 +100,10 @@ export function registerWorkflowLifecycleHandlers(
99
100
  deps.intercomControlRef.current?.();
100
101
  deps.intercomControlRef.current = null;
101
102
  if (reason === "quit") {
102
- killAllRuns({ store, cancellation: cancellationRegistry, persistence: runtimeState.persistenceRef.current });
103
+ // CLI/orchestrator quit is a resumable process boundary, not explicit
104
+ // `/workflow kill`. Durable-progress workflows stay available through
105
+ // `/workflow resume`; stage handles are disposed after being paused.
106
+ quitAllRuns({ store, stageControlRegistry });
103
107
  stageControlRegistry.clear();
104
108
  }
105
109
  deps.storeWidgetRef.current?.();