@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
@@ -0,0 +1,144 @@
1
+ import { flattenTruncatedString } from "./flat-string.js";
2
+ import type {
3
+ PendingPrompt,
4
+ RunSnapshot,
5
+ StageInputRequest,
6
+ StageSnapshot,
7
+ StoreSnapshot,
8
+ ToolEvent,
9
+ ToolNodeSnapshot,
10
+ WorkflowChildReplaySnapshot,
11
+ WorkflowNotice,
12
+ } from "./store-types.js";
13
+ import type { WorkflowOutputValues, WorkflowSerializableValue } from "./types.js";
14
+
15
+ export const COMPACT_RESULT_FIELD_LIMIT = 1024;
16
+
17
+ function compactResultField(value: WorkflowSerializableValue | undefined): string | undefined {
18
+ if (typeof value !== "string") return undefined;
19
+ if (value.length <= COMPACT_RESULT_FIELD_LIMIT) return value;
20
+ // Flattened, not just sliced: `graphSnapshot()` memoizes this projection, so a
21
+ // SlicedString here would keep the whole run result alive inside the store.
22
+ return flattenTruncatedString(value.slice(0, COMPACT_RESULT_FIELD_LIMIT));
23
+ }
24
+
25
+ function compactRunResult(result: WorkflowOutputValues | undefined): WorkflowOutputValues | undefined {
26
+ if (result === undefined) return undefined;
27
+ const status = compactResultField(result.status);
28
+ const summary = compactResultField(result.summary);
29
+ const remainingWork = compactResultField(result.remaining_work);
30
+ const resultText = compactResultField(result.result);
31
+ const compact: WorkflowOutputValues = {
32
+ ...(status !== undefined ? { status } : {}),
33
+ ...(summary !== undefined ? { summary } : {}),
34
+ ...(remainingWork !== undefined ? { remaining_work: remainingWork } : {}),
35
+ ...(resultText !== undefined ? { result: resultText } : {}),
36
+ };
37
+ return Object.keys(compact).length === 0 ? undefined : compact;
38
+ }
39
+
40
+ // Both cloners are called only from a `!== undefined` guard at their call sites,
41
+ // so they take and return a concrete value; an internal undefined branch here
42
+ // would be unreachable and would only weaken the type for every caller.
43
+ function clonePrompt(prompt: PendingPrompt): PendingPrompt {
44
+ return {
45
+ ...prompt,
46
+ ...(prompt.choices !== undefined ? { choices: [...prompt.choices] } : {}),
47
+ };
48
+ }
49
+
50
+ function cloneInputRequest(request: StageInputRequest): StageInputRequest {
51
+ return {
52
+ ...request,
53
+ questions: request.questions.map((question) => ({
54
+ ...question,
55
+ options: question.options.map((option) => ({ ...option })),
56
+ })),
57
+ };
58
+ }
59
+
60
+ function compactToolEvents(events: readonly ToolEvent[]): ToolEvent[] {
61
+ const event = events.at(-1);
62
+ return event === undefined ? [] : [{ name: event.name }];
63
+ }
64
+
65
+ function compactWorkflowChild(child: WorkflowChildReplaySnapshot): WorkflowChildReplaySnapshot {
66
+ return {
67
+ ...child,
68
+ outputs: {},
69
+ outputCount: child.outputCount ?? Object.keys(child.outputs).length,
70
+ };
71
+ }
72
+
73
+ function compactStage(stage: StageSnapshot): StageSnapshot {
74
+ const {
75
+ toolEvents,
76
+ workflowChild,
77
+ workflowChildRun,
78
+ pendingPrompt,
79
+ promptFootprint,
80
+ inputRequest,
81
+ notices,
82
+ mcpScope: _mcpScope,
83
+ attemptedModels: _attemptedModels,
84
+ modelAttempts: _modelAttempts,
85
+ // Agent-stage results can be unbounded and graph cards do not render
86
+ // them. Durable tool cards use ToolNodeSnapshot.resultSummary instead.
87
+ result: _result,
88
+ ...metadata
89
+ } = stage;
90
+ return {
91
+ ...metadata,
92
+ parentIds: [...stage.parentIds],
93
+ toolEvents: compactToolEvents(toolEvents),
94
+ ...(workflowChild !== undefined ? { workflowChild: compactWorkflowChild(workflowChild) } : {}),
95
+ ...(workflowChildRun !== undefined ? { workflowChildRun: { ...workflowChildRun } } : {}),
96
+ ...(pendingPrompt !== undefined ? { pendingPrompt: clonePrompt(pendingPrompt) } : {}),
97
+ ...(promptFootprint !== undefined ? { promptFootprint: clonePrompt(promptFootprint) } : {}),
98
+ ...(inputRequest !== undefined ? { inputRequest: cloneInputRequest(inputRequest) } : {}),
99
+ ...(notices !== undefined ? { notices: notices.map((notice) => ({ ...notice })) } : {}),
100
+ };
101
+ }
102
+
103
+ function compactToolNode(node: ToolNodeSnapshot): ToolNodeSnapshot {
104
+ return { ...node, parentIds: [...node.parentIds] };
105
+ }
106
+
107
+ function compactRun(run: RunSnapshot): RunSnapshot {
108
+ const { inputs: _inputs, result: sourceResult, stages, toolNodes, pendingPrompt, ...metadata } = run;
109
+ const result = compactRunResult(sourceResult);
110
+ return {
111
+ ...metadata,
112
+ inputs: {},
113
+ stages: stages.map(compactStage),
114
+ ...(toolNodes !== undefined ? { toolNodes: toolNodes.map(compactToolNode) } : {}),
115
+ ...(pendingPrompt !== undefined ? { pendingPrompt: clonePrompt(pendingPrompt) } : {}),
116
+ ...(result !== undefined ? { result } : {}),
117
+ };
118
+ }
119
+
120
+ export function createGraphStoreSnapshot(
121
+ runs: readonly RunSnapshot[],
122
+ notices: readonly WorkflowNotice[],
123
+ version: number,
124
+ ): StoreSnapshot {
125
+ const snapshot: StoreSnapshot = {
126
+ runs: runs.map(compactRun),
127
+ notices: notices.map((notice) => ({ ...notice })),
128
+ version,
129
+ };
130
+ deepFreezeGraphValue(snapshot);
131
+ return snapshot;
132
+ }
133
+
134
+ function deepFreezeGraphValue(value: unknown): void {
135
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
136
+ Object.freeze(value);
137
+ if (Array.isArray(value)) {
138
+ for (const item of value) deepFreezeGraphValue(item);
139
+ return;
140
+ }
141
+ for (const key of Object.keys(value as Record<string, unknown>)) {
142
+ deepFreezeGraphValue((value as Record<string, unknown>)[key]);
143
+ }
144
+ }
@@ -139,7 +139,7 @@ export interface RunBlockedPayload {
139
139
 
140
140
  /**
141
141
  * Appends a `workflow.run.start` entry and labels it for /tree filtering.
142
- * Label format: `wf:<name>:<short-id>` (first 8 chars of runId).
142
+ * Label format: `wf:<name>:<full-run-id>`.
143
143
  */
144
144
  export function appendRunStart(api: PersistenceAPI, payload: RunStartPayload): void {
145
145
  if (typeof api.appendEntry !== "function") return;
@@ -156,8 +156,7 @@ export function appendRunStart(api: PersistenceAPI, payload: RunStartPayload): v
156
156
  ts: payload.ts,
157
157
  });
