@bastani/atomic 0.9.11-alpha.9 → 0.9.11

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 (285) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +1 -1
  3. package/dist/builtin/intercom/CHANGELOG.md +6 -0
  4. package/dist/builtin/intercom/package.json +1 -1
  5. package/dist/builtin/intercom/subagent-relay.ts +8 -5
  6. package/dist/builtin/mcp/CHANGELOG.md +10 -0
  7. package/dist/builtin/mcp/package.json +1 -1
  8. package/dist/builtin/subagents/CHANGELOG.md +20 -0
  9. package/dist/builtin/subagents/agents/code-simplifier.md +2 -2
  10. package/dist/builtin/subagents/agents/codebase-analyzer.md +2 -2
  11. package/dist/builtin/subagents/agents/codebase-locator.md +2 -2
  12. package/dist/builtin/subagents/agents/codebase-online-researcher.md +2 -2
  13. package/dist/builtin/subagents/agents/codebase-pattern-finder.md +2 -2
  14. package/dist/builtin/subagents/agents/codebase-research-analyzer.md +2 -2
  15. package/dist/builtin/subagents/agents/codebase-research-locator.md +2 -2
  16. package/dist/builtin/subagents/agents/worker.md +2 -2
  17. package/dist/builtin/subagents/package.json +1 -1
  18. package/dist/builtin/subagents/src/extension/notification-content.ts +14 -9
  19. package/dist/builtin/subagents/src/runs/background/notify.ts +20 -4
  20. package/dist/builtin/web-access/CHANGELOG.md +20 -0
  21. package/dist/builtin/web-access/content-tools.ts +1 -0
  22. package/dist/builtin/web-access/flat-string.ts +37 -0
  23. package/dist/builtin/web-access/github-api.ts +2 -1
  24. package/dist/builtin/web-access/github-extract.ts +3 -2
  25. package/dist/builtin/web-access/package.json +2 -2
  26. package/dist/builtin/workflows/CHANGELOG.md +57 -0
  27. package/dist/builtin/workflows/builtin/goal-ledger.ts +6 -4
  28. package/dist/builtin/workflows/builtin/goal-runner.ts +2 -1
  29. package/dist/builtin/workflows/builtin/goal.ts +1 -1
  30. package/dist/builtin/workflows/builtin/ralph-core.ts +8 -9
  31. package/dist/builtin/workflows/builtin/ralph-forked-prompts.ts +2 -4
  32. package/dist/builtin/workflows/builtin/ralph-models.ts +4 -4
  33. package/dist/builtin/workflows/builtin/ralph-runner.ts +5 -8
  34. package/dist/builtin/workflows/builtin/ralph.ts +2 -1
  35. package/dist/builtin/workflows/package.json +1 -1
  36. package/dist/builtin/workflows/src/durable/completed-catalog.ts +14 -8
  37. package/dist/builtin/workflows/src/durable/completed-inspection.ts +10 -11
  38. package/dist/builtin/workflows/src/durable/resume-catalog.ts +1 -1
  39. package/dist/builtin/workflows/src/durable/resume-eligibility.ts +42 -0
  40. package/dist/builtin/workflows/src/durable/resume-runtime.ts +36 -37
  41. package/dist/builtin/workflows/src/durable/tool-primitive.ts +10 -2
  42. package/dist/builtin/workflows/src/engine/run.ts +1 -0
  43. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +12 -8
  44. package/dist/builtin/workflows/src/extension/hil-answer-notifications.ts +5 -2
  45. package/dist/builtin/workflows/src/extension/index.bundle.mjs +1966 -880
  46. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +6 -3
  47. package/dist/builtin/workflows/src/extension/postmortem-deps.ts +3 -2
  48. package/dist/builtin/workflows/src/extension/render-result.ts +4 -11
  49. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +17 -20
  50. package/dist/builtin/workflows/src/extension/runtime.ts +3 -3
  51. package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +1 -1
  52. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +3 -7
  53. package/dist/builtin/workflows/src/extension/workflow-durable-resume-command.ts +21 -24
  54. package/dist/builtin/workflows/src/extension/workflow-resume-picker-rows.ts +38 -26
  55. package/dist/builtin/workflows/src/extension/workflow-resume-shadow.ts +2 -1
  56. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +56 -64
  57. package/dist/builtin/workflows/src/extension/workflow-schema.ts +2 -2
  58. package/dist/builtin/workflows/src/extension/workflow-status-summary.ts +0 -5
  59. package/dist/builtin/workflows/src/extension/workflow-targets.ts +17 -20
  60. package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -1
  61. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +24 -51
  62. package/dist/builtin/workflows/src/extension/workflow-tool-inspection.ts +7 -29
  63. package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +5 -13
  64. package/dist/builtin/workflows/src/extension/workflow-tool.ts +4 -13
  65. package/dist/builtin/workflows/src/runs/background/quit.ts +4 -3
  66. package/dist/builtin/workflows/src/runs/background/run-inspect.ts +8 -8
  67. package/dist/builtin/workflows/src/runs/background/status.ts +2 -1
  68. package/dist/builtin/workflows/src/runs/background/workflow-lifecycle-aggregate.ts +2 -1
  69. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +5 -5
  70. package/dist/builtin/workflows/src/runs/foreground/executor-lifecycle.ts +4 -0
  71. package/dist/builtin/workflows/src/runs/foreground/executor-scheduler.ts +2 -0
  72. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +23 -8
  73. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +6 -2
  74. package/dist/builtin/workflows/src/runs/foreground/executor-task-prompts.ts +5 -0
  75. package/dist/builtin/workflows/src/runs/foreground/stage-runner-context.ts +56 -10
  76. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +6 -0
  77. package/dist/builtin/workflows/src/runs/foreground/stage-runner-output.ts +137 -6
  78. package/dist/builtin/workflows/src/shared/authoring-contract-ui.d.ts +2 -0
  79. package/dist/builtin/workflows/src/shared/authoring-contract-ui.ts +2 -0
  80. package/dist/builtin/workflows/src/shared/expanded-workflow-graph.ts +159 -10
  81. package/dist/builtin/workflows/src/shared/flat-string.ts +33 -0
  82. package/dist/builtin/workflows/src/shared/graph-store-snapshot.ts +144 -0
  83. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +2 -3
  84. package/dist/builtin/workflows/src/shared/run-id.ts +36 -0
  85. package/dist/builtin/workflows/src/shared/store-internal.ts +32 -3
  86. package/dist/builtin/workflows/src/shared/store-observation.ts +10 -0
  87. package/dist/builtin/workflows/src/shared/store-public-types.ts +8 -0
  88. package/dist/builtin/workflows/src/shared/store-run-methods.ts +14 -0
  89. package/dist/builtin/workflows/src/shared/store-types.ts +4 -0
  90. package/dist/builtin/workflows/src/shared/types.ts +2 -0
  91. package/dist/builtin/workflows/src/shared/workflow-artifacts.ts +261 -0
  92. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +39 -23
  93. package/dist/builtin/workflows/src/tui/graph-canvas.ts +26 -9
  94. package/dist/builtin/workflows/src/tui/graph-view-constants.ts +3 -0
  95. package/dist/builtin/workflows/src/tui/graph-view-graph-render.ts +95 -98
  96. package/dist/builtin/workflows/src/tui/graph-view-render.ts +7 -20
  97. package/dist/builtin/workflows/src/tui/graph-view-state.ts +160 -13
  98. package/dist/builtin/workflows/src/tui/layout.ts +8 -2
  99. package/dist/builtin/workflows/src/tui/node-card.ts +57 -45
  100. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +4 -2
  101. package/dist/builtin/workflows/src/tui/prompt-card-render.ts +291 -21
  102. package/dist/builtin/workflows/src/tui/prompt-card.ts +1 -1
  103. package/dist/builtin/workflows/src/tui/renderers.ts +0 -1
  104. package/dist/builtin/workflows/src/tui/run-detail.ts +52 -35
  105. package/dist/builtin/workflows/src/tui/run-identity-rows.ts +97 -0
  106. package/dist/builtin/workflows/src/tui/session-overlays.ts +24 -3
  107. package/dist/builtin/workflows/src/tui/session-picker.ts +94 -41
  108. package/dist/builtin/workflows/src/tui/stage-chat-view-archive-history.ts +190 -26
  109. package/dist/builtin/workflows/src/tui/stage-chat-view-footer-status.ts +52 -18
  110. package/dist/builtin/workflows/src/tui/stage-chat-view-input.ts +9 -5
  111. package/dist/builtin/workflows/src/tui/stage-chat-view-state.ts +44 -4
  112. package/dist/builtin/workflows/src/tui/stage-chat-view-types.ts +3 -1
  113. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +3 -2
  114. package/dist/builtin/workflows/src/tui/status-list.ts +67 -45
  115. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +7 -6
  116. package/dist/builtin/workflows/src/tui/widget.ts +49 -40
  117. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +15 -10
  118. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +21 -8
  119. package/dist/core/agent-session-accessors.d.ts.map +1 -1
  120. package/dist/core/agent-session-accessors.js +5 -0
  121. package/dist/core/agent-session-accessors.js.map +1 -1
  122. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  123. package/dist/core/agent-session-auto-compaction.js +5 -0
  124. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  125. package/dist/core/agent-session-compaction.d.ts.map +1 -1
  126. package/dist/core/agent-session-compaction.js +3 -0
  127. package/dist/core/agent-session-compaction.js.map +1 -1
  128. package/dist/core/agent-session-events.d.ts.map +1 -1
  129. package/dist/core/agent-session-events.js +2 -1
  130. package/dist/core/agent-session-events.js.map +1 -1
  131. package/dist/core/agent-session-message-queue.d.ts +3 -7
  132. package/dist/core/agent-session-message-queue.d.ts.map +1 -1
  133. package/dist/core/agent-session-message-queue.js +27 -13
  134. package/dist/core/agent-session-message-queue.js.map +1 -1
  135. package/dist/core/agent-session-methods.d.ts +5 -3
  136. package/dist/core/agent-session-methods.d.ts.map +1 -1
  137. package/dist/core/agent-session-methods.js.map +1 -1
  138. package/dist/core/agent-session-persistent-custom-messages.d.ts.map +1 -1
  139. package/dist/core/agent-session-persistent-custom-messages.js +6 -3
  140. package/dist/core/agent-session-persistent-custom-messages.js.map +1 -1
  141. package/dist/core/agent-session-post-tool-compaction.d.ts.map +1 -1
  142. package/dist/core/agent-session-post-tool-compaction.js +11 -2
  143. package/dist/core/agent-session-post-tool-compaction.js.map +1 -1
  144. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  145. package/dist/core/agent-session-runtime.js +5 -3
  146. package/dist/core/agent-session-runtime.js.map +1 -1
  147. package/dist/core/agent-session-tree.d.ts.map +1 -1
  148. package/dist/core/agent-session-tree.js +4 -0
  149. package/dist/core/agent-session-tree.js.map +1 -1
  150. package/dist/core/agent-session-types.d.ts +4 -0
  151. package/dist/core/agent-session-types.d.ts.map +1 -1
  152. package/dist/core/agent-session-types.js.map +1 -1
  153. package/dist/core/agent-session.d.ts +2 -1
  154. package/dist/core/agent-session.d.ts.map +1 -1
  155. package/dist/core/agent-session.js +1 -0
  156. package/dist/core/agent-session.js.map +1 -1
  157. package/dist/core/auth-storage-backends.d.ts.map +1 -1
  158. package/dist/core/auth-storage-backends.js +12 -1
  159. package/dist/core/auth-storage-backends.js.map +1 -1
  160. package/dist/core/bounded-model-refresh.d.ts +14 -0
  161. package/dist/core/bounded-model-refresh.d.ts.map +1 -0
  162. package/dist/core/bounded-model-refresh.js +38 -0
  163. package/dist/core/bounded-model-refresh.js.map +1 -0
  164. package/dist/core/context-tool-pairing.d.ts +4 -4
  165. package/dist/core/context-tool-pairing.js +4 -4
  166. package/dist/core/context-tool-pairing.js.map +1 -1
  167. package/dist/core/http-dispatcher.d.ts.map +1 -1
  168. package/dist/core/http-dispatcher.js +26 -1
  169. package/dist/core/http-dispatcher.js.map +1 -1
  170. package/dist/core/messages.d.ts +18 -6
  171. package/dist/core/messages.d.ts.map +1 -1
  172. package/dist/core/messages.js +46 -18
  173. package/dist/core/messages.js.map +1 -1
  174. package/dist/core/model-refresh-timeout.d.ts +9 -0
  175. package/dist/core/model-refresh-timeout.d.ts.map +1 -0
  176. package/dist/core/model-refresh-timeout.js +9 -0
  177. package/dist/core/model-refresh-timeout.js.map +1 -0
  178. package/dist/core/model-runtime-snapshot.d.ts +3 -0
  179. package/dist/core/model-runtime-snapshot.d.ts.map +1 -1
  180. package/dist/core/model-runtime-snapshot.js +59 -0
  181. package/dist/core/model-runtime-snapshot.js.map +1 -1
  182. package/dist/core/model-runtime.d.ts +7 -1
  183. package/dist/core/model-runtime.d.ts.map +1 -1
  184. package/dist/core/model-runtime.js +72 -17
  185. package/dist/core/model-runtime.js.map +1 -1
  186. package/dist/core/sdk.d.ts.map +1 -1
  187. package/dist/core/sdk.js +4 -3
  188. package/dist/core/sdk.js.map +1 -1
  189. package/dist/core/session-manager-core.d.ts +1 -1
  190. package/dist/core/session-manager-core.d.ts.map +1 -1
  191. package/dist/core/session-manager-core.js +2 -2
  192. package/dist/core/session-manager-core.js.map +1 -1
  193. package/dist/core/session-manager-entries.d.ts +1 -1
  194. package/dist/core/session-manager-entries.d.ts.map +1 -1
  195. package/dist/core/session-manager-entries.js +2 -1
  196. package/dist/core/session-manager-entries.js.map +1 -1
  197. package/dist/core/session-manager-history.d.ts.map +1 -1
  198. package/dist/core/session-manager-history.js +2 -2
  199. package/dist/core/session-manager-history.js.map +1 -1
  200. package/dist/core/session-manager-types.d.ts +3 -0
  201. package/dist/core/session-manager-types.d.ts.map +1 -1
  202. package/dist/core/session-manager-types.js.map +1 -1
  203. package/dist/index.d.ts +1 -1
  204. package/dist/index.d.ts.map +1 -1
  205. package/dist/index.js.map +1 -1
  206. package/dist/main-app-mode.d.ts +1 -0
  207. package/dist/main-app-mode.d.ts.map +1 -1
  208. package/dist/main-app-mode.js +3 -0
  209. package/dist/main-app-mode.js.map +1 -1
  210. package/dist/main.d.ts.map +1 -1
  211. package/dist/main.js +8 -3
  212. package/dist/main.js.map +1 -1
  213. package/dist/modes/interactive/components/chat-session-host-actions.d.ts +7 -2
  214. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  215. package/dist/modes/interactive/components/chat-session-host-actions.js +13 -4
  216. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  217. package/dist/modes/interactive/components/chat-session-host-editor.d.ts +3 -1
  218. package/dist/modes/interactive/components/chat-session-host-editor.d.ts.map +1 -1
  219. package/dist/modes/interactive/components/chat-session-host-editor.js +2 -2
  220. package/dist/modes/interactive/components/chat-session-host-editor.js.map +1 -1
  221. package/dist/modes/interactive/components/chat-session-host-events.d.ts +3 -0
  222. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  223. package/dist/modes/interactive/components/chat-session-host-events.js +5 -4
  224. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  225. package/dist/modes/interactive/components/chat-session-host.d.ts +16 -2
  226. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -1
  227. package/dist/modes/interactive/components/chat-session-host.js +35 -5
  228. package/dist/modes/interactive/components/chat-session-host.js.map +1 -1
  229. package/dist/modes/interactive/components/index.d.ts +1 -0
  230. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/index.js.map +1 -1
  232. package/dist/modes/interactive/components/model-selector.d.ts +0 -1
  233. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/model-selector.js +25 -35
  235. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  236. package/dist/modes/interactive/interactive-editor-actions.js +2 -1
  237. package/dist/modes/interactive/interactive-editor-actions.js.map +1 -1
  238. package/dist/modes/interactive/interactive-input-handling.js +1 -1
  239. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  240. package/dist/modes/interactive/interactive-mode-surface.d.ts +4 -1
  241. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  242. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  243. package/dist/modes/interactive/interactive-model-routing.js +4 -1
  244. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  245. package/dist/modes/interactive/interactive-pause.d.ts +3 -1
  246. package/dist/modes/interactive/interactive-pause.d.ts.map +1 -1
  247. package/dist/modes/interactive/interactive-pause.js +5 -1
  248. package/dist/modes/interactive/interactive-pause.js.map +1 -1
  249. package/dist/modes/interactive/interactive-queueing.js +3 -3
  250. package/dist/modes/interactive/interactive-queueing.js.map +1 -1
  251. package/dist/modes/interactive-engine/engine-health.d.ts +6 -0
  252. package/dist/modes/interactive-engine/engine-health.d.ts.map +1 -1
  253. package/dist/modes/interactive-engine/engine-health.js +15 -1
  254. package/dist/modes/interactive-engine/engine-health.js.map +1 -1
  255. package/dist/modes/interactive-engine/isolated-auth.js +1 -1
  256. package/dist/modes/interactive-engine/isolated-auth.js.map +1 -1
  257. package/dist/modes/interactive-engine/isolated-runtime.d.ts +1 -0
  258. package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
  259. package/dist/modes/interactive-engine/isolated-runtime.js +7 -1
  260. package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
  261. package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -1
  262. package/dist/modes/interactive-engine/remote-model-catalog.js +5 -1
  263. package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -1
  264. package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
  265. package/dist/modes/rpc/rpc-command-handler.js +23 -7
  266. package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
  267. package/dist/modes/rpc/rpc-input-scheduler.js +1 -1
  268. package/dist/modes/rpc/rpc-input-scheduler.js.map +1 -1
  269. package/dist/modes/rpc/rpc-types.d.ts +2 -1
  270. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  271. package/dist/modes/rpc/rpc-types.js.map +1 -1
  272. package/docs/custom-provider.md +3 -3
  273. package/docs/extensions.md +3 -1
  274. package/docs/index.md +10 -0
  275. package/docs/keybindings.md +1 -1
  276. package/docs/models.md +2 -0
  277. package/docs/providers.md +7 -3
  278. package/docs/quickstart.md +7 -1
  279. package/docs/session-format.md +2 -0
  280. package/docs/termux.md +3 -1
  281. package/docs/usage.md +3 -2
  282. package/docs/workflows.md +82 -46
  283. package/npm-shrinkwrap.json +59 -27
  284. package/package.json +3 -3
  285. package/dist/builtin/workflows/src/tui/toast.ts +0 -107
