@bastani/atomic 0.9.13-alpha.2 → 0.9.13-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/builtin/i-have-adhd/package.json +1 -1
  4. package/dist/builtin/intercom/CHANGELOG.md +14 -0
  5. package/dist/builtin/intercom/README.md +12 -12
  6. package/dist/builtin/intercom/incoming-message-delivery.ts +1 -1
  7. package/dist/builtin/intercom/index.ts +1 -1
  8. package/dist/builtin/intercom/intercom-tool.ts +1 -1
  9. package/dist/builtin/intercom/intercom-utils.ts +3 -6
  10. package/dist/builtin/intercom/package.json +1 -1
  11. package/dist/builtin/intercom/result-renderers.ts +1 -1
  12. package/dist/builtin/intercom/session-target.ts +1 -15
  13. package/dist/builtin/intercom/skills/intercom/SKILL.md +6 -6
  14. package/dist/builtin/intercom/terminal-ordering-barrier.ts +2 -2
  15. package/dist/builtin/intercom/ui/inline-message.ts +7 -5
  16. package/dist/builtin/intercom/ui/session-list.ts +23 -10
  17. package/dist/builtin/mcp/package.json +1 -1
  18. package/dist/builtin/subagents/CHANGELOG.md +19 -0
  19. package/dist/builtin/subagents/README.md +29 -73
  20. package/dist/builtin/subagents/package.json +2 -2
  21. package/dist/builtin/subagents/prompts/parallel-cleanup.md +1 -1
  22. package/dist/builtin/subagents/prompts/parallel-review.md +1 -1
  23. package/dist/builtin/subagents/prompts/review-loop.md +2 -2
  24. package/dist/builtin/subagents/skills/subagent/SKILL.md +23 -59
  25. package/dist/builtin/subagents/skills/tmux/SKILL.md +1 -1
  26. package/dist/builtin/subagents/src/extension/control-notices.ts +2 -3
  27. package/dist/builtin/subagents/src/extension/doctor.ts +2 -14
  28. package/dist/builtin/subagents/src/extension/fanout-child.ts +1 -17
  29. package/dist/builtin/subagents/src/extension/index.ts +9 -70
  30. package/dist/builtin/subagents/src/extension/notification-content.ts +1 -1
  31. package/dist/builtin/subagents/src/extension/prompt-guidance.ts +2 -2
  32. package/dist/builtin/subagents/src/extension/schemas.ts +2 -8
  33. package/dist/builtin/subagents/src/extension/tool-description.ts +2 -3
  34. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +1 -1
  35. package/dist/builtin/subagents/src/intercom/result-intercom.ts +5 -46
  36. package/dist/builtin/subagents/src/runs/{background → foreground}/completion-notification.ts +2 -2
  37. package/dist/builtin/subagents/src/runs/foreground/inprocess-run-sync.ts +25 -88
  38. package/dist/builtin/subagents/src/runs/{background → foreground}/notify.ts +5 -5
  39. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-context.ts +11 -28
  40. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel-task.ts +3 -0
  41. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-parallel.ts +2 -0
  42. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-runtime.ts +0 -5
  43. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-single.ts +30 -20
  44. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-status.ts +4 -4
  45. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-types.ts +0 -13
  46. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +104 -48
  47. package/dist/builtin/subagents/src/runs/inprocess/index.ts +1 -1
  48. package/dist/builtin/subagents/src/runs/inprocess/runner.ts +5 -5
  49. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-api.ts +0 -7
  50. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-core.ts +0 -10
  51. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-projection.ts +3 -108
  52. package/dist/builtin/subagents/src/runs/inprocess/runtime-support/nested-sanitize.ts +0 -4
  53. package/dist/builtin/subagents/src/runs/shared/subagent-control.ts +1 -1
  54. package/dist/builtin/subagents/src/shared/formatters.ts +3 -5
  55. package/dist/builtin/subagents/src/shared/model-resolution.ts +78 -0
  56. package/dist/builtin/subagents/src/shared/status-format.ts +4 -11
  57. package/dist/builtin/subagents/src/shared/types-config.ts +10 -9
  58. package/dist/builtin/subagents/src/shared/types-nested.ts +127 -0
  59. package/dist/builtin/subagents/src/shared/types-results.ts +2 -8
  60. package/dist/builtin/subagents/src/shared/types-runtime.ts +0 -9
  61. package/dist/builtin/subagents/src/shared/types.ts +1 -1
  62. package/dist/builtin/subagents/src/shared/utils.ts +1 -109
  63. package/dist/builtin/subagents/src/slash/slash-commands.ts +7 -17
  64. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +2 -252
  65. package/dist/builtin/subagents/src/tui/render-layout.ts +4 -4
  66. package/dist/builtin/subagents/src/tui/render-result.ts +0 -9
  67. package/dist/builtin/subagents/src/tui/render-stable-output.ts +1 -34
  68. package/dist/builtin/subagents/src/tui/render.ts +0 -2
  69. package/dist/builtin/web-access/package.json +1 -1
  70. package/dist/builtin/workflows/CHANGELOG.md +12 -0
  71. package/dist/builtin/workflows/README.md +3 -3
  72. package/dist/builtin/workflows/package.json +1 -1
  73. package/dist/builtin/workflows/src/durable/backend.ts +11 -0
  74. package/dist/builtin/workflows/src/durable/completed-catalog-stage-groups.ts +63 -25
  75. package/dist/builtin/workflows/src/durable/completed-catalog.ts +166 -25
  76. package/dist/builtin/workflows/src/durable/dbos-backend.ts +42 -24
  77. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +2 -1
  78. package/dist/builtin/workflows/src/durable/run-timing.ts +28 -7
  79. package/dist/builtin/workflows/src/durable/scoped-backend.ts +4 -0
  80. package/dist/builtin/workflows/src/durable/targeted-inspection.ts +110 -0
  81. package/dist/builtin/workflows/src/engine/run-durable-stage-session.ts +2 -2
  82. package/dist/builtin/workflows/src/engine/runtime.ts +19 -2
  83. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +3 -0
  84. package/dist/builtin/workflows/src/extension/index.bundle.mjs +680 -152
  85. package/dist/builtin/workflows/src/extension/runtime-durable-resume.ts +6 -0
  86. package/dist/builtin/workflows/src/extension/ui-surface.ts +2 -0
  87. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +6 -1
  88. package/dist/builtin/workflows/src/extension/workflow-targets.ts +18 -10
  89. package/dist/builtin/workflows/src/extension/workflow-tool-inspection.ts +31 -16
  90. package/dist/builtin/workflows/src/extension/workflow-tool-send.ts +75 -3
  91. package/dist/builtin/workflows/src/extension/workflow-tool.ts +53 -6
  92. package/dist/builtin/workflows/src/runs/background/run-inspect.ts +5 -1
  93. package/dist/builtin/workflows/src/runs/foreground/executor-stage-call.ts +94 -60
  94. package/dist/builtin/workflows/src/runs/foreground/executor-stage-context.ts +1 -1
  95. package/dist/builtin/workflows/src/runs/foreground/executor-stage-control.ts +7 -7
  96. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +25 -4
  97. package/dist/builtin/workflows/src/runs/foreground/executor-stage-types.ts +4 -1
  98. package/dist/builtin/workflows/src/runs/foreground/executor-types.ts +2 -0
  99. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +142 -4
  100. package/dist/builtin/workflows/src/runs/foreground/stage-runner-types.ts +2 -0
  101. package/dist/builtin/workflows/src/runs/foreground/stage-session-heartbeat.ts +88 -0
  102. package/dist/builtin/workflows/src/tui/run-detail.ts +25 -12
  103. package/dist/builtin/workflows/src/tui/workflow-resume-selector.ts +1 -0
  104. package/dist/core/agent-session-events.d.ts.map +1 -1
  105. package/dist/core/agent-session-events.js +16 -5
  106. package/dist/core/agent-session-events.js.map +1 -1
  107. package/dist/core/agent-session-export.d.ts.map +1 -1
  108. package/dist/core/agent-session-export.js +2 -1
  109. package/dist/core/agent-session-export.js.map +1 -1
  110. package/dist/core/agent-session-methods.d.ts +8 -4
  111. package/dist/core/agent-session-methods.d.ts.map +1 -1
  112. package/dist/core/agent-session-methods.js.map +1 -1
  113. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  114. package/dist/core/agent-session-prompt.js +5 -0
  115. package/dist/core/agent-session-prompt.js.map +1 -1
  116. package/dist/core/agent-session-summary.d.ts +21 -0
  117. package/dist/core/agent-session-summary.d.ts.map +1 -0
  118. package/dist/core/agent-session-summary.js +156 -0
  119. package/dist/core/agent-session-summary.js.map +1 -0
  120. package/dist/core/agent-session-tool-registry.d.ts.map +1 -1
  121. package/dist/core/agent-session-tool-registry.js +0 -4
  122. package/dist/core/agent-session-tool-registry.js.map +1 -1
  123. package/dist/core/agent-session-transfer.d.ts.map +1 -1
  124. package/dist/core/agent-session-transfer.js +0 -2
  125. package/dist/core/agent-session-transfer.js.map +1 -1
  126. package/dist/core/agent-session-tree.d.ts.map +1 -1
  127. package/dist/core/agent-session-tree.js +6 -0
  128. package/dist/core/agent-session-tree.js.map +1 -1
  129. package/dist/core/agent-session.d.ts +7 -3
  130. package/dist/core/agent-session.d.ts.map +1 -1
  131. package/dist/core/agent-session.js +8 -5
  132. package/dist/core/agent-session.js.map +1 -1
  133. package/dist/core/atomic-guide-command.js +2 -2
  134. package/dist/core/atomic-guide-command.js.map +1 -1
  135. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  136. package/dist/core/compaction/branch-summarization.js +1 -0
  137. package/dist/core/compaction/branch-summarization.js.map +1 -1
  138. package/dist/core/compaction/index.d.ts +1 -0
  139. package/dist/core/compaction/index.d.ts.map +1 -1
  140. package/dist/core/compaction/index.js +1 -0
  141. package/dist/core/compaction/index.js.map +1 -1
  142. package/dist/core/compaction/session-summarization.d.ts +41 -0
  143. package/dist/core/compaction/session-summarization.d.ts.map +1 -0
  144. package/dist/core/compaction/session-summarization.js +97 -0
  145. package/dist/core/compaction/session-summarization.js.map +1 -0
  146. package/dist/core/extensions/ui-types.d.ts +2 -0
  147. package/dist/core/extensions/ui-types.d.ts.map +1 -1
  148. package/dist/core/extensions/ui-types.js.map +1 -1
  149. package/dist/core/session-manager-core.d.ts +3 -1
  150. package/dist/core/session-manager-core.d.ts.map +1 -1
  151. package/dist/core/session-manager-core.js +9 -1
  152. package/dist/core/session-manager-core.js.map +1 -1
  153. package/dist/core/session-manager-entries.d.ts +24 -1
  154. package/dist/core/session-manager-entries.d.ts.map +1 -1
  155. package/dist/core/session-manager-entries.js +48 -0
  156. package/dist/core/session-manager-entries.js.map +1 -1
  157. package/dist/core/session-manager-list.d.ts.map +1 -1
  158. package/dist/core/session-manager-list.js +10 -0
  159. package/dist/core/session-manager-list.js.map +1 -1
  160. package/dist/core/session-manager-types.d.ts +10 -1
  161. package/dist/core/session-manager-types.d.ts.map +1 -1
  162. package/dist/core/session-manager-types.js.map +1 -1
  163. package/dist/core/settings-manager-basic-accessors.d.ts +3 -0
  164. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  165. package/dist/core/settings-manager-basic-accessors.js +5 -0
  166. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  167. package/dist/core/settings-manager.d.ts +1 -1
  168. package/dist/core/settings-manager.d.ts.map +1 -1
  169. package/dist/core/settings-manager.js.map +1 -1
  170. package/dist/core/settings-types.d.ts +4 -0
  171. package/dist/core/settings-types.d.ts.map +1 -1
  172. package/dist/core/settings-types.js.map +1 -1
  173. package/dist/core/slash-commands.js +2 -2
  174. package/dist/core/slash-commands.js.map +1 -1
  175. package/dist/core/tools/bash.d.ts +0 -15
  176. package/dist/core/tools/bash.d.ts.map +1 -1
  177. package/dist/core/tools/bash.js +3 -81
  178. package/dist/core/tools/bash.js.map +1 -1
  179. package/dist/core/tools/fetch-url.d.ts +12 -0
  180. package/dist/core/tools/fetch-url.d.ts.map +1 -1
  181. package/dist/core/tools/fetch-url.js +76 -10
  182. package/dist/core/tools/fetch-url.js.map +1 -1
  183. package/dist/core/tools/index.js +4 -4
  184. package/dist/core/tools/index.js.map +1 -1
  185. package/dist/core/tools/session-temp-dir.d.ts +2 -2
  186. package/dist/core/tools/session-temp-dir.d.ts.map +1 -1
  187. package/dist/core/tools/session-temp-dir.js +6 -7
  188. package/dist/core/tools/session-temp-dir.js.map +1 -1
  189. package/dist/core/usage-totals.js +1 -1
  190. package/dist/core/usage-totals.js.map +1 -1
  191. package/dist/modes/interactive/components/footer.js +1 -1
  192. package/dist/modes/interactive/components/footer.js.map +1 -1
  193. package/dist/modes/interactive/components/host-session-picker.d.ts.map +1 -1
  194. package/dist/modes/interactive/components/host-session-picker.js +1 -0
  195. package/dist/modes/interactive/components/host-session-picker.js.map +1 -1
  196. package/dist/modes/interactive/components/session-selector-list.d.ts.map +1 -1
  197. package/dist/modes/interactive/components/session-selector-list.js +29 -4
  198. package/dist/modes/interactive/components/session-selector-list.js.map +1 -1
  199. package/dist/modes/interactive/components/session-selector-search.js +1 -1
  200. package/dist/modes/interactive/components/session-selector-search.js.map +1 -1
  201. package/dist/modes/interactive/components/tree-selector-content.d.ts.map +1 -1
  202. package/dist/modes/interactive/components/tree-selector-content.js +6 -0
  203. package/dist/modes/interactive/components/tree-selector-content.js.map +1 -1
  204. package/dist/modes/interactive-engine/protocol.d.ts.map +1 -1
  205. package/dist/modes/interactive-engine/protocol.js +4 -1
  206. package/dist/modes/interactive-engine/protocol.js.map +1 -1
  207. package/docs/environment-variables.md +1 -1
  208. package/docs/intercom.md +20 -21
  209. package/docs/quickstart.md +5 -1
  210. package/docs/sdk.md +1 -1
  211. package/docs/session-format.md +15 -0
  212. package/docs/sessions.md +8 -0
  213. package/docs/settings.md +6 -0
  214. package/docs/subagents.md +19 -31
  215. package/docs/tools.md +3 -5
  216. package/docs/usage.md +1 -1
  217. package/docs/windows.md +1 -1
  218. package/docs/workflows.md +60 -32
  219. package/npm-shrinkwrap.json +29 -29
  220. package/package.json +2 -2
  221. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +0 -331
  222. package/dist/builtin/subagents/src/runs/foreground/subagent-executor-async.ts +0 -161
  223. package/dist/builtin/subagents/src/runs/inprocess/background-single.ts +0 -195
  224. package/dist/builtin/subagents/src/runs/inprocess/background.ts +0 -90
  225. package/dist/builtin/subagents/src/shared/types-async.ts +0 -247
  226. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +0 -217
  227. package/dist/builtin/subagents/src/tui/render-widget.ts +0 -366
  228. package/dist/core/async/format.d.ts +0 -3
  229. package/dist/core/async/format.d.ts.map +0 -1
  230. package/dist/core/async/format.js +0 -52
  231. package/dist/core/async/format.js.map +0 -1
  232. package/dist/core/async/job-manager.d.ts +0 -40
  233. package/dist/core/async/job-manager.d.ts.map +0 -1
  234. package/dist/core/async/job-manager.js +0 -293
  235. package/dist/core/async/job-manager.js.map +0 -1
  236. package/dist/core/async/session-manager.d.ts +0 -16
  237. package/dist/core/async/session-manager.d.ts.map +0 -1
  238. package/dist/core/async/session-manager.js +0 -34
  239. package/dist/core/async/session-manager.js.map +0 -1
  240. package/dist/core/async/types.d.ts +0 -23
  241. package/dist/core/async/types.d.ts.map +0 -1
  242. package/dist/core/async/types.js +0 -2
  243. package/dist/core/async/types.js.map +0 -1
  244. package/dist/core/tools/bash-async-execution.d.ts +0 -27
  245. package/dist/core/tools/bash-async-execution.d.ts.map +0 -1
  246. package/dist/core/tools/bash-async-execution.js +0 -81
  247. package/dist/core/tools/bash-async-execution.js.map +0 -1
  248. package/dist/core/tools/bash-async-jobs.d.ts +0 -30
  249. package/dist/core/tools/bash-async-jobs.d.ts.map +0 -1
  250. package/dist/core/tools/bash-async-jobs.js +0 -87
  251. package/dist/core/tools/bash-async-jobs.js.map +0 -1
  252. package/dist/core/tools/bash-async-output.d.ts +0 -13
  253. package/dist/core/tools/bash-async-output.d.ts.map +0 -1
  254. package/dist/core/tools/bash-async-output.js +0 -122
  255. package/dist/core/tools/bash-async-output.js.map +0 -1