158
158
  if (entryId && typeof api.setLabel === "function") {
159
- const shortId = payload.runId.slice(0, 8);
160
- api.setLabel(entryId, `wf:${payload.name}:${shortId}`);
159
+ api.setLabel(entryId, `wf:${payload.name}:${payload.runId}`);
161
160
  }
162
161
  }
163
162
 
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The run identifier contract.
3
+ *
4
+ * A run id is a bare `crypto.randomUUID()` value (see `extension/dispatcher.ts`),
5
+ * and the durable DBOS `workflowId` is that same value. Every user-facing surface
6
+ * renders it in full, so every resolver accepts it only in full: there is no
7
+ * unique-prefix fallback and no truncated form that addresses a run.
8
+ *
9
+ * This lives in `shared/` because both the extension resolvers and the durable
10
+ * catalog need it, and `durable/` must not import from `extension/`.
11
+ */
12
+
13
+ /** Canonical rendered length of a run id, dashes included. */
14
+ export const RUN_ID_LENGTH = 36;
15
+
16
+ const RUN_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
17
+
18
+ /**
19
+ * True only for a full 8-4-4-4-12 hex UUID.
20
+ *
21
+ * Rejects a prefix, a 32-character dashless form, and a 36-character string that
22
+ * is the right length but not hex — a transposed or truncated paste should fail
23
+ * loudly here rather than silently miss during lookup.
24
+ */
25
+ export function isFullRunId(value: string): boolean {
26
+ return RUN_ID_PATTERN.test(value);
27
+ }
28
+
29
+ /**
30
+ * Reported instead of "not found" so a truncated paste is diagnosable as
31
+ * truncated. A well-formed id that no run happens to carry is a different
32
+ * failure and keeps the not-found message.
33
+ */
34
+ export function malformedRunIdMessage(target: string): string {
35
+ return `Run id must be a full ${RUN_ID_LENGTH}-character UUID; got "${target}" (${target.length} chars).`;
36
+ }
@@ -1,3 +1,4 @@
1
+ import { createGraphStoreSnapshot } from "./graph-store-snapshot.js";
1
2
  import type { PromptAnswerRecord, RunEndMetadata } from "./store-public-types.js";