@@ -1,4 +1,5 @@
1
1
  import { getDurableBackend } from "../durable/factory.js";
2
+ import { isWorkflowRunResumable } from "../durable/resume-eligibility.js";
2
3
  import type { ResumableWorkflowEntry } from "../durable/types.js";
3
4
  import { quitAllRuns, quitRun } from "../runs/background/quit.js";
4
5
  import { abortToolNode } from "../runs/background/quit-tool-node.js";
@@ -7,6 +8,7 @@ import { workflowHasPausedStages, workflowHasPausedState } from "../runs/backgro
7
8
  import { store } from "../shared/store.js";
8
9
  import type { RunSnapshot } from "../shared/store-types.js";
9
10
  import type { WorkflowExecutionPolicy, WorkflowToolNodeIdentity } from "../shared/types.js";
11
+ import { workflowRunResumeCandidate } from "../shared/workflow-artifacts.js";
10
12
  import type { WorkflowToolArgs } from "./public-types.js";
11
13
  import type { WorkflowToolResult } from "./render-result.js";
12
14
  import type { ExtensionRuntime } from "./runtime.js";
@@ -16,7 +18,6 @@ import { normalizeWorkflowReloadReport, type WorkflowReloadReport } from "./work
16
18
  import { classifyDurableResumeShadow } from "./workflow-resume-shadow.js";