@@ -1,257 +1,7 @@
1
- import { formatNestedAggregate } from "../runs/inprocess/runtime-support/nested-rendering.ts";
2
- import { formatDuration, formatModelThinking, shortenPath } from "../shared/formatters.ts";
3
- import { formatAgentRunningLabel } from "../shared/status-format.ts";
4
- import {
5
- type AsyncJobState,
6
- type AsyncJobStep,
7
- MAX_SUBAGENT_NESTING_DEPTH,
8
- type NestedRunSummary,
9
- type NestedStepSummary,
10
- } from "../shared/types.ts";
11
- import { runningGlyph, runningPulseGlyph, type Theme, truncLine } from "./render-layout.ts";
12
- import {
13
- buildLiveStatusLine,
14
- formatCurrentToolLine,
15
- formatTokenStat,
16
- formatToolUseStat,
17
- statJoin,
18
- } from "./render-status-progress.ts";
19
-
20
- export function formatWidgetAgents(agents: string[]): string {
21
- const distinct = [...new Set(agents)];
22
- if (distinct.length === 1 && agents.length > 1) return `${distinct[0]} ×${agents.length}`;
23
- if (agents.length > 3) return `${agents.slice(0, 2).join(", ")} +${agents.length - 2} more`;
24
- return agents.join(", ");
25
- }
26
-
27
- export function widgetJobName(job: AsyncJobState): string {
28
- if (job.mode === "parallel") return "parallel";
29
- if (job.mode === "single" && job.agents?.length === 1) return job.agents[0]!;
30
- if (job.agents?.length) return formatWidgetAgents(job.agents);
31
- return job.mode ?? "subagent";
32
- }
33
-
34
- export function widgetActivity(job: AsyncJobState): string {
35
- const facts: string[] = [];
36
- if (job.currentTool && job.currentToolStartedAt !== undefined && job.updatedAt !== undefined)
37
- facts.push(`${job.currentTool} ${formatDuration(Math.max(0, job.updatedAt - job.currentToolStartedAt))}`);
38
- else if (job.currentTool) facts.push(job.currentTool);
39
- if (job.currentPath) facts.push(shortenPath(job.currentPath));
40
- if (job.turnCount !== undefined) facts.push(`${job.turnCount} turns`);
41
- if (job.toolCount !== undefined) facts.push(`${job.toolCount} tools`);
42
- const activity = buildLiveStatusLine(job, job.updatedAt);
43
- if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
44
- if (activity) return activity;
45
- if (facts.length) return facts.join(" · ");
46
- if (job.status === "running") return "thinking…";
47
- if (job.status === "queued") return "queued…";
48
- if (job.status === "paused") return "Paused";
49
- if (job.status === "failed") return "Failed";
50
- return "Done";
51
- }
52
-
53
- export function widgetStatusGlyph(job: AsyncJobState, theme: Theme, pulseFrame?: number): string {
54
- if (job.status === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
55
- if (job.status === "queued") return theme.fg("muted", "◦");
56
- if (job.status === "complete") return theme.fg("success", "✓");
57
- if (job.status === "paused") return theme.fg("warning", "■");
58
- return theme.fg("error", "✗");
59
- }
60
-
61
- export function widgetStepGlyph(status: AsyncJobStep["status"], theme: Theme, seed?: number, now?: number): string {
62
- if (status === "running") return theme.fg("accent", runningGlyph(seed, now));
63
- if (status === "complete" || status === "completed") return theme.fg("success", "✓");
64
- if (status === "failed") return theme.fg("error", "✗");
65
- if (status === "paused") return theme.fg("warning", "■");
66
- return theme.fg("muted", "◦");
67
- }
68
-
69
- export function widgetStepPulseGlyph(status: AsyncJobStep["status"], theme: Theme, pulseFrame?: number): string {
70
- if (status === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
71
- if (status === "complete" || status === "completed") return theme.fg("success", "✓");
72
- if (status === "failed") return theme.fg("error", "✗");
73
- if (status === "paused") return theme.fg("warning", "■");
74
- return theme.fg("muted", "◦");
75
- }
76
-
77
- export function widgetStepStatus(status: AsyncJobStep["status"], theme: Theme): string {
78
- if (status === "running") return theme.fg("accent", "running");
79
- if (status === "complete" || status === "completed") return theme.fg("success", "complete");
80
- if (status === "failed") return theme.fg("error", "failed");
81
- if (status === "paused") return theme.fg("warning", "paused");
82
- return theme.fg("dim", status);
83
- }
84
-
85
- export function widgetStepActivity(step: NonNullable<AsyncJobState["steps"]>[number], snapshotNow?: number): string {
86
- const facts: string[] = [];
87
- if (step.currentTool && step.currentToolStartedAt !== undefined && snapshotNow !== undefined)
88
- facts.push(`${step.currentTool} ${formatDuration(Math.max(0, snapshotNow - step.currentToolStartedAt))}`);
89
- else if (step.currentTool) facts.push(step.currentTool);
90
- if (step.currentPath) facts.push(shortenPath(step.currentPath));
91
- if (step.turnCount !== undefined) facts.push(`${step.turnCount} turns`);
92
- if (step.toolCount !== undefined) facts.push(`${step.toolCount} tools`);
93
- if (step.tokens?.total) facts.push(formatTokenStat(step.tokens.total));
94
- const activity = buildLiveStatusLine(step, snapshotNow);
95
- if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
96
- if (activity) return activity;
97
- return facts.join(" · ");
98
- }
99
-
100
- export function widgetStats(job: AsyncJobState, theme: Theme): string {
101
- const parts: string[] = [];
102
- const stepsTotal = job.stepsTotal ?? job.agents?.length ?? 1;
103
- if (job.activeParallelGroup) {
104
- const running = job.runningSteps ?? (job.status === "running" ? 1 : 0);
105
- const done = job.completedSteps ?? (job.status === "complete" ? stepsTotal : 0);
106
- if (job.status === "running" && running > 0) parts.push(formatAgentRunningLabel(running));
107
- if (stepsTotal > 0) parts.push(`${done}/${stepsTotal} done`);
108
- } else if (stepsTotal > 1) {
109
- parts.push(`steps ${stepsTotal}`);
110
- }
111
- if (job.toolCount !== undefined) parts.push(formatToolUseStat(job.toolCount));
112
- if (job.totalTokens?.total) parts.push(formatTokenStat(job.totalTokens.total));
113
- if (job.startedAt !== undefined && job.updatedAt !== undefined)
114
- parts.push(formatDuration(Math.max(0, job.updatedAt - job.startedAt)));
115
- return statJoin(theme, parts);
116
- }
117
-
118
- export function widgetStepStats(theme: Theme, step: NonNullable<AsyncJobState["steps"]>[number]): string {
119
- return statJoin(theme, [
120
- step.turnCount !== undefined ? `${step.turnCount} turns` : "",
121
- step.toolCount !== undefined ? formatToolUseStat(step.toolCount) : "",
122
- step.tokens?.total ? formatTokenStat(step.tokens.total) : "",
123
- step.durationMs !== undefined ? formatDuration(step.durationMs) : "",
124
- ]);
125
- }
1
+ import { formatModelThinking } from "../shared/formatters.ts";
2
+ import type { Theme } from "./render-layout.ts";
126
3
 
127
4
  export function modelThinkingBadge(theme: Theme, model?: string, thinking?: string, fastMode?: boolean): string {
128
5
  const label = formatModelThinking(model, thinking, fastMode);
129
6
  return label ? theme.fg("dim", ` (${label})`) : "";
130
7
  }
131
-
132
- export function widgetStepActivityLine(
133
- step: NonNullable<AsyncJobState["steps"]>[number],
134
- width: number,
135
- expanded: boolean,
136
- snapshotNow?: number,
137
- ): string {
138
- const toolLine = formatCurrentToolLine(step, width, expanded, snapshotNow);
139
- if (toolLine) return toolLine;
140
- const activity = buildLiveStatusLine(step, snapshotNow);
141
- if (activity) return activity;
142
- if (step.status === "running") return "thinking…";
143
- return "";
144
- }
145
-
146
- export function nestedRunName(run: NestedRunSummary): string {
147
- if (run.agent) return run.agent;
148
- if (run.agents?.length) return formatWidgetAgents(run.agents);
149
- return run.id;
150
- }
151
-
152
- export function nestedStatusGlyph(
153
- state: NestedRunSummary["state"] | NestedStepSummary["status"],
154
- theme: Theme,
155
- pulseFrame?: number,
156
- ): string {
157
- if (state === "running") return theme.fg("accent", runningPulseGlyph(pulseFrame));
158
- if (state === "complete" || state === "completed") return theme.fg("success", "✓");
159
- if (state === "failed") return theme.fg("error", "✗");
160
- if (state === "paused") return theme.fg("warning", "■");
161
- return theme.fg("muted", "◦");
162
- }
163
-
164
- export function nestedActivity(
165
- input: Pick<
166
- NestedRunSummary | NestedStepSummary,
167
- | "activityState"
168
- | "lastActivityAt"
169
- | "currentTool"
170
- | "currentToolStartedAt"
171
- | "currentPath"
172
- | "turnCount"
173
- | "toolCount"
174
- >,
175
- state: NestedRunSummary["state"] | NestedStepSummary["status"],
176
- snapshotNow?: number,
177
- ): string {
178
- const facts: string[] = [];
179
- if (input.currentTool && input.currentToolStartedAt !== undefined && snapshotNow !== undefined)
180
- facts.push(`${input.currentTool} ${formatDuration(Math.max(0, snapshotNow - input.currentToolStartedAt))}`);
181
- else if (input.currentTool) facts.push(input.currentTool);
182
- if (input.currentPath) facts.push(shortenPath(input.currentPath));
183
- if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
184
- if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
185
- const activity = buildLiveStatusLine(input, snapshotNow);
186
- if (activity && facts.length) return `${activity} · ${facts.join(" · ")}`;
187
- if (activity) return activity;
188
- if (facts.length) return facts.join(" · ");
189
- if (state === "running") return "thinking…";
190
- if (state === "queued" || state === "pending") return "queued…";
191
- if (state === "paused") return "Paused";
192
- if (state === "failed") return "Failed";
193
- return "Done";
194
- }
195
-
196
- export function formatNestedWidgetLines(
197
- children: NestedRunSummary[] | undefined,
198
- theme: Theme,
199
- width: number,
200
- expanded: boolean,
201
- snapshotNow?: number,
202
- lineBudget = expanded ? 12 : 1,
203
- now?: number,
204
- ): string[] {
205
- if (!children?.length || lineBudget <= 0) return [];
206
- if (!expanded) {
207
- const aggregate = formatNestedAggregate(children);
208
- return aggregate ? [theme.fg("dim", `↳ ${aggregate}`)] : [];
209
- }
210
- const lines: string[] = [];
211
- const maxDepth = MAX_SUBAGENT_NESTING_DEPTH;
212
- const append = (items: NestedRunSummary[] | undefined, depth: number, prefix: string): void => {
213
- if (!items?.length || lines.length >= lineBudget) return;
214
- if (depth > maxDepth) {
215
- const aggregate = formatNestedAggregate(items);
216
- if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix}↳ ${aggregate}`));
217
- return;
218
- }
219
- for (let index = 0; index < items.length; index++) {
220
- const child = items[index]!;
221
- if (lines.length >= lineBudget) {
222
- const aggregate = formatNestedAggregate(items.slice(index));
223
- if (aggregate) lines[lines.length - 1] = theme.fg("dim", `${prefix}↳ ${aggregate}`);
224
- return;
225
- }
226
- const activity = nestedActivity(child, child.state, snapshotNow ?? child.lastUpdate);
227
- const error = child.error ? ` · ${child.error}` : "";
228
- lines.push(
229
- theme.fg(
230
- "dim",
231
- `${prefix}↳ ${nestedStatusGlyph(child.state, theme, now)} ${nestedRunName(child)} · ${child.state} · ${activity}${error}`,
232
- ),
233
- );
234
- if (depth === maxDepth) {
235
- const aggregate = formatNestedAggregate([
236
- ...(child.steps?.flatMap((step) => step.children ?? []) ?? []),
237
- ...(child.children ?? []),
238
- ]);
239
- if (aggregate && lines.length < lineBudget) lines.push(theme.fg("dim", `${prefix} ↳ ${aggregate}`));
240
- continue;
241
- }
242
- for (const step of child.steps ?? []) {
243
- if (lines.length >= lineBudget) return;
244
- lines.push(
245
- theme.fg(
246
- "dim",
247
- `${prefix} ↳ ${nestedStatusGlyph(step.status, theme, now)} ${step.agent} · ${step.status} · ${nestedActivity(step, step.status, snapshotNow ?? child.lastUpdate)}`,
248
- ),
249
- );
250
- append(step.children, depth + 1, `${prefix} `);
251
- }
252
- append(child.children, depth + 1, `${prefix} `);
253
- }
254
- };
255
- append(children, 0, "");
256
- return lines.map((line) => truncLine(line, width));
257
- }
@@ -76,7 +76,7 @@ export const RUNNING_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦",
76
76
  * the clock. This fixes the frozen/stuttering spinner from issue #1084.
77
77
  *
78
78
  * IMPORTANT: a wall-clock spinner only stays flicker-free for widgets pinned to
79
- * the bottom of the buffer (e.g. the below-editor async widget), where every
79
+ * the bottom of the buffer (e.g. a below-editor widget), where every
80
80
  * tick stays inside the viewport. Content rendered into chat scrollback (live
81
81
  * foreground subagent results) can scroll above the viewport fold; there, even
82
82
  * a single-cell spinner diff forces pi-tui into a destructive full-screen +
@@ -135,9 +135,9 @@ export function pulseGlyph(frame?: number): string {
135
135
  }
136
136
 
137
137
  /**
138
- * Running async/background subagent pulse. It reuses the exact foreground pulse
139
- * glyph frames while accepting an explicit frame advanced by the widget only on
140
- * real async progress/status updates, matching foreground result semantics.
138
+ * Running subagent pulse. It reuses the exact foreground pulse glyph frames
139
+ * while accepting an explicit frame advanced by the renderer on real progress
140
+ * updates, matching foreground result semantics.
141
141
  */
142
142
  export function runningPulseGlyph(frame?: number): string {
143
143
  return pulseGlyph(frame);
@@ -69,15 +69,6 @@ export function renderSubagentResult(
69
69
  theme: Theme,
70
70
  ): Component {
71
71
  const d = result.details;
72
- if (d?.asyncId && d.results.length === 0) {
73
- const contextPrefix = d.context === "fork" ? `${theme.fg("warning", "[fork]")} ` : "";
74
- const container = new Container();
75
- container.addChild(new Text(`${contextPrefix}${theme.fg("success", "launched")} · async run ${d.asyncId}`, 0, 0));
76
- container.addChild(
77
- new Text(theme.fg("dim", "completion pending; the detached result will be delivered when it finishes"), 0, 0),
78
- );
79
- return container;
80
- }
81
72
  const liveMultiProgress = d?.mode === "parallel" && (d?.progress?.length ?? 0) > 0;
82
73
  if (!d?.results.length && !liveMultiProgress) {
83
74
  const t = result.content[0];
@@ -1,29 +1,5 @@
1
1
  import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
- import type { AgentProgress, AsyncJobState, Details } from "../shared/types.ts";
3
-
4
- export function widgetRenderKey(job: AsyncJobState): string {
5
- return JSON.stringify({
6
- asyncDir: job.asyncDir,
7
- status: job.status,
8
- activityState: job.activityState,
9
- lastActivityAt: job.lastActivityAt,
10
- currentTool: job.currentTool,
11
- currentToolStartedAt: job.currentToolStartedAt,
12
- currentPath: job.currentPath,
13
- turnCount: job.turnCount,
14
- toolCount: job.toolCount,
15
- mode: job.mode,
16
- agents: job.agents,
17
- steps: job.steps,
18
- nestedChildren: job.nestedChildren,
19
- stepsTotal: job.stepsTotal,
20
- runningSteps: job.runningSteps,
21
- completedSteps: job.completedSteps,
22
- startedAt: job.startedAt,
23
- updatedAt: job.updatedAt,
24
- totalTokens: job.totalTokens,
25
- });
26
- }
2
+ import type { AgentProgress, Details } from "../shared/types.ts";
27
3
 
28
4
  export function progressRenderKey(progress: Partial<AgentProgress> | undefined): string {
29
5
  if (!progress) return "";
@@ -42,14 +18,6 @@ export function progressRenderKey(progress: Partial<AgentProgress> | undefined):
42
18
  ].join(":");
43
19
  }
44
20
 
45
- export function isRunningSubagentResult(result: AgentToolResult<Details>): boolean {
46
- return (
47
- result.details?.progress?.some((entry) => entry.status === "running") ||
48
- result.details?.results.some((entry) => entry.progress?.status === "running") ||
49
- false
50
- );
51
- }
52
-
53
21
  export function subagentResultRenderKey(
54
22
  result: AgentToolResult<Details>,
55
23
  options: { expanded: boolean; isPartial: boolean },
@@ -75,7 +43,6 @@ export function subagentResultRenderKey(
75
43
  options.isPartial ? "partial" : "final",
76
44
  options.expanded ? "expanded" : "compact",
77
45
  details.mode,
78
- details.asyncId ?? "",
79
46
  details.totalSteps ?? "",
80
47
  progressRenderKey(details.progressSummary),
81
48
  progressKeys.join("|"),
@@ -21,5 +21,3 @@ export {
21
21
  clearResultAnimationTimer,
22
22
  stopResultAnimations,
23
23
  } from "./render-result-animation.ts";
24
- export { widgetRenderKey } from "./render-stable-output.ts";
25
- export { buildWidgetLines, renderWidget, stopWidgetAnimation } from "./render-widget.ts";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.13-alpha.2",
3
+ "version": "0.9.13-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.13-alpha.3] - 2026-08-13
10
+
11
+ ### Changed
12
+
13
+ - Workflow model fallback attempt metadata now includes per-attempt input/output tokens, cache reads/writes, provider-reported total cost, and meaningful assistant-turn counts when usage is available ([#2197](https://github.com/bastani-inc/atomic/issues/2197)).
14
+
15
+ ### Fixed
16
+
17
+ - Corrected stale README claims that `workflow` tool `runId`/`stageId` targets and expanded-graph stage identifiers accept prefixes; run ids require the full 36-character UUID and stage ids/names match whole, matching `resolveRunId` and `stageMatchesExpandedIdentifier` behavior.
18
+ - Fixed exact workflow inspection returning `Run not found` after process loss even when DBOS retained a stale running, resumable root. `workflow` status/stages/stage/transcript actions with an explicit full run id and `/workflow status <id>` now hydrate only that DBOS root on a local miss, reconstruct its authoritative checkpoint DAG as read-only state, report stale owners as `crashed` with explicit resume guidance, protect fresh foreign owners, distinguish absent/deleted/malformed records, and reject cyclic or nonreciprocal topology without claiming, transitioning, dispatching, or auto-resuming the run. Status without an id remains current-session-only.
19
+ - Fixed long model turns losing their active stage session identity and root liveness when the process died between turn boundaries. Atomic now records stage start before the first session checkpoint, waits for that identity to persist before the first model use, and runs serialized, unref'd 30-second checkpoints for root and nested scoped stages. Heartbeats stop on every exit, refresh root metadata without status transitions, and surface persistent storage or MCP cleanup faults instead of dropping them in detached promise handlers. A checkpoint still in flight when the stage shuts down is drained rather than abandoned, so a late durability failure is never silently discarded, and a stage whose final durability checkpoint fails is recorded as failed instead of completed while its caller still receives the error and its concurrency slot is still released; explicit resume still replays completed effects exactly once.
20
+
9
21
  ## [0.9.13-alpha.2] - 2026-08-12
10
22
 
11
23
  ### Breaking Changes
@@ -246,7 +246,7 @@ The child executes as a nested workflow behind a parent boundary stage named `wo
246
246
 
247
247
  For durable runs, Atomic writes and awaits a versioned boundary-start identity before child code can run. That record fixes the boundary id, child run id, owning root/parent, source order and parents, replay scope, alias, workflow, and a deterministic fingerprint of the child definition plus exact validated inputs across pause, process restart, cached replay, and completed inspection. Distinct-input parallel calls therefore keep their own cache even when restart reverses dispatch order; repeated identical calls share the fingerprint but retain a per-invocation ordinal. A completed child boundary and its `ctx.tool` side effects replay exactly once from the root's scoped checkpoints; only incomplete work continues.
248
248
 
249
- If no valid child graph can stand in for the boundary—including a failed or skipped boundary, a missing or empty child graph, stale or mismatched ownership metadata, or a recursive link that cannot produce a valid expansion—the expanded graph keeps the boundary summary instead of flattening an unrelated or invalid child. Exact expanded ids resolve first; a local id, prefix, or stage name is accepted only when it identifies one visible stage. Inputs are strictly validated against the child workflow before it starts: unknown keys, missing required values, type mismatches, and invalid `select` choices fail before the child body runs. The parent receives the child's declared `outputs` on `child.outputs` after those outputs pass their declared runtime type checks.
249
+ If no valid child graph can stand in for the boundary—including a failed or skipped boundary, a missing or empty child graph, stale or mismatched ownership metadata, or a recursive link that cannot produce a valid expansion—the expanded graph keeps the boundary summary instead of flattening an unrelated or invalid child. Exact expanded ids resolve first; a local id or stage name is accepted only when it identifies one visible stage, and identifiers always match whole—a stage id is never truncated, so a prefix never selects a stage. Inputs are strictly validated against the child workflow before it starts: unknown keys, missing required values, type mismatches, and invalid `select` choices fail before the child body runs. The parent receives the child's declared `outputs` on `child.outputs` after those outputs pass their declared runtime type checks.
250
250
 
251
251
  For workflows intended to be called as children, declare an `outputs` entry for every non-default field a parent should rely on. `outputs` is only the schema/contract: use normal TypeScript in `run()` to gather values from any stage/task/child workflow and return those keys.
252
252
 
@@ -663,8 +663,8 @@ Raw stage-chat prompt answer replay is live-memory only. `StageSnapshot.promptAn
663
663
  "workflow": "string (optional) — workflow ID or normalized name",
664
664
  "inputs": "object (optional) — key/value map of workflow inputs",
665
665
  "action": "'run' | 'list' | 'get' | 'inputs' | 'models' | 'status' | 'stages' | 'stage' | 'transcript' | 'send' | 'pause' | 'interrupt' | 'quit' | 'resume' | 'reload'",
666
- "runId": "optional run id or unique prefix; control actions default to the active run where safe; use '--all' or all:true for pause/interrupt/quit all",
667
- "stageId": "optional stage id, prefix, or name for stage-scoped actions; cannot be combined with all:true",
666
+ "runId": "optional full 36-character run id; prefixes are rejected; control actions default to the active run where safe; use '--all' or all:true for pause/interrupt/quit all",
667
+ "stageId": "optional exact stage id or exact stage name for stage-scoped actions; prefixes and partial names are rejected; cannot be combined with all:true",
668
668
  "statusFilter": "optional filter for stages or the no-runId status run listing: pending/running/awaiting_input/paused/blocked/completed/failed/skipped/cancelled/killed/all; for the status listing, run statuses match directly and awaiting_input selects runs with a pending human prompt",
669
669
  "format": "optional agent-facing output format: text or json",
670
670
  "limit": "transcript-only explicit maximum number of recent entries; omitted with tail omitted uses the path-only default when sessionFile/transcriptPath exists",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/workflows",
3
- "version": "0.9.13-alpha.2",
3
+ "version": "0.9.13-alpha.3",
4
4
  "private": true,
5
5
  "description": "Atomic extension for multi-stage workflow authoring and execution.",
6
6
  "contributors": [
@@ -44,6 +44,10 @@ export type DurableInactiveDeleteResult =
44
44
  | { readonly ok: true }
45
45
  | { readonly ok: false; readonly reason: "not_found" | "running" };
46
46
 
47
+ export type DurableWorkflowHydrationResult =
48
+ | { readonly kind: "current"; readonly handle: DurableWorkflowHandle }
49
+ | { readonly kind: "absent" | "deleted" | "malformed" };
50
+
47
51
  /** DBOS is the sole persistent implementation. In-memory is a test seam and the non-durable last-resort fallback. */
48
52
  export interface DurableWorkflowBackend {
49
53
  /** Whether state survives the current process. */
@@ -154,6 +158,8 @@ export interface DurableWorkflowBackend {
154
158
 
155
159
  /** Hydrate one workflow from persistent storage. */
156
160
  hydrateWorkflow(workflowId: string): Promise<void>;
161
+ /** Optional exact, read-only hydration classification for targeted inspection. */
162
+ hydrateWorkflowForInspection?(workflowId: string): Promise<DurableWorkflowHydrationResult>;
157
163
  /** Hydrate all catalog candidates from persistent storage. */
158
164
  hydrateResumableWorkflows(): Promise<void>;
159
165
  promptReservationScope(workflowId: string): { readonly rootWorkflowId: string; readonly scope: string };
@@ -352,6 +358,11 @@ export class InMemoryDurableBackend implements DurableWorkflowBackend {
352
358
  }
353
359
 
354
360
  async hydrateWorkflow(_workflowId: string): Promise<void> {}
361
+ async hydrateWorkflowForInspection(workflowId: string): Promise<DurableWorkflowHydrationResult> {
362
+ const handle = this.getLoadableWorkflow(workflowId);
363
+ if (handle !== undefined) return { kind: "current", handle };
364
+ return { kind: this.deletedWorkflowIds.has(workflowId) ? "deleted" : "absent" };
365
+ }
355
366
 
356
367
  async hydrateResumableWorkflows(): Promise<void> {}
357
368
 
@@ -1,10 +1,11 @@
1
1
  import type { RunSnapshot, StageSnapshot } from "../shared/store-types.js";
2
2
  import { boundaryTransitionError, resolveBoundaryLifecycle } from "./boundary-lifecycle.js";
3
- import type {
4
- DurableCheckpoint,
5
- DurableStageCheckpoint,
6
- DurableStageRunTopology,
7
- DurableToolCheckpoint,
3
+ import {
4
+ DURABLE_TOOL_TOPOLOGY_VERSION,
5
+ type DurableCheckpoint,
6
+ type DurableStageCheckpoint,
7
+ type DurableStageRunTopology,
8
+ type DurableToolCheckpoint,
8
9
  } from "./types.js";
9
10
  import { parseLegacyWorkflowChildResult, parseWorkflowChildResult } from "./workflow-child-result.js";
10
11
 
@@ -28,7 +29,11 @@ export interface StageDraft {
28
29
  readonly topology?: DurableStageCheckpoint["topology"];
29
30
  }
30
31
 
31
- export function validBoundaryRecordSet(checkpoints: readonly DurableCheckpoint[], strict: boolean): boolean {
32
+ export function validBoundaryRecordSet(
33
+ checkpoints: readonly DurableCheckpoint[],
34
+ strict: boolean,
35
+ requireActiveFingerprint = false,
36
+ ): boolean {
32
37
  const stages = checkpoints.filter((item): item is DurableStageCheckpoint => item.kind === "stage");
33
38
  const boundaries = stages.filter((item) => item.topology?.boundary !== undefined);
34
39
  const startsByReplay = new Map<string, DurableStageCheckpoint[]>();
@@ -47,27 +52,53 @@ export function validBoundaryRecordSet(checkpoints: readonly DurableCheckpoint[]
47
52
  )
48
53
  )
49
54
  return false;
50
- return [...startsByReplay.values()].every(
51
- (starts) => boundaryTransitionError(starts[0]!, stages, strict) === undefined,
52
- );
55
+ return [...startsByReplay.values()].every((starts) => {
56
+ const start = starts[0]!;
57
+ if (boundaryTransitionError(start, stages, strict) !== undefined) return false;
58
+ if (!requireActiveFingerprint || start.topology!.boundary!.invocationFingerprint !== undefined) return true;
59
+ return (
60
+ stages.some(
61
+ (stage) =>
62
+ stage.replayKey === start.replayKey &&
63
+ stage.topology?.boundary?.event === "terminal" &&
64
+ stage.topology.boundary.status === "completed",
65
+ ) && boundaryTransitionError(start, stages, true) === undefined
66
+ );
67
+ });
53
68
  }
54
69
 
55
70
  export function validStageGroup(
56
71
  drafts: readonly StageDraft[],
57
72
  runId: string,
58
- allowedNodeIds: ReadonlySet<string> = new Set(),
73
+ rootRunId: string,
74
+ tools: readonly DurableToolCheckpoint[] = [],
59
75
  ): boolean {
60
76
  if (drafts.some((draft) => draft.topology!.stageId.length === 0)) return false;
61
- const stageIds = new Set(drafts.map((draft) => draft.topology!.stageId));
62
- if (stageIds.size !== drafts.length) return false;
63
- const ids = new Set([...allowedNodeIds, ...stageIds]);
64
- if (ids.size !== allowedNodeIds.size + stageIds.size) return false;
77
+ if (
78
+ tools.some(
79
+ (tool) =>
80
+ tool.topology !== undefined &&
81
+ (tool.topology.version !== DURABLE_TOOL_TOPOLOGY_VERSION || tool.topology.nodeId.length === 0),
82
+ )
83
+ )
84
+ return false;
85
+ const stageIds = drafts.map((draft) => draft.topology!.stageId);
86
+ const toolIds = tools.map((tool) => tool.topology?.nodeId ?? tool.checkpointId);
87
+ const ids = new Set([...stageIds, ...toolIds]);
88
+ if (ids.size !== stageIds.length + toolIds.length) return false;
65
89
  const orders = drafts.flatMap((draft) =>
66
90
  draft.topology!.sourceOrder === undefined ? [] : [draft.topology!.sourceOrder],
67
91
  );
68
92
  if (new Set(orders).size !== orders.length) return false;
69
- if (drafts.some((draft) => draft.topology!.parentIds.some((parentId) => !ids.has(parentId)))) return false;
70
- const runs = drafts.flatMap((draft) => (draft.topology?.run === undefined ? [] : [draft.topology.run]));
93
+ const parentEntries: ReadonlyArray<readonly [string, readonly string[]]> = [
94
+ ...drafts.map((draft) => [draft.topology!.stageId, draft.topology!.parentIds] as const),
95
+ ...tools.map((tool) => [tool.topology?.nodeId ?? tool.checkpointId, tool.topology?.parentIds ?? []] as const),
96
+ ];
97
+ if (parentEntries.some(([, parentIds]) => parentIds.some((parentId) => !ids.has(parentId)))) return false;
98
+ const runs = [
99
+ ...drafts.flatMap((draft) => (draft.topology?.run === undefined ? [] : [draft.topology.run])),
100
+ ...tools.flatMap((tool) => (tool.topology?.run === undefined ? [] : [tool.topology.run])),
101
+ ];
71
102
  const first = runs[0];
72
103
  if (
73
104
  runs.some(
@@ -81,19 +112,26 @@ export function validStageGroup(
81
112
  )
82
113
  )
83
114
  return false;
84
- const parents = new Map(drafts.map((draft) => [draft.topology!.stageId, draft.topology!.parentIds]));
115
+ if (
116
+ runId === rootRunId &&
117
+ runs.some(
118
+ (run) => run.parentRunId !== undefined || run.parentStageId !== undefined || run.rootRunId !== undefined,
119
+ )
120
+ )
121
+ return false;
122
+ const parents = new Map(parentEntries);
85
123
  const visiting = new Set<string>();
86
124
  const visited = new Set<string>();
87
- const cyclic = (stageId: string): boolean => {
88
- if (visiting.has(stageId)) return true;
89
- if (visited.has(stageId)) return false;
90
- visiting.add(stageId);
91
- if ((parents.get(stageId) ?? []).some(cyclic)) return true;
92
- visiting.delete(stageId);
93
- visited.add(stageId);
125
+ const cyclic = (nodeId: string): boolean => {
126
+ if (visiting.has(nodeId)) return true;
127
+ if (visited.has(nodeId)) return false;
128
+ visiting.add(nodeId);
129
+ if ((parents.get(nodeId) ?? []).some(cyclic)) return true;
130
+ visiting.delete(nodeId);
131
+ visited.add(nodeId);
94
132
  return false;
95
133
  };
96
- return ![...stageIds].some(cyclic);
134
+ return ![...ids].some(cyclic);
97
135
  }
98
136
 
99
137
  export function compareDraftSourceOrder(left: StageDraft, right: StageDraft): number {