2
3
  import type {
3
4
  PendingPrompt,
@@ -42,6 +43,7 @@ export interface StoreState {
42
43
  readonly runs: RunSnapshot[];
43
44
  readonly notices: WorkflowNotice[];
44
45
  readonly listeners: Set<(snap: StoreSnapshot) => void>;
46
+ readonly invalidationListeners: Set<() => void>;
45
47
  readonly resolvers: Map<string, ResolverEntry>;
46
48
  readonly stagePromptAnswers: Map<string, PromptAnswerRecord>;
47
49
  readonly stagePromptDrafts: Map<string, string>;
@@ -51,6 +53,7 @@ export interface StoreState {
51
53
  export interface StoreContext {
52
54
  readonly state: StoreState;
53
55
  snapshot(): StoreSnapshot;
56
+ graphSnapshot(): StoreSnapshot;
54
57
  notify(): void;
55
58
  bumpAndNotify(): void;
56
59
  findRun(runId: string): RunSnapshot | undefined;
@@ -128,6 +131,7 @@ export function createStoreState(): StoreState {
128
131
  runs: [],
129
132
  notices: [],
130
133
  listeners: new Set(),
134
+ invalidationListeners: new Set(),
131
135
  resolvers: new Map(),
132
136
  stagePromptAnswers: new Map(),
133
137
  stagePromptDrafts: new Map(),
@@ -136,16 +140,40 @@ export function createStoreState(): StoreState {
136
140
  }
137
141
 
138
142
  export function createStoreContext(state: StoreState = createStoreState()): StoreContext {
143
+ let cachedGraphVersion = -1;
144
+ let cachedGraphSnapshot: StoreSnapshot | undefined;
145
+
139
146
  function snapshot(): StoreSnapshot {
140
147
  return JSON.parse(
141
148
  JSON.stringify({ runs: state.runs, notices: state.notices, version: state.version }),
142
149
  ) as StoreSnapshot;
143
150
  }
144
151
 
152
+ function graphSnapshot(): StoreSnapshot {
153
+ if (cachedGraphSnapshot === undefined || cachedGraphVersion !== state.version) {
154
+ cachedGraphSnapshot = createGraphStoreSnapshot(state.runs, state.notices, state.version);
155
+ cachedGraphVersion = state.version;
156
+ }
157
+ return cachedGraphSnapshot;
158
+ }
159
+
145
160
  function notify(): void {
146
- const snap = snapshot();
147
- for (const fn of state.listeners) {
148
- fn(snap);
161
+ for (const fn of state.invalidationListeners) {
162
+ try {
163
+ fn();
164
+ } catch {
165
+ // One observer must not starve later observers or the snapshot channel.
166
+ }
167
+ }
168
+ if (state.listeners.size > 0) {
169
+ const snap = snapshot();
170
+ for (const fn of state.listeners) {
171
+ try {
172
+ fn(snap);
173
+ } catch {
174
+ // Store mutations remain authoritative when a consumer fails.
175
+ }
176
+ }
149
177
  }
150
178
  }
151
179
 
@@ -209,6 +237,7 @@ export function createStoreContext(state: StoreState = createStoreState()): Stor
209
237
  return {
210
238
  state,
211
239
  snapshot,
240
+ graphSnapshot,
212
241
  notify,
213
242
  bumpAndNotify,
214
243
  findRun,
@@ -0,0 +1,10 @@
1
+ import type { Store } from "./store-public-types.js";
2
+ import type { StoreSnapshot } from "./store-types.js";
3
+
4
+ export function readGraphStoreSnapshot(store: Store): StoreSnapshot {
5
+ return store.graphSnapshot();
6
+ }
7
+
8
+ export function subscribeStoreInvalidation(store: Store, listener: () => void): () => void {
9
+ return store.subscribeInvalidation(listener);
10
+ }
@@ -228,5 +228,13 @@ export interface Store {
228
228
  /** Drop every run and notice. */
229
229
  clear(): void;
230
230
  snapshot(): StoreSnapshot;
231
+ /**
232
+ * Return one immutable payload-free graph projection per store version.
233
+ * Graph-visible state may change only through a version-bumping store method;
234
+ * direct mutation of values returned by `runs()` would leave this cache stale.
235
+ */
236
+ graphSnapshot(): StoreSnapshot;
231
237
  subscribe(fn: (snap: StoreSnapshot) => void): () => void;
238
+ /** Subscribe synchronously to store invalidation without constructing a full snapshot. */
239
+ subscribeInvalidation(fn: () => void): () => void;
232
240
  }
@@ -27,7 +27,9 @@ type RunStoreMethods = Pick<
27
27
  | "recordRunResumed"
28
28
  | "clear"
29
29
  | "snapshot"
30
+ | "graphSnapshot"
30
31
  | "subscribe"
32
+ | "subscribeInvalidation"
31
33
  >;
32
34
 
33
35
  export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
@@ -179,6 +181,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
179
181
  if (!run) return false;
180
182
  if (TERMINAL_STATUSES.has(run.status)) return false;
181
183
  const wasPaused = run.status === "paused";
184
+ const enteringQuit = metadata?.exitReason === "quit" && run.exitReason !== "quit";
182
185
  if (!wasPaused) {
183
186
  run.status = "paused";
184
187
  run.pausedAt = pausedAt ?? Date.now();
@@ -186,6 +189,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
186
189
  }
187
190
  if (metadata?.resumable !== undefined) run.resumable = metadata.resumable;
188
191
  if (metadata?.exitReason !== undefined) run.exitReason = metadata.exitReason;
192
+ if (enteringQuit) run.quitAt = Date.now();
189
193
  if (wasPaused && metadata === undefined) return false;
190
194
  context.bumpAndNotify();
191
195
  return true;
@@ -201,6 +205,7 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
201
205
  run.pausedDurationMs = accumulatePausedDurationMs(run.pausedDurationMs, run.pausedAt, resumedTs);
202
206
  run.resumedAt = resumedTs;
203
207
  run.pausedAt = undefined;
208
+ delete run.quitAt;
204
209
  delete run.exitReason;
205
210
  context.bumpAndNotify();
206
211
  return true;
@@ -230,12 +235,21 @@ export function createRunStoreMethods(context: StoreContext): RunStoreMethods {
230
235
  return context.snapshot();
231
236
  },
232
237
 
238
+ graphSnapshot(): StoreSnapshot {
239
+ return context.graphSnapshot();
240
+ },
241
+
233
242
  subscribe(fn: (snap: StoreSnapshot) => void): () => void {
234
243
  state.listeners.add(fn);
235
244
  return () => {
236
245
  state.listeners.delete(fn);
237
246
  };
238
247
  },
248
+
249
+ subscribeInvalidation(fn: () => void): () => void {
250
+ state.invalidationListeners.add(fn);
251
+ return () => state.invalidationListeners.delete(fn);
252
+ },
239
253
  };
240
254
  }
241
255
 
@@ -153,6 +153,8 @@ export interface WorkflowChildReplaySnapshot {
153
153
  /** True when the child reached this terminal status through ctx.exit(). */
154
154
  readonly exited?: boolean;
155
155
  readonly outputs: WorkflowOutputValues;
156
+ /** Payload-free output count used by compact graph projections. */
157
+ readonly outputCount?: number;
156
158
  readonly exitReason?: string;
157
159
  }
158
160
 
@@ -282,6 +284,8 @@ export interface RunSnapshot {
282
284
  pausedDurationMs?: number;
283
285
  /** Timestamp set when a controlled pause begins; cleared on resume. */
284
286
  pausedAt?: number;
287
+ /** Timestamp when the run entered resumable quit state; display-only expiry marker. */
288
+ quitAt?: number;
285
289
  /** Timestamp recorded on the most recent resume from a paused state. */
286
290
  resumedAt?: number;
287
291
  result?: WorkflowOutputValues;
@@ -392,6 +392,8 @@ export interface WorkflowRunContext<
392
392
  > {
393
393
  /** Typed inputs provided by the caller, validated against the input schema. */
394
394
  readonly inputs: TInputs;
395
+ /** Stable owning workflow-run id for durable run-scoped artifacts. */
396
+ readonly runId?: string;
395
397
  /** Invocation working directory for workflow-owned artifacts. Defaults to the host process cwd when omitted. */
396
398
  readonly cwd?: string;
397
399
  /** Intentionally end this workflow run from any call depth. */
@@ -0,0 +1,261 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, rm, stat } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { getAgentDir, getEnvValue } from "@bastani/atomic";
6
+ import type { DurableWorkflowBackend } from "../durable/backend.js";
7
+ import { getDurableBackend } from "../durable/factory.js";
8
+ import { type WorkflowRunResumeCandidate, workflowRunHasPausedState } from "../durable/resume-eligibility.js";
9
+ import { store } from "./store.js";
10
+ import type { RunSnapshot } from "./store-types.js";
11
+
12
+ /** Maximum age of durable workflow run artifacts before the next workflow write prunes them. */
13
+ export const WORKFLOW_ARTIFACT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
14
+
15
+ /** Overrides the durable workflow-artifact root, mirroring the agent-directory override convention. */
16
+ export const ENV_WORKFLOW_ARTIFACT_DIR = "ATOMIC_WORKFLOW_ARTIFACT_DIR";
17
+
18
+ export type WorkflowArtifactRunState = "protected" | "terminal" | "orphan";
19
+ export type WorkflowArtifactRunStateResolver = (runId: string) => WorkflowArtifactRunState | undefined;
20
+
21
+ const ARTIFACT_PRUNE_RECHECK_MS = 60 * 60 * 1000;
22
+ const lastArtifactPruneAt = new Map<string, number>();
23
+ const pendingArtifactPrunes = new Map<string, Promise<void>>();
24
+
25
+ function workflowArtifactRoot(): string {
26
+ const override = getEnvValue(ENV_WORKFLOW_ARTIFACT_DIR);
27
+ if (override !== undefined && override.length > 0) return override;
28
+ return join(dirname(getAgentDir()), "workflows");
29
+ }
30
+
31
+ export function workflowArtifactRunsRoot(): string {
32
+ return join(workflowArtifactRoot(), "runs");
33
+ }
34
+
35
+ function safeRunId(runId: string): string {
36
+ const safe = runId.replace(/[^A-Za-z0-9._-]/g, "_");
37
+ return safe.length > 0 ? safe : "run";
38
+ }
39
+
40
+ export function workflowArtifactRunPath(runId: string): string {
41
+ return join(workflowArtifactRunsRoot(), safeRunId(runId));
42
+ }
43
+
44
+ /**
45
+ * Detect a run-scoped artifact path anywhere in a run's result or stages.
46
+ *
47
+ * `JSON.stringify` escapes each Windows separator, so `…\runs\<id>\…` serializes
48
+ * as `…\\runs\\<id>\\…` and a single backslash-to-slash pass leaves `//runs//`.
49
+ * Collapsing repeated separators after that pass makes the probe behave the same
50
+ * on both platforms; without it every Windows artifact reference was invisible,
51
+ * so no run was ever reported as having lost its artifacts.
52
+ */
53
+ export function workflowRunHasArtifactReference(run: Pick<RunSnapshot, "id" | "result" | "stages">): boolean {
54
+ const serialized = JSON.stringify({ result: run.result, stages: run.stages });
55
+ if (serialized === undefined) return false;
56
+ const normalized = serialized.replaceAll("\\", "/").replace(/\/{2,}/g, "/");
57
+ return normalized.includes(`/runs/${safeRunId(run.id)}/`);
58
+ }
59
+
60
+ /** Return artifact integrity only for runs whose snapshot names a run-scoped artifact. */
61
+ export function workflowRunArtifactsIntact(run: Pick<RunSnapshot, "id" | "result" | "stages">): boolean | undefined {
62
+ if (!workflowRunHasArtifactReference(run)) return undefined;
63
+ return existsSync(workflowArtifactRunPath(run.id));
64
+ }
65
+
66
+ /** Build the one resume candidate shape used by all live-run resume surfaces. */
67
+ export function workflowRunResumeCandidate(run: RunSnapshot): WorkflowRunResumeCandidate {
68
+ const artifactsIntact = workflowRunArtifactsIntact(run);
69
+ let hasDurableCheckpoint: boolean | undefined;
70
+ try {
71
+ hasDurableCheckpoint = getDurableBackend().isWorkflowLoadable(run.id);
72
+ } catch {
73
+ // A backend that is not ready cannot prove a missing checkpoint.
74
+ }
75
+ return {
76
+ ...run,
77
+ hasPausedState: workflowRunHasPausedState(run),
78
+ ...(hasDurableCheckpoint === undefined ? {} : { hasDurableCheckpoint }),
79
+ ...(artifactsIntact === undefined ? {} : { artifactsIntact }),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Protection means live work, not "could theoretically be resumed".
85
+ *
86
+ * A run still in flight, holding a pending prompt, or explicitly quit keeps its
87
+ * artifacts indefinitely, because a resume is expected and those files are its
88
+ * inputs. A run that has FAILED is terminal: it is retryable, but the retention
89
+ * window is precisely the grace period it gets. Treating `resumable === true` as
90
+ * protection made every failed run permanent, so repeated recoverable failures
91
+ * accumulated run-scoped artifacts that the advertised window never reclaimed.
92
+ */
93
+ function storeRunState(run: RunSnapshot): WorkflowArtifactRunState {
94
+ const hasPendingInput =
95
+ run.pendingPrompt !== undefined ||
96
+ run.stages.some(
97
+ (stage) => stage.status === "awaiting_input" || stage.status === "paused" || stage.status === "blocked",
98
+ );
99
+ if (
100
+ hasPendingInput ||
101
+ run.status === "pending" ||
102
+ run.status === "running" ||
103
+ run.status === "paused" ||
104
+ run.status === "blocked" ||
105
+ run.exitReason === "quit"
106
+ ) {
107
+ return "protected";
108
+ }
109
+ return "terminal";
110
+ }
111
+
112
+ function durableRunState(backend: DurableWorkflowBackend, runId: string): WorkflowArtifactRunState | undefined {
113
+ const handle = backend.getLoadableWorkflow(runId);
114
+ if (handle === undefined) return undefined;
115
+ // `isDurableWorkflowResumable` deliberately accepts `failed`, so it cannot be
116
+ // used here: it would pin a failed run's artifacts forever.
117
+ if (
118
+ handle.status === "running" ||
119
+ handle.status === "paused" ||
120
+ handle.status === "blocked" ||
121
+ handle.pendingPrompts > 0
122
+ ) {
123
+ return "protected";
124
+ }
125
+ return "terminal";
126
+ }
127
+
128
+ /**
129
+ * Snapshot the live store and durable backend once for a pruning pass. This
130
+ * keeps the transcript write path from querying durable state once per stage.
131
+ */
132
+ export function createWorkflowArtifactRunStateResolver(): WorkflowArtifactRunStateResolver {
133
+ const localRuns = new Map(store.runs().map((run) => [run.id, run]));
134
+ let backend: DurableWorkflowBackend | undefined;
135
+ let backendReady = true;
136
+ try {
137
+ backend = getDurableBackend();
138
+ } catch {
139
+ backendReady = false;
140
+ }
141
+ return (runId: string): WorkflowArtifactRunState | undefined => {
142
+ const local = localRuns.get(runId) ?? [...localRuns.values()].find((run) => safeRunId(run.id) === runId);
143
+ const localState = local === undefined ? undefined : storeRunState(local);
144
+ if (!backendReady) {
145
+ // An unavailable durable backend cannot prove that even a terminal-looking
146
+ // local snapshot has no resumable durable counterpart.
147
+ return localState === "protected" ? "protected" : undefined;
148
+ }
149
+ let durable: WorkflowArtifactRunState | undefined;
150
+ if (backend !== undefined) {
151
+ try {
152
+ durable = durableRunState(backend, runId);
153
+ } catch {
154
+ backendReady = false;
155
+ return localState === "protected" ? "protected" : undefined;
156
+ }
157
+ }
158
+ if (durable === "protected" || localState === "protected") return "protected";
159
+ if (durable === "terminal" || localState === "terminal") return "terminal";
160
+ return "orphan";
161
+ };
162
+ }
163
+
164
+ /** Resolve one run's artifact ownership from a fresh durable/store snapshot. */
165
+ export function resolveWorkflowArtifactRunState(runId: string): WorkflowArtifactRunState | undefined {
166
+ return createWorkflowArtifactRunStateResolver()(runId);
167
+ }
168
+
169
+ /** Delete the durable owner of a terminal run before its artifacts are removed. */
170
+ async function deleteTerminalDurableEntry(runId: string): Promise<boolean> {
171
+ let backend: DurableWorkflowBackend;
172
+ try {
173
+ backend = getDurableBackend();
174
+ } catch {
175
+ return false;
176
+ }
177
+ try {
178
+ const result = await backend.deleteWorkflowIfInactive(runId);
179
+ return result.ok || result.reason === "not_found";
180
+ } catch {
181
+ return false;
182
+ }
183
+ }
184
+
185
+ /** Remove stale run directories, retaining artifacts for resumable/non-terminal runs. */
186
+ export async function pruneWorkflowArtifactRuns(
187
+ root = workflowArtifactRunsRoot(),
188
+ now = Date.now(),
189
+ stateResolver?: WorkflowArtifactRunStateResolver,
190
+ ): Promise<void> {
191
+ try {
192
+ const entries = await readdir(root, { withFileTypes: true, encoding: "utf8" });
193
+ for (const entry of entries) {
194
+ if (!entry.isDirectory()) continue;
195
+ const entryPath = join(root, entry.name);
196
+ let metadata: Awaited<ReturnType<typeof stat>>;
197
+ try {
198
+ metadata = await stat(entryPath);
199
+ } catch (error) {
200
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") continue;
201
+ throw error;
202
+ }
203
+ if (now - metadata.mtimeMs <= WORKFLOW_ARTIFACT_RETENTION_MS) continue;
204
+ // Without authoritative state, preserve the directory. An unknown run is
205
+ // not safe to classify as history merely because its mtime is old.
206
+ if (stateResolver === undefined) continue;
207
+ const state = stateResolver(entry.name);
208
+ if (state !== "terminal" && state !== "orphan") continue;
209
+ // Delete the durable owner first. If authoritative deletion is unavailable
210
+ // or refuses an active run, preserve the directory rather than leaving a
211
+ // durable entry pointing at missing artifacts.
212
+ if (state === "terminal" && !(await deleteTerminalDurableEntry(entry.name))) continue;
213
+ await rm(entryPath, { recursive: true, force: true });
214
+ }
215
+ } catch (error) {
216
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
217
+ throw error;
218
+ }
219
+ }
220
+
221
+ async function pruneArtifactRootIfDue(root: string, now: number): Promise<void> {
222
+ const pending = pendingArtifactPrunes.get(root);
223
+ if (pending !== undefined) {
224
+ try {
225
+ await pending;
226
+ } catch {
227
+ // Pruning is opportunistic and must never block a stage artifact write.
228
+ }
229
+ return;
230
+ }
231
+ const previous = lastArtifactPruneAt.get(root);
232
+ if (previous !== undefined && now - previous < ARTIFACT_PRUNE_RECHECK_MS) return;
233
+ const operation = pruneWorkflowArtifactRuns(root, now, createWorkflowArtifactRunStateResolver());
234
+ pendingArtifactPrunes.set(root, operation);
235
+ try {
236
+ await operation;
237
+ } catch {
238
+ // A stale sibling that cannot be inspected is retained; writes continue.
239
+ } finally {
240
+ lastArtifactPruneAt.set(root, now);
241
+ pendingArtifactPrunes.delete(root);
242
+ }
243
+ }
244
+
245
+ /** Ensure a run-scoped durable directory exists and perform bounded state-aware GC at most hourly. */
246
+ export async function ensureWorkflowArtifactRunDirectory(runId: string): Promise<string> {
247
+ const root = workflowArtifactRunsRoot();
248
+ await pruneArtifactRootIfDue(root, Date.now());
249
+ const directory = workflowArtifactRunPath(runId);
250
+ await mkdir(directory, { recursive: true });
251
+ return directory;
252
+ }
253
+
254
+ /** Create a unique durable artifact directory beneath the owning run and prune expired siblings. */
255
+ export async function createWorkflowArtifactDirectory(runId?: string): Promise<string> {
256
+ const effectiveRunId = runId ?? randomUUID();
257
+ const runDirectory = await ensureWorkflowArtifactRunDirectory(effectiveRunId);
258
+ const artifactDirectory = join(runDirectory, `artifact-${randomUUID()}`);
259
+ await mkdir(artifactDirectory, { recursive: true });
260
+ return artifactDirectory;
261
+ }