17
19
  import {
18
20
  allStageConflictMessage,
19
- ambiguousRunMessage,
20
21
  reloadFailureMessage,
21
22
  resolveControlNodeTarget,
22
23
  resolveToolRunTarget,
@@ -81,14 +82,8 @@ export async function workflowPauseAction(args: WorkflowToolArgs): Promise<Workf
81
82
  return controlFailure(action, "--all", error);
82
83
  }
83
84
  }
84
- if (target.kind === "ambiguous")
85
- return {
86
- action,
87
- runId: target.target,
88
- status: "noop",
89
- message: ambiguousRunMessage(target.target, target.matches),
90
- };
91
- if (target.kind === "not_found") return { action, runId: target.target, status: "noop", message: target.message };
85
+ if (target.kind === "malformed" || target.kind === "not_found")
86
+ return { action, runId: target.target, status: "noop", message: target.message };
92
87
  const controlNode = resolveControlNodeTarget(target.runId, args.stageId);
93
88
  // Tool nodes have no turn boundary to stop at, so pause rejects them loudly
94
89
  // instead of silently resolving to nothing.
@@ -111,7 +106,7 @@ export async function workflowPauseAction(args: WorkflowToolArgs): Promise<Workf
111
106
  action,
112
107
  runId: result.runId,
113
108
  status: "paused",
114
- message: `Paused ${result.paused.length} stage(s) on run ${result.runId.slice(0, 8)}.`,
109
+ message: `Paused ${result.paused.length} stage(s) on run ${result.runId}.`,
115
110
  }
116
111
  : {
117
112
  action,
@@ -214,7 +209,7 @@ async function quitToolNodeAction(
214
209
  action,
215
210
  runId,
216
211
  status: "noop",
217
- message: `Tool node ${nodeId} is not running on run ${runId.slice(0, 8)}.`,
212
+ message: `Tool node ${nodeId} is not running on run ${runId}.`,
218
213
  };
219
214
  }
220
215
  // Let the rejected tool promise reach workflow code before the run status is
@@ -232,7 +227,7 @@ async function quitToolNodeAction(
232
227
  workflowStatus,
233
228
  abandoned: aborted.abandoned,
234
229
  message:
235
- `Cancelled ctx.tool ${aborted.name} (${nodeId}) on run ${runId.slice(0, 8)}.${abandonedNote}` +
230
+ `Cancelled ctx.tool ${aborted.name} (${nodeId}) on run ${runId}.${abandonedNote}` +
236
231
  ` Sibling work was not aborted. Current workflow status: ${workflowStatus}.` +
237
232
  " If workflow code awaited this call without catching cancellation, the workflow may fail." +
238
233
  " A later resume re-runs this call.",
@@ -262,15 +257,9 @@ export async function workflowQuitAction(args: WorkflowToolArgs): Promise<Workfl
262
257
  : "No in-flight runs to quit.",
263
258
  };
264
259
  }
265
- if (target.kind === "ambiguous") {
266
- return {
267
- action,
268
- runId: target.target,
269
- status: "noop",
270
- message: ambiguousRunMessage(target.target, target.matches),
271
- };
260
+ if (target.kind === "malformed" || target.kind === "not_found") {
261
+ return { action, runId: target.target, status: "noop", message: target.message };
272
262
  }
273
- if (target.kind === "not_found") return { action, runId: target.target, status: "noop", message: target.message };
274
263
  const controlNode = resolveControlNodeTarget(target.runId, args.stageId);
275
264
  if (!controlNode.ok) return { action, runId: target.runId, status: "noop", message: controlNode.message };
276
265
  if (controlNode.kind === "tool") return quitToolNodeAction(controlNode.runId, controlNode.nodeId, action);
@@ -290,9 +279,9 @@ export async function workflowQuitAction(args: WorkflowToolArgs): Promise<Workfl
290
279
  status: "noop",
291
280
  message:
292
281
  result.reason === "already_ended"
293
- ? `Run ${target.runId.slice(0, 8)} already ended.`
282
+ ? `Run ${target.runId} already ended.`
294
283
  : result.reason === "no_active_stages"
295
- ? `No controllable stages on run ${target.runId.slice(0, 8)}; the run remains active.`
284
+ ? `No controllable stages on run ${target.runId}; the run remains active.`
296
285
  : `Run not found: ${target.runId}`,
297
286
  };
298
287
  } catch (error) {
@@ -320,14 +309,8 @@ export async function workflowInterruptAction(args: WorkflowToolArgs): Promise<W
320
309
  return controlFailure(action, "--all", error);
321
310
  }
322
311
  }
323
- if (target.kind === "ambiguous")
324
- return {
325
- action,
326
- runId: target.target,
327
- status: "noop",
328
- message: ambiguousRunMessage(target.target, target.matches),
329
- };
330
- if (target.kind === "not_found") return { action, runId: target.target, status: "noop", message: target.message };
312
+ if (target.kind === "malformed" || target.kind === "not_found")
313
+ return { action, runId: target.target, status: "noop", message: target.message };
331
314
  const controlNode = resolveControlNodeTarget(target.runId, args.stageId);
332
315
  if (!controlNode.ok) return { action, runId: target.runId, status: "noop", message: controlNode.message };
333
316
  if (controlNode.kind === "tool") return quitToolNodeAction(controlNode.runId, controlNode.nodeId, action);
@@ -412,14 +395,8 @@ async function resolveExplicitDurableTarget(
412
395
  return controlFailure("resume", target, error);
413
396
  }
414
397
  const resolved = resolveWorkflowResumeTarget(target, liveRuns, durable, []);
415
- if (resolved.kind === "ambiguous") {
416
- const matches = resolved.matches.map((match) => match.workflowId);
417
- return {
418
- action: "resume",
419
- runId: target,
420
- status: "noop",
421
- message: `Ambiguous run prefix "${target}" matches: ${matches.join(", ")}`,
422
- };
398
+ if (resolved.kind === "malformed") {
399
+ return { action: "resume", runId: target, status: "noop", message: resolved.message };
423
400
  }
424
401
  if (resolved.kind === "durable") return resumePreparedDurableTarget(resolved.workflowId, deps);
425
402
  if (resolved.kind === "live") {
@@ -458,9 +435,8 @@ export async function workflowResumeAction(
458
435
  const target = resolveToolRunTarget(args, "No active run to resume.");
459
436
  if (target.kind === "all")
460
437
  return { action: "resume", runId: "--all", status: "noop", message: "Resume does not support --all." };
461
- if (target.kind === "ambiguous") {
462
- const liveMatches = store.runs().filter((run) => target.matches.includes(run.id));
463
- return resolveExplicitDurableTarget(target.target, args, deps, liveMatches);
438
+ if (target.kind === "malformed") {
439
+ return { action: "resume", runId: target.target, status: "noop", message: target.message };
464
440
  }
465
441
  if (target.kind === "not_found") {
466
442
  const explicitTarget = args.runId?.trim();
@@ -469,11 +445,8 @@ export async function workflowResumeAction(
469
445
  }
470
446
  return { action: "resume", runId: target.target, status: "noop", message: target.message };
471
447
  }
472
- const explicitTarget = args.runId?.trim();
473
- if (explicitTarget !== undefined && explicitTarget.length > 0 && explicitTarget !== target.runId) {
474
- const liveMatches = store.runs().filter((run) => run.id.startsWith(explicitTarget));
475
- return resolveExplicitDurableTarget(explicitTarget, args, deps, liveMatches);
476
- }
448
+ // An explicit target now resolves only by exact id, so it can never disagree
449
+ // with the resolved run; the old re-resolution branch here is unreachable.
477
450
  const backend = getDurableBackend();
478
451
  const exact = store.runs().find((run) => run.id === target.runId);
479
452
  const shadow = exact === undefined ? "not_shadow" : classifyDurableResumeShadow(exact, store, { backend });
@@ -512,8 +485,8 @@ export async function workflowResumeAction(
512
485
  const isResumableContinuation =
513
486
  run !== undefined &&
514
487
  !isPaused &&
515
- ((run.status === "failed" && run.endedAt !== undefined && run.resumable !== false) ||
516
- (run.endedAt === undefined && run.resumable === true && run.failureRecoverability === "recoverable"));
488
+ run.exitReason !== "quit" &&
489
+ isWorkflowRunResumable(workflowRunResumeCandidate(run));
517
490
  if (isResumableContinuation) {
518
491
  try {
519
492
  await deps.ensureWorkflowResourcesLoaded();
@@ -544,9 +517,9 @@ export async function workflowResumeAction(
544
517
  (isPaused
545
518
  ? result.resumed.length === 0
546
519
  ? runLevelResumed
547
- ? `Resumed run ${result.runId.slice(0, 8)}.`
548
- : `No paused stages on run ${result.runId.slice(0, 8)}.`
549
- : `Resumed ${result.resumed.length} stage(s) on run ${result.runId.slice(0, 8)}${args.message ? ` with message: "${args.message}"` : ""}.`
520
+ ? `Resumed run ${result.runId}.`
521
+ : `No paused stages on run ${result.runId}.`
522
+ : `Resumed ${result.resumed.length} stage(s) on run ${result.runId}${args.message ? ` with message: "${args.message}"` : ""}.`
550
523
  : `Snapshot available: run ${result.runId} (${result.snapshot.name}) — status: ${result.snapshot.status}, stages: ${result.snapshot.stages.length}`);
551
524
  const status = result.mode === "partial" ? "partial" : noPausedProgress ? "noop" : "ok";
552
525
  return { action: "resume", runId: result.runId, status, message };
@@ -12,7 +12,7 @@ import {
12
12
  summarizeStage,
13
13
  transcriptEntryFromMessage,
14
14
  } from "./workflow-stage-results.js";
15
- import { ambiguousRunMessage, resolveToolRunTarget, resolveToolStageTarget } from "./workflow-targets.js";
15
+ import { resolveToolRunTarget, resolveToolStageTarget } from "./workflow-targets.js";
16
16
 
17
17
  export function workflowStagesResult(args: WorkflowToolArgs): WorkflowToolResult {
18
18
  const target = resolveToolRunTarget(args, "No active run to inspect.");
@@ -26,16 +26,7 @@ export function workflowStagesResult(args: WorkflowToolArgs): WorkflowToolResult
26
26
  error: "Stage listing requires a single run.",
27
27
  };
28
28
  }
29
- if (target.kind === "ambiguous") {
30
- return {
31
- action: "stages",
32
- runId: target.target,
33
- filter,
34
- stages: [],
35
- error: ambiguousRunMessage(target.target, target.matches),
36
- };
37
- }
38
- if (target.kind === "not_found") {
29
+ if (target.kind === "malformed" || target.kind === "not_found") {
39
30
  return {
40
31
  action: "stages",
41
32
  runId: target.target,
@@ -56,10 +47,7 @@ export function workflowStageResult(args: WorkflowToolArgs): WorkflowToolResult
56
47
  if (target.kind === "all") {
57
48
  return { action: "stage", runId: "--all", error: "Stage inspection requires a single run." };
58
49
  }
59
- if (target.kind === "ambiguous") {
60
- return { action: "stage", runId: target.target, error: ambiguousRunMessage(target.target, target.matches) };
61
- }
62
- if (target.kind === "not_found") {
50
+ if (target.kind === "malformed" || target.kind === "not_found") {
63
51
  return { action: "stage", runId: target.target, error: target.message };
64
52
  }
65
53
  const stage = resolveToolStageTarget(target.runId, args.stageId);
@@ -67,7 +55,7 @@ export function workflowStageResult(args: WorkflowToolArgs): WorkflowToolResult
67
55
  return {
68
56
  action: "stage",
69
57
  runId: target.runId,
70
- error: stage.ok ? "Stage id, prefix, or name is required." : stage.message,
58
+ error: stage.ok ? "Stage id or name is required." : stage.message,
71
59
  };
72
60
  }
73
61
  const stageRunId = stage.runId ?? target.runId;
@@ -78,7 +66,7 @@ export function workflowStageResult(args: WorkflowToolArgs): WorkflowToolResult
78
66
  : {
79
67
  action: "stage",
80
68
  runId: stageRunId,
81
- error: `Stage not found in run ${stageRunId.slice(0, 8)}: ${stage.stageId}`,
69
+ error: `Stage not found in run ${stageRunId}: ${stage.stageId}`,
82
70
  };
83
71
  }
84
72
 
@@ -94,17 +82,7 @@ export function workflowTranscriptResult(args: WorkflowToolArgs): WorkflowToolRe
94
82
  truncated: false,
95
83
  };
96
84
  }
97
- if (target.kind === "ambiguous") {
98
- return {
99
- action: "transcript",
100
- runId: target.target,
101
- stageId: "",
102
- source: "error",
103
- entries: [{ role: "notice", text: ambiguousRunMessage(target.target, target.matches) }],
104
- truncated: false,
105
- };
106
- }
107
- if (target.kind === "not_found") {
85
+ if (target.kind === "malformed" || target.kind === "not_found") {
108
86
  return {
109
87
  action: "transcript",
110
88
  runId: target.target,
@@ -121,7 +99,7 @@ export function workflowTranscriptResult(args: WorkflowToolArgs): WorkflowToolRe
121
99
  runId: target.runId,
122
100
  stageId: "",
123
101
  source: "error",
124
- entries: [{ role: "notice", text: stage.ok ? "Stage id, prefix, or name is required." : stage.message }],
102
+ entries: [{ role: "notice", text: stage.ok ? "Stage id or name is required." : stage.message }],
125
103
  truncated: false,
126
104
  };
127
105
  }
@@ -9,10 +9,11 @@ import { coerceStageInputAnswer, hasStageInputAnswerContent, type StageInputAnsw
9
9
  import { stageUiBroker } from "../shared/stage-ui-broker.js";
10
10
  import { store } from "../shared/store.js";
11
11
  import { isTerminalRunStatus } from "../shared/store-internal.js";
12
+ import { subscribeStoreInvalidation } from "../shared/store-observation.js";
12
13
  import { reciprocalWorkflowRootRunId } from "../shared/workflow-run-ownership.js";
13
14
  import type { WorkflowToolArgs } from "./public-types.js";
14
15
  import type { WorkflowToolResult } from "./render-result.js";
15
- import { ambiguousRunMessage, resolveToolRunTarget, resolveToolStageTarget } from "./workflow-targets.js";
16
+ import { resolveToolRunTarget, resolveToolStageTarget } from "./workflow-targets.js";
16
17
 
17
18
  /**
18
19
  * Optional dependencies enabling `workflow send` to revive an eligible
@@ -120,16 +121,7 @@ export async function workflowSendAction(
120
121
  if (target.kind === "all") {
121
122
  return workflowSendResult("--all", "", requestedDelivery, "noop", "Send requires a single run.");
122
123
  }
123
- if (target.kind === "ambiguous") {
124
- return workflowSendResult(
125
- target.target,
126
- "",
127
- requestedDelivery,
128
- "noop",
129
- ambiguousRunMessage(target.target, target.matches),
130
- );
131
- }
132
- if (target.kind === "not_found") {
124
+ if (target.kind === "malformed" || target.kind === "not_found") {
133
125
  return workflowSendResult(target.target, "", requestedDelivery, "noop", target.message);
134
126
  }
135
127
  const runs = store.runs();
@@ -146,7 +138,7 @@ export async function workflowSendAction(
146
138
  "",
147
139
  requestedDelivery,
148
140
  "noop",
149
- stage.ok ? "Stage id, prefix, or name is required." : stage.message,
141
+ stage.ok ? "Stage id or name is required." : stage.message,
150
142
  );
151
143
  }
152
144
  const resolvedStageId = stage.stageId;
@@ -280,7 +272,7 @@ export async function workflowSendAction(
280
272
  admitted = true;
281
273
  };
282
274
  const terminalPending = Promise.withResolvers<never>();
283
- const unsubscribeTerminal = store.subscribe(() => {
275
+ const unsubscribeTerminal = subscribeStoreInvalidation(store, () => {
284
276
  if (admitted) return;
285
277
  const terminal = terminalWorkflowSendResultForRoot(rootRunId, args.stageId?.trim() ?? resolvedStageId);
286
278
  if (terminal !== undefined) terminalPending.reject(new WorkflowSendAdmissionError(terminal));
@@ -8,12 +8,7 @@ import { formatWorkflowResourceLoadWarning } from "./workflow-command-surfaces.j
8
8
  import { workflowPolicyFromContext } from "./workflow-policy.js";
9
9
  import type { WorkflowReloadReport } from "./workflow-reload-report.js";
10
10
  import { buildWorkflowStatusListing } from "./workflow-status-summary.js";
11
- import {
12
- ambiguousRunMessage,
13
- isWorkflowStageToolContext,
14
- resolveRunIdPrefix,
15
- topLevelExpandedSnapshots,
16
- } from "./workflow-targets.js";
11
+ import { isWorkflowStageToolContext, resolveRunId, topLevelExpandedSnapshots } from "./workflow-targets.js";
17
12
  import { workflowGetResult } from "./workflow-tool-content.js";
18
13
  import {
19
14
  workflowInterruptAction,
@@ -84,13 +79,9 @@ export function makeExecuteWorkflowTool(
84
79
  case "status": {
85
80
  const target = args.runId;
86
81
  if (target !== undefined) {
87
- const resolved = resolveRunIdPrefix(target);
88
- if (resolved.kind === "ambiguous") {
89
- return {
90
- action: "statusDetail",
91
- runId: target,
92
- error: ambiguousRunMessage(target, resolved.matches),
93
- };
82
+ const resolved = resolveRunId(target);
83
+ if (resolved.kind === "malformed") {
84
+ return { action: "statusDetail", runId: target, error: resolved.message };
94
85
  }
95
86
  if (resolved.kind === "not_found") {
96
87
  return { action: "statusDetail", runId: target, error: `run not found: ${target}` };
@@ -18,6 +18,7 @@ import { WorkflowGracefulQuitError } from "../../engine/workflow-tool-abort.js";
18
18
  import { expandWorkflowGraph } from "../../shared/expanded-workflow-graph.js";
19
19
  import { topLevelWorkflowRuns } from "../../shared/run-visibility.js";
20
20
  import { store as defaultStore } from "../../shared/store.js";
21
+ import { readGraphStoreSnapshot } from "../../shared/store-observation.js";
21
22
  import type { Store } from "../../shared/store-public-types.js";
22
23
  import type { RunSnapshot, StageSnapshot } from "../../shared/store-types.js";
23
24
  import type { WorkflowCancelledToolNode, WorkflowToolNodeIdentity } from "../../shared/types.js";
@@ -100,7 +101,7 @@ export async function quitRun(
100
101
  if (!run) return { ok: false, runId, reason: "not_found" };
101
102
  if (run.endedAt !== undefined) return { ok: false, runId, reason: "already_ended" };
102
103
 
103
- const graph = expandWorkflowGraph(activeStore.snapshot(), runId);
104
+ const graph = expandWorkflowGraph(readGraphStoreSnapshot(activeStore), runId);
104
105
  const handles = controllableHandles(activeStore, registry, runId);
105
106
  const admissionBoundaries = controllableAdmissionBoundaries(activeStore, toolControls, runId);
106
107
  const promptStages = graph.stages.filter(
@@ -247,7 +248,7 @@ function controllableAdmissionBoundaries(
247
248
  */
248
249
  async function closeToolAdmission(boundaries: readonly ToolAdmissionBoundary[], runId: string): Promise<void> {
249
250
  if (boundaries.length === 0) return;
250
- const reason = new WorkflowGracefulQuitError(runId, `run ${runId.slice(0, 8)}`);
251
+ const reason = new WorkflowGracefulQuitError(runId, `run ${runId}`);
251
252
  await Promise.all(boundaries.map((boundary) => boundary.closeForQuit(reason)));
252
253
  }
253
254
 
@@ -303,7 +304,7 @@ function controllableHandles(
303
304
  registry: StageControlRegistry,
304
305
  runId: string,
305
306
  ): Array<{ controlRunId: string; handle: StageControlHandle }> {
306
- const graph = expandWorkflowGraph(activeStore.snapshot(), runId);
307
+ const graph = expandWorkflowGraph(readGraphStoreSnapshot(activeStore), runId);
307
308
  const controlRunIds = new Set<string>([runId]);
308
309
  for (const stage of graph.stages) controlRunIds.add(stage.workflowGraphTarget.runId);
309
310
  return [...controlRunIds].flatMap((controlRunId) =>
@@ -62,18 +62,18 @@ export type InspectRunResult =
62
62
  | { ok: false; runId: string; reason: "not_found" };
63
63
 
64
64
  /**
65
- * Look up a single run by id (full UUID or unique prefix) and return a
66
- * normalised {@link RunDetail} for the per-run text/TUI surfaces.
65
+ * Look up a single run by its exact id and return a normalised
66
+ * {@link RunDetail} for the per-run text/TUI surfaces.
67
67
  *
68
- * Returns ok:false "not_found" when no run matches, "ambiguous" when a
69
- * prefix matches multiple. Read-only: does not mutate the store.
68
+ * Exact match only. Callers reach this with an id already resolved at the input
69
+ * boundary, where shape is validated; a second, looser prefix match here would
70
+ * only reintroduce the truncated targeting the resolvers now reject.
71
+ *
72
+ * Read-only: does not mutate the store.
70
73
  */
71
74
  export function inspectRun(runId: string, opts?: { store?: Store }): InspectRunResult {
72
75
  const activeStore = opts?.store ?? defaultStore;
73
- const runs = activeStore.runs();
74
-
75
- const exact = runs.find((r) => r.id === runId);
76
- const candidate = exact ?? (runs.length > 0 ? runs.find((r) => r.id.startsWith(runId)) : undefined);
76
+ const candidate = activeStore.runs().find((r) => r.id === runId);
77
77
 
78
78
  if (!candidate) {
79
79
  return { ok: false, runId, reason: "not_found" };
@@ -15,6 +15,7 @@ import { effectiveRunStatus } from "../../shared/returned-run-status.js";
15
15
  import { topLevelWorkflowRuns } from "../../shared/run-visibility.js";
16
16
  import type { Store } from "../../shared/store.js";
17
17
  import { store as defaultStore } from "../../shared/store.js";
18
+ import { readGraphStoreSnapshot } from "../../shared/store-observation.js";
18
19
  import type { RunSnapshot, RunStatus, StageSnapshot } from "../../shared/store-types.js";
19
20
  import type { WorkflowPersistencePort } from "../../shared/types.js";
20
21
  import type { StageControlRegistry } from "../foreground/stage-control-registry.js";
@@ -89,7 +90,7 @@ export { type InspectRunResult, inspectRun, type RunDetail } from "./run-inspect
89
90
  export function statusRuns(opts?: { all?: boolean; store?: Store }): RunStatusEntry[] {
90
91
  const activeStore = opts?.store ?? defaultStore;
91
92
 
92
- const snapshot = activeStore.snapshot();
93
+ const snapshot = readGraphStoreSnapshot(activeStore);
93
94
  return topLevelWorkflowRuns(snapshot.runs).map((run) => {
94
95
  const graph = expandWorkflowGraph(snapshot, run.id);
95
96
  return {
@@ -1,10 +1,11 @@
1
1
  import { expandWorkflowGraph } from "../../shared/expanded-workflow-graph.js";
2
+ import { readGraphStoreSnapshot } from "../../shared/store-observation.js";
2
3
  import type { Store } from "../../shared/store-public-types.js";
3
4
  import { reciprocalWorkflowRootRunId } from "../../shared/workflow-run-ownership.js";
4
5
 
5
6
  /** Control-run ids visible below one workflow boundary, in graph order. */
6
7
  export function expandedControlRunIds(store: Store, runId: string): string[] {
7
- const graph = expandWorkflowGraph(store.snapshot(), runId);
8
+ const graph = expandWorkflowGraph(readGraphStoreSnapshot(store), runId);
8
9
  const ids = new Set<string>([runId]);
9
10
  for (const stage of graph.stages) ids.add(stage.workflowGraphTarget.runId);
10
11
  // Tool-only nested runs own no stage, yet their in-flight ctx.tool nodes are
@@ -1,7 +1,7 @@
1
- import { tmpdir } from "node:os";
2
1
  import { isAbsolute, join, resolve } from "node:path";
3
- import { APP_NAME, CONFIG_DIR_NAME, isCodexFastModeCandidateModelId } from "@bastani/atomic";
2
+ import { CONFIG_DIR_NAME, isCodexFastModeCandidateModelId } from "@bastani/atomic";
4
3
  import type { StageOptions, WorkflowArtifact, WorkflowTaskOptions, WorkflowTaskStep } from "../../shared/types.js";
4
+ import { workflowArtifactRunPath } from "../../shared/workflow-artifacts.js";
5
5
  import { buildModelCandidatesFromCatalog, workflowModelId } from "../shared/model-fallback.js";
6
6
  import {
7
7
  cleanupWorktrees,
@@ -211,8 +211,8 @@ function resolvedTaskCwd(cwd: string | undefined, workflowInvocationCwd: string)
211
211
  return isAbsolute(cwd) ? cwd : resolve(workflowInvocationCwd, cwd);
212
212
  }
213
213
 
214
- function taskWorktreeOutputsRoot(): string {
215
- return join(tmpdir(), `${APP_NAME}-workflow-outputs`);
214
+ function taskWorktreeOutputsRoot(runId: string): string {
215
+ return join(workflowArtifactRunPath(runId), "task-outputs");
216
216
  }
217
217
 
218
218
  export function prepareTaskWorktrees(
@@ -250,7 +250,7 @@ export function prepareTaskWorktrees(
250
250
  baseBranch: options.baseBranch,
251
251
  symlinkDirectories,
252
252
  });
253
- const trustedRoot = taskWorktreeOutputsRoot();
253
+ const trustedRoot = taskWorktreeOutputsRoot(runId);
254
254
  return {
255
255
  tasks: tasks.map((task, index) => ({ ...task, cwd: setup.worktrees[index]!.agentCwd })),
256
256
  setup,
@@ -103,6 +103,10 @@ export interface RunFailureMetadata {
103
103
  readonly retryAfterMs?: number;
104
104
  }
105
105
 
106
+ /**
107
+ * Apply failure fields to a live stage. Executor callers invoke this next to
108
+ * a version-bumping stage store method, with no intervening asynchronous work.
109
+ */
106
110
  export function applyFailureToStage(stage: StageSnapshot, failure: WorkflowFailure): void {
107
111
  stage.status = "failed";
108
112
  stage.error = failure.userMessage;
@@ -52,6 +52,8 @@ export function createStageScheduler(input: {
52
52
  input.runSnapshot.stages.find((stage) => stage.id === stageId);
53
53
 
54
54
  const setStageParentIds = (stage: StageSnapshot, parentIds: readonly string[]): void => {
55
+ // The tracked-stage caller invokes this next to the version-bumping
56
+ // `recordStageStart` method, with no intervening asynchronous work.
55
57
  stage.parentIds = Object.freeze([...parentIds]);
56
58
  };
57
59
 
@@ -134,6 +134,7 @@ export function createTrackedStageCaller(input: {
134
134
  }
135
135
 
136
136
  const trackStageLifecycle = !runtime.state.stageFinalized;
137
+ let refreshedParentIds: readonly string[] | undefined;
137
138
  if (
138
139
  trackStageLifecycle &&
139
140
  !input.hasContinuation &&
@@ -146,13 +147,11 @@ export function createTrackedStageCaller(input: {
146
147
  actualParentIds.every((value) => runtime.stageSnapshot.parentIds.includes(value));
147
148
  if (!sameParents) {
148
149
  runtime.scheduler.tracker.replaceParents(runtime.stageId, actualParentIds);
149
- runtime.scheduler.setStageParentIds(runtime.stageSnapshot, actualParentIds);
150
+ refreshedParentIds = actualParentIds;
150
151
  }
151
152
  }
152
153
  if (trackStageLifecycle) {
153
154
  const now = Date.now();
154
- runtime.stageSnapshot.status = "running";
155
- runtime.stageSnapshot.startedAt ??= rebasedStageStartedAt(input.options?.durableAccumulatedDurationMs, now);
156
155
  const hasNoExplicitModelConfig =
157
156
  input.options?.model === undefined && input.options?.fallbackModels === undefined;
158
157
  const promptAdapterHandlesInitialPrompt = input.adapters.prompt !== undefined;
@@ -173,7 +172,13 @@ export function createTrackedStageCaller(input: {
173
172
  if (!(err instanceof Error && err.message.includes("prompt adapter not configured"))) throw err;
174
173
  }
175
174
  }
175
+ if (refreshedParentIds !== undefined) {
176
+ runtime.scheduler.setStageParentIds(runtime.stageSnapshot, refreshedParentIds);
177
+ }
178
+ runtime.stageSnapshot.status = "running";
179
+ runtime.stageSnapshot.startedAt ??= rebasedStageStartedAt(input.options?.durableAccumulatedDurationMs, now);
176
180
  runtime.applyModelFallbackMeta(runtime.innerCtx.__modelFallbackMeta());
181
+ // Publish every graph-visible live write before this task can yield.
177
182
  runtime.activeStore.recordStageStart(runtime.runId, runtime.stageSnapshot);
178
183
  runtime.appendStageStartOnce();
179
184
  } else {
@@ -182,6 +187,7 @@ export function createTrackedStageCaller(input: {
182
187
 
183
188
  runtime.mcpScope.apply();
184
189
 
190
+ let applyTerminalStageState: (() => void) | undefined;
185
191
  try {
186
192
  const abortSession = (): void => {
187
193
  void runtime.innerCtx.abort().catch(() => {});
@@ -260,9 +266,11 @@ export function createTrackedStageCaller(input: {
260
266
  }
261
267
  if (trackStageLifecycle && runtime.state.stageFinalized) throw runtime.parallelFailFastError();
262
268
  if (trackStageLifecycle) {
263
- runtime.stageSnapshot.status = "completed";
264
269
  const assistantText = runtime.innerCtx.__getLastAssistantText();
265
- if (assistantText !== undefined) runtime.stageSnapshot.result = assistantText;
270
+ applyTerminalStageState = () => {
271
+ runtime.stageSnapshot.status = "completed";
272
+ if (assistantText !== undefined) runtime.stageSnapshot.result = assistantText;
273
+ };
266
274
  }
267
275
  return result;
268
276
  } catch (err) {
@@ -270,11 +278,15 @@ export function createTrackedStageCaller(input: {
270
278
  if (workflowExitAbort !== undefined && !runtime.state.skippedForParallelFailFast) {
271
279
  runtime.state.stageClosedByWorkflowExit = true;
272
280
  if (trackStageLifecycle && !isTerminalStage(runtime.stageSnapshot)) {
273
- runtime.stageSnapshot.status = "skipped";
274
- runtime.stageSnapshot.skippedReason = runtime.exit.workflowExitSkippedReason(workflowExitAbort.reason);
281
+ const skippedReason = runtime.exit.workflowExitSkippedReason(workflowExitAbort.reason);
282
+ applyTerminalStageState = () => {
283
+ runtime.stageSnapshot.status = "skipped";
284
+ runtime.stageSnapshot.skippedReason = skippedReason;
285
+ };
275
286
  }
276
287
  } else if (trackStageLifecycle && !runtime.signal.aborted && !runtime.state.skippedForParallelFailFast) {
277
- applyFailureToStage(runtime.stageSnapshot, runtime.classifyExecutorFailure(err));
288
+ const failure = runtime.classifyExecutorFailure(err);
289
+ applyTerminalStageState = () => applyFailureToStage(runtime.stageSnapshot, failure);
278
290
  }
279
291
  throw err;
280
292
  } finally {
@@ -291,6 +303,9 @@ export function createTrackedStageCaller(input: {
291
303
  finalizationError = { thrown: true, error: err };
292
304
  }
293
305
  if (trackStageLifecycle) {
306
+ if (!runtime.state.stageFinalized) applyTerminalStageState?.();
307
+ // `finalizeStageSnapshot` calls the version-bumping `recordStageEnd`
308
+ // before its first yield, so the live write cannot outpace the cache.
294
309
  try {
295
310
  await runtime.finalizeStageSnapshot();
296
311
  } catch (err) {
@@ -290,6 +290,10 @@ export function createWorkflowStageFactory(input: {
290
290
  input.tracker.onSettle(stageId);
291
291
  return false;
292
292
  }
293
+ if (state.skippedForParallelFailFast) {
294
+ stageSnapshot.status = "skipped";
295
+ stageSnapshot.skippedReason = "fail-fast";
296
+ }
293
297
  state.stageFinalized = true;
294
298
  runtime.unregisterWorkflowExitCleanup();
295
299
  stageSnapshot.endedAt = Date.now();
@@ -340,9 +344,9 @@ export function createWorkflowStageFactory(input: {
340
344
  };
341
345
 
342
346
  const markSkippedForParallelFailFast = (): void => {
347
+ // Defer graph-visible fields until `finalizeStageSnapshot`, directly
348
+ // before its version-bumping `recordStageEnd` call.
343
349
  state.skippedForParallelFailFast = true;
344
- stageSnapshot.status = "skipped";
345
- stageSnapshot.skippedReason = "fail-fast";
346
350
  };
347
351
  const parallelFailFastError = (): unknown =>
348
352
  stageFailFastScope?.firstFailure ?? new Error("atomic-workflows: skipped after parallel fail-fast");
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { isAbsolute, resolve } from "node:path";
2
3
  import type {
3
4
  StageOptions,
@@ -80,6 +81,10 @@ export function taskReadInstruction(options: WorkflowTaskExecutionOptions): stri
80
81
  if (options.reads === false || options.reads === undefined || options.reads.length === 0) return "";
81
82
  const baseDir = taskBaseDir(options);
82
83
  const files = options.reads.map((file) => resolveWorkflowPath(file, baseDir));
84
+ const missing = files.find((file) => !existsSync(file));
85
+ if (missing !== undefined) {
86
+ throw new Error(`atomic-workflows: referenced artifact does not exist: ${missing}`);
87
+ }
83
88
  return `[Read from: ${files.join(", ")}]\n\n`;
84
89
  }
85
90