@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
@@ -2,7 +2,12 @@ import { createStructuredOutputCapture, runCallback } from "@bastani/atomic";
2
2
  import type { StageExecutionMeta } from "../../shared/types.js";
3
3
  import { StageSessionController } from "./stage-runner-controller.js";
4
4
  import { assistantMessage } from "./stage-runner-messages.js";
5
- import { finalizePromptOutput, splitPromptOptions, validatePromptOutputOptions } from "./stage-runner-output.js";
5
+ import {
6
+ finalizePromptOutput,
7
+ splitPromptOptions,
8
+ stageOutputInstruction,
9
+ validatePromptOutputOptions,
10
+ } from "./stage-runner-output.js";
6
11
  import {
7
12
  formatStructuredOutputCorrectionPrompt,
8
13
  STRUCTURED_OUTPUT_MAX_CORRECTIVE_PROMPTS,
@@ -27,18 +32,31 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
27
32
  };
28
33
  const controller = new StageSessionController(opts, meta, effectiveStageOptions, structuredOutputCapture);
29
34
  let lastAssistantText: string | undefined;
35
+ let lastFinalizedOutput: string | undefined;
36
+ let lastFinalizedMessageCount: number | undefined;
30
37
  let adapterMessages = [] as InternalStageContext["messages"];
31
38
 
32
39
  function runtimeCwd(): string {
33
40
  return typeof effectiveStageOptions?.cwd === "string" ? effectiveStageOptions.cwd : process.cwd();
34
41
  }
35
42
 
43
+ function finalizedOutputIsCurrent(): boolean {
44
+ return (
45
+ lastFinalizedOutput !== undefined &&
46
+ (lastFinalizedMessageCount === undefined ||
47
+ controller.currentSession?.messages.length === lastFinalizedMessageCount)
48
+ );
49
+ }
50
+
36
51
  return {
37
52
  name: stageName,
38
53
 
39
54
  async prompt(text, options) {
40
55
  const { sdkOptions, outputOptions } = splitPromptOptions(options);
41
56
  validatePromptOutputOptions(outputOptions);
57
+ // The runtime owns the artifact write, so it states that contract itself
58
+ // rather than relying on every workflow definition to describe it.
59
+ const promptText = `${text}${stageOutputInstruction(outputOptions)}`;
42
60
  if (structuredOutputCapture?.called) {
43
61
  throw new Error(
44
62
  "atomic-workflows: stage schema supports one prompt() call per stage context because structured_output may be called exactly once. Create a new ctx.stage(...) for each additional schema-backed prompt.",
@@ -52,14 +70,22 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
52
70
  }
53
71
  const rawText = await runCallback(
54
72
  { kind: "workflow.stage_adapter", name: `prompt:${stageName}`, runId, stageId },
55
- () => adapters.prompt!.prompt(text, meta),
73
+ () => adapters.prompt!.prompt(promptText, meta),
56
74
  );
57
- lastAssistantText = await finalizePromptOutput(rawText, outputOptions, runtimeCwd());
58
- adapterMessages = assistantMessage(lastAssistantText);
75
+ adapterMessages = assistantMessage(rawText);
76
+ lastAssistantText = await finalizePromptOutput(
77
+ rawText,
78
+ outputOptions,
79
+ runtimeCwd(),
80
+ runId,
81
+ adapterMessages,
82
+ );
83
+ lastFinalizedOutput = lastAssistantText;
84
+ lastFinalizedMessageCount = controller.currentSession?.messages.length;
59
85
  return lastAssistantText;
60
86
  }
61
87
  if (structuredOutputCapture) {
62
- let nextPrompt = text;
88
+ let nextPrompt = promptText;
63
89
  let correctiveAttempts = 0;
64
90
  let structuredOutputError = STRUCTURED_OUTPUT_MISSING_ERROR;
65
91
  while (!structuredOutputCapture.called) {
@@ -74,17 +100,35 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
74
100
  nextPrompt = formatStructuredOutputCorrectionPrompt(structuredOutputError, correctiveAttempts);
75
101
  }
76
102
  const rawStructuredText = stringifyStructuredOutputValue(structuredOutputCapture.value);
77
- lastAssistantText = await finalizePromptOutput(rawStructuredText, outputOptions, runtimeCwd());
103
+ lastAssistantText = await finalizePromptOutput(
104
+ rawStructuredText,
105
+ outputOptions,
106
+ runtimeCwd(),
107
+ runId,
108
+ controller.currentSession?.messages,
109
+ );
110
+ lastFinalizedOutput = lastAssistantText;
111
+ lastFinalizedMessageCount = controller.currentSession?.messages.length;
78
112
  return structuredOutputCapture.value as never;
79
113
  }
80
- await controller.promptWithFallback(text, sdkOptions);
114
+ await controller.promptWithFallback(promptText, sdkOptions);
81
115
  const rawText = controller.lastAssistantText(lastAssistantText) ?? "";
82
- lastAssistantText = await finalizePromptOutput(rawText, outputOptions, runtimeCwd());
116
+ lastAssistantText = await finalizePromptOutput(
117
+ rawText,
118
+ outputOptions,
119
+ runtimeCwd(),
120
+ runId,
121
+ controller.currentSession?.messages,
122
+ );
123
+ lastFinalizedOutput = lastAssistantText;
124
+ lastFinalizedMessageCount = controller.currentSession?.messages.length;
83
125
  return lastAssistantText;
84
126
  },
85
127
 
86
128
  async complete(text, completeOpts) {
87
129
  if (adapters.complete) {
130
+ lastFinalizedOutput = undefined;
131
+ lastFinalizedMessageCount = undefined;
88
132
  lastAssistantText = await runCallback(
89
133
  { kind: "workflow.stage_adapter", name: `complete:${stageName}`, runId, stageId },
90
134
  () => adapters.complete!.complete(text, completeOpts, meta),
@@ -102,6 +146,8 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
102
146
  );
103
147
  }
104
148
  await controller.promptWithFallback(text, undefined, "complete");
149
+ lastFinalizedOutput = undefined;
150
+ lastFinalizedMessageCount = undefined;
105
151
  lastAssistantText = controller.lastAssistantText(lastAssistantText) ?? "";
106
152
  return lastAssistantText;
107
153
  },
@@ -195,11 +241,11 @@ export function createStageContext(opts: StageRunnerOpts): InternalStageContext
195
241
  },
196
242
 
197
243
  __getLastAssistantText() {
198
- return controller.lastAssistantText(lastAssistantText);
244
+ return finalizedOutputIsCurrent() ? lastFinalizedOutput : controller.lastAssistantText(lastAssistantText);
199
245
  },
200
246
 
201
247
  getLastAssistantText() {
202
- return controller.lastAssistantText(lastAssistantText);
248
+ return finalizedOutputIsCurrent() ? lastFinalizedOutput : controller.lastAssistantText(lastAssistantText);
203
249
  },
204
250
 
205
251
  async __ensureSession() {
@@ -65,6 +65,7 @@ export class StageSessionController {
65
65
  private activeCreation: Promise<StageSessionRuntime> | undefined;
66
66
  private sessionPromise: Promise<StageSessionRuntime> | undefined;
67
67
  private reattachSessionFile: string | undefined;
68
+ private lastPromptStartIndex: number | undefined;
68
69
  private readonly terminatingToolCallIds = new Set<string>();
69
70
  private latestStructuredOutputToolErrorValue: string | undefined;
70
71
  private unsubscribeTerminateWatcher: (() => void) | undefined;
@@ -116,6 +117,10 @@ export class StageSessionController {
116
117
  return this.session;
117
118
  }
118
119
 
120
+ get currentPromptStartIndex(): number | undefined {
121
+ return this.lastPromptStartIndex;
122
+ }
123
+
119
124
  get latestStructuredOutputToolError(): string | undefined {
120
125
  return this.latestStructuredOutputToolErrorValue;
121
126
  }
@@ -478,6 +483,7 @@ export class StageSessionController {
478
483
  continue;
479
484
  }
480
485
  const promptStartIndex = activeSession.messages.length;
486
+ this.lastPromptStartIndex = promptStartIndex;
481
487
  this.unresolvedContextOverflowMessage = undefined;
482
488
  try {
483
489
  await activeSession.prompt(nextText, sdkOptions);
@@ -1,11 +1,29 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, resolve } from "node:path";
3
- import type { PromptOptions } from "@bastani/atomic";
3
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
4
+ import type { AgentSession, PromptOptions } from "@bastani/atomic";
4
5
  import type { StageOutputOptions, StagePromptOptions, WorkflowMaxOutput } from "../../shared/types.js";
6
+ import { ensureWorkflowArtifactRunDirectory, workflowArtifactRunPath } from "../../shared/workflow-artifacts.js";
5
7
 
6
8
  const DEFAULT_MAX_OUTPUT_BYTES = 200 * 1024;
7
9
  const DEFAULT_MAX_OUTPUT_LINES = 5000;
8
10
 
11
+ type TranscriptContentBlock = {
12
+ readonly type?: string;
13
+ readonly text?: string;
14
+ readonly name?: string;
15
+ readonly id?: string;
16
+ };
17
+
18
+ type TranscriptMessage = {
19
+ readonly role?: string;
20
+ readonly content?: string | readonly TranscriptContentBlock[];
21
+ readonly customType?: string;
22
+ readonly stageAdmissionKey?: string;
23
+ readonly toolCallId?: string;
24
+ readonly toolName?: string;
25
+ };
26
+
9
27
  function normalizeMaxOutput(maxOutput: WorkflowMaxOutput | undefined): Required<WorkflowMaxOutput> {
10
28
  return {
11
29
  bytes: maxOutput?.bytes ?? DEFAULT_MAX_OUTPUT_BYTES,
@@ -45,7 +63,7 @@ function truncateOutput(text: string, maxOutput: WorkflowMaxOutput | undefined):
45
63
  function countLines(text: string): number {
46
64
  if (!text) return 0;
47
65
  const newlineMatches = text.match(/\r\n|\r|\n/g);
48
- return (newlineMatches?.length ?? 0) + (/[\r\n]$/.test(text) ? 0 : 1);
66
+ return (newlineMatches?.length ?? 0) + (text.endsWith("\n") || text.endsWith("\r") ? 0 : 1);
49
67
  }
50
68
 
51
69
  function formatByteSize(bytes: number): string {
@@ -60,11 +78,68 @@ function formatByteSize(bytes: number): string {
60
78
  return `${value.toFixed(1)} ${units[unitIndex]}`;
61
79
  }
62
80
 
63
- function savedOutputReference(outputPath: string, fullOutput: string): string {
81
+ function transcriptPath(runId: string, outputPath: string): string {
82
+ const digest = createHash("sha256").update(outputPath).digest("hex").slice(0, 16);
83
+ return join(workflowArtifactRunPath(runId), "transcripts", `${digest}-${basename(outputPath)}.transcript.md`);
84
+ }
85
+
86
+ function appendContent(lines: string[], content: TranscriptMessage["content"]): void {
87
+ if (typeof content === "string") {
88
+ lines.push(content);
89
+ return;
90
+ }
91
+ if (!Array.isArray(content)) return;
92
+ for (const block of content) {
93
+ if (block.type === "text" && typeof block.text === "string") {
94
+ lines.push(block.text);
95
+ continue;
96
+ }
97
+ const serialized = JSON.stringify(block);
98
+ if (serialized !== undefined) lines.push(serialized);
99
+ }
100
+ }
101
+
102
+ function renderTranscript(messages: AgentSession["messages"] | undefined, fallbackOutput: string): string {
103
+ const lines = ["# Workflow stage transcript", ""];
104
+ for (const [index, message] of (messages ?? []).entries()) {
105
+ const record = message as TranscriptMessage;
106
+ const role = typeof record.role === "string" ? record.role : "message";
107
+ lines.push(`## ${index + 1} ${role}`);
108
+ if (typeof record.customType === "string") lines.push(`customType: ${record.customType}`);
109
+ if (typeof record.stageAdmissionKey === "string") lines.push(`stageAdmissionKey: ${record.stageAdmissionKey}`);
110
+ if (typeof record.toolName === "string") lines.push(`toolName: ${record.toolName}`);
111
+ if (typeof record.toolCallId === "string") lines.push(`toolCallId: ${record.toolCallId}`);
112
+ appendContent(lines, record.content);
113
+ lines.push("");
114
+ }
115
+ if ((messages?.length ?? 0) === 0) {
116
+ lines.push("## 1 assistant");
117
+ lines.push(fallbackOutput);
118
+ lines.push("");
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+
123
+ function savedOutputReference(
124
+ outputPath: string,
125
+ fullOutput: string,
126
+ transcriptPathValue: string,
127
+ transcriptText: string,
128
+ transcriptAvailable: boolean,
129
+ warning?: string,
130
+ ): string {
64
131
  const absolutePath = resolve(outputPath);
65
132
  const bytes = Buffer.byteLength(fullOutput, "utf8");
66
133
  const lines = countLines(fullOutput);
67
- return `Output saved to: ${absolutePath} (${formatByteSize(bytes)}, ${lines} ${lines === 1 ? "line" : "lines"}). Read this file if needed.`;
134
+ const transcriptBytes = Buffer.byteLength(transcriptText, "utf8");
135
+ const transcriptLines = countLines(transcriptText);
136
+ const outputReference = `Output saved to: ${absolutePath} (${formatByteSize(bytes)}, ${lines} ${lines === 1 ? "line" : "lines"}). Read this file if needed.`;
137
+ const transcriptReference = transcriptAvailable
138
+ ? `Transcript saved to: ${transcriptPathValue} (${formatByteSize(transcriptBytes)}, ${transcriptLines} ${transcriptLines === 1 ? "line" : "lines"}). Search it with rg, read narrow line ranges, and do not read it whole.`
139
+ : `Transcript unavailable at: ${transcriptPathValue}. Search it with rg when it becomes available, read narrow line ranges, and do not read it whole.`;
140
+ return [outputReference, transcriptReference, warning]
141
+ .filter((line): line is string => line !== undefined)
142
+ .join("\n");
68
143
  }
69
144
 
70
145
  function resolveOutputPath(
@@ -83,6 +158,19 @@ function resolveOutputPath(
83
158
  return resolve(baseCwd, output);
84
159
  }
85
160
 
161
+ /**
162
+ * Report only what can be observed, never what the text appears to mean.
163
+ *
164
+ * An empty artifact is a fact. Whether a non-empty artifact is "really" a
165
+ * pointer, an acknowledgement, or a deliverable is an inference this code
166
+ * cannot make, so it does not try: the companion transcript named in every
167
+ * receipt is the recovery path for anything that looks wrong to a reader.
168
+ */
169
+ function degenerateWarning(fullOutput: string): string | undefined {
170
+ return fullOutput.trim().length === 0
171
+ ? "WARNING: the stage artifact is empty; search the companion transcript for this stage's work."
172
+ : undefined;
173
+ }
86
174
  export function splitPromptOptions(options: StagePromptOptions | undefined): {
87
175
  sdkOptions: PromptOptions | undefined;
88
176
  outputOptions: StageOutputOptions;
@@ -122,23 +210,66 @@ export function validatePromptOutputOptions(outputOptions: StageOutputOptions):
122
210
  }
123
211
  }
124
212
 
213
+ /**
214
+ * Instruction appended to any prompt whose stage configures `output:`.
215
+ *
216
+ * The runtime owns the artifact write, so the prompt author should not have to
217
+ * describe that plumbing — or remember to. Without this, a stage can end with
218
+ * "done, see the file" and persist that sentence as its deliverable, which is a
219
+ * foot-gun the workflow definition cannot see.
220
+ */
221
+ export function stageOutputInstruction(outputOptions: StageOutputOptions): string {
222
+ if (typeof outputOptions.output !== "string" || outputOptions.output.length === 0) return "";
223
+ return "\n\n[This stage's final message is saved verbatim as its artifact and is what later stages read. Return the complete result in that message rather than a pointer to it. Files you write yourself are not the artifact.]";
224
+ }
225
+
125
226
  export async function finalizePromptOutput(
126
227
  fullOutput: string,
127
228
  outputOptions: StageOutputOptions,
128
229
  runtimeCwd: string,
230
+ runId: string,
231
+ messages?: AgentSession["messages"],
129
232
  ): Promise<string> {
130
233
  const outputPath = resolveOutputPath(outputOptions.output, runtimeCwd, outputOptions.cwd);
131
234
  validatePromptOutputOptions(outputOptions);
132
235
  const displayOutput = truncateOutput(fullOutput, outputOptions.maxOutput);
133
236
  if (outputPath === undefined) return displayOutput;
134
237
 
238
+ const transcriptFile = transcriptPath(runId, outputPath);
239
+ const transcript = renderTranscript(messages, fullOutput);
135
240
  try {
136
241
  await mkdir(dirname(outputPath), { recursive: true });
137
242
  await writeFile(outputPath, fullOutput, "utf8");
138
243
  } catch (err) {
139
244
  return `${displayOutput}\n\nOutput file error: ${outputPath}\n${err instanceof Error ? err.message : String(err)}`;
140
245
  }
246
+ let transcriptAvailable = false;
247
+ let transcriptError: string | undefined;
248
+ try {
249
+ await ensureWorkflowArtifactRunDirectory(runId);
250
+ await mkdir(dirname(transcriptFile), { recursive: true });
251
+ await writeFile(transcriptFile, transcript, "utf8");
252
+ transcriptAvailable = true;
253
+ } catch (err) {
254
+ transcriptError = err instanceof Error ? err.message : String(err);
255
+ }
141
256
 
142
- const reference = savedOutputReference(outputPath, fullOutput);
257
+ const warning =
258
+ [
259
+ transcriptError === undefined
260
+ ? undefined
261
+ : `WARNING: companion transcript unavailable at ${transcriptFile}: ${transcriptError}`,
262
+ degenerateWarning(fullOutput),
263
+ ]
264
+ .filter((line): line is string => line !== undefined)
265
+ .join("\n") || undefined;
266
+ const reference = savedOutputReference(
267
+ outputPath,
268
+ fullOutput,
269
+ transcriptFile,
270
+ transcript,
271
+ transcriptAvailable,
272
+ warning,
273
+ );
143
274
  return outputOptions.outputMode === "file-only" ? reference : `${displayOutput}\n\n${reference}`;
144
275
  }
@@ -46,6 +46,8 @@ export interface WorkflowUIAdapter {
46
46
  }
47
47
  export interface WorkflowRunContext<TInputs extends WorkflowInputValues = WorkflowInputValues, TDefinitionBrand extends object = Record<never, never>, TOutputs extends WorkflowOutputValues = WorkflowOutputValues> {
48
48
  readonly inputs: Readonly<TInputs>;
49
+ /** Stable owning workflow-run id for durable run-scoped artifacts. */
50
+ readonly runId?: string;
49
51
  readonly cwd?: string;
50
52
  exit(options?: WorkflowExitOptions<TOutputs>): never;
51
53
  stage<TSchemaDef extends TSchema>(name: string, options: StageOptions<TSchemaDef> & {
@@ -88,6 +88,8 @@ export interface WorkflowRunContext<
88
88
  TOutputs extends WorkflowOutputValues = WorkflowOutputValues,
89
89
  > {
90
90
  readonly inputs: Readonly<TInputs>;
91
+ /** Stable owning workflow-run id for durable run-scoped artifacts. */
92
+ readonly runId?: string;
91
93
  readonly cwd?: string;
92
94
  exit(options?: WorkflowExitOptions<TOutputs>): never;
93
95
  stage<TSchemaDef extends TSchema>(
@@ -255,6 +255,157 @@ export function expandWorkflowGraph(snapshot: StoreSnapshot, rootRunId: string):
255
255
  };
256
256
  }
257
257
 
258
+ function sameIds(left: readonly string[], right: readonly string[]): boolean {
259
+ if (left.length !== right.length) return false;
260
+ for (let index = 0; index < left.length; index++) {
261
+ if (left[index] !== right[index]) return false;
262
+ }
263
+ return true;
264
+ }
265
+
266
+ interface RefreshRunSources {
267
+ readonly run: RunSnapshot;
268
+ readonly stages: ReadonlyMap<string, StageSnapshot>;
269
+ readonly tools: ReadonlyMap<string, ToolNodeSnapshot>;
270
+ }
271
+
272
+ function refreshRunSources(snapshot: StoreSnapshot): Map<string, RefreshRunSources> {
273
+ return new Map(
274
+ snapshot.runs.map((run) => [
275
+ run.id,
276
+ {
277
+ run,
278
+ stages: new Map(run.stages.map((stage) => [stage.id, stage])),
279
+ tools: new Map((run.toolNodes ?? []).map((tool) => [tool.id, tool])),
280
+ },
281
+ ]),
282
+ );
283
+ }
284
+
285
+ export function sameExpandedWorkflowTopology(left: StoreSnapshot, right: StoreSnapshot): boolean {
286
+ if (left.runs.length !== right.runs.length) return false;
287
+ for (let runIndex = 0; runIndex < left.runs.length; runIndex++) {
288
+ const a = left.runs[runIndex]!;
289
+ const b = right.runs[runIndex]!;
290
+ if (
291
+ a.id !== b.id ||
292
+ a.parentRunId !== b.parentRunId ||
293
+ a.parentStageId !== b.parentStageId ||
294
+ a.rootRunId !== b.rootRunId ||
295
+ a.stages.length !== b.stages.length ||
296
+ (a.toolNodes?.length ?? 0) !== (b.toolNodes?.length ?? 0)
297
+ )
298
+ return false;
299
+ for (let stageIndex = 0; stageIndex < a.stages.length; stageIndex++) {
300
+ const x = a.stages[stageIndex]!;
301
+ const y = b.stages[stageIndex]!;
302
+ const xChildRunId = authoritativeWorkflowChildRunId(x);
303
+ const yChildRunId = authoritativeWorkflowChildRunId(y);
304
+ if (
305
+ x.id !== y.id ||
306
+ x.executionOrder !== y.executionOrder ||
307
+ x.nodeKind !== y.nodeKind ||
308
+ // The authoritative id becomes undefined at failed/skipped boundaries,
309
+ // so this comparison also catches a terminal-boundary flip.
310
+ xChildRunId !== yChildRunId ||
311
+ !sameIds(x.parentIds, y.parentIds)
312
+ )
313
+ return false;
314
+ }
315
+ const aTools = a.toolNodes ?? [];
316
+ const bTools = b.toolNodes ?? [];
317
+ for (let toolIndex = 0; toolIndex < aTools.length; toolIndex++) {
318
+ const x = aTools[toolIndex]!;
319
+ const y = bTools[toolIndex]!;
320
+ if (x.id !== y.id || x.executionOrder !== y.executionOrder || !sameIds(x.parentIds, y.parentIds)) return false;
321
+ }
322
+ }
323
+ return true;
324
+ }
325
+
326
+ export function refreshExpandedWorkflowGraph(
327
+ graph: ExpandedWorkflowGraph,
328
+ snapshot: StoreSnapshot,
329
+ ): ExpandedWorkflowGraph | undefined {
330
+ const sourcesByRunId = refreshRunSources(snapshot);
331
+ const refreshedStages = new Map<string, ExpandedWorkflowStage>();
332
+ const refreshedTools = new Map<string, ExpandedWorkflowTool>();
333
+ const renderStages: ExpandedWorkflowStage[] = [];
334
+
335
+ for (const cached of graph.renderStages) {
336
+ const target = graph.targets.get(cached.id);
337
+ const sources = target ? sourcesByRunId.get(target.runId) : undefined;
338
+ if (!target || !sources) return undefined;
339
+ if (cached.nodeKind === "tool") {
340
+ const source = sources.tools.get(target.stageId);
341
+ if (!source) return undefined;
342
+ const stage: ExpandedWorkflowStage = {
343
+ id: cached.id,
344
+ name: source.name,
345
+ status: projectedToolStatus(source),
346
+ parentIds: cached.parentIds,
347
+ executionOrder: source.executionOrder,
348
+ nodeKind: "tool",
349
+ toolStatus: source.status,
350
+ startedAt: source.startedAt,
351
+ endedAt: source.endedAt,
352
+ result: source.resultSummary,
353
+ error: source.error,
354
+ toolEvents: [],
355
+ attachable: false,
356
+ workflowGraphTarget: target,
357
+ };
358
+ renderStages.push(stage);
359
+ refreshedStages.set(stage.id, stage);
360
+ continue;
361
+ }
362
+ const source = sources.stages.get(target.stageId);
363
+ if (!source) return undefined;
364
+ const stage: ExpandedWorkflowStage = {
365
+ ...source,
366
+ id: cached.id,
367
+ parentIds: cached.parentIds,
368
+ workflowGraphTarget: target,
369
+ };
370
+ renderStages.push(stage);
371
+ refreshedStages.set(stage.id, stage);
372
+ }
373
+
374
+ const stages = renderStages.filter((stage) => stage.nodeKind !== "tool");
375
+ const tools: ExpandedWorkflowTool[] = [];
376
+
377
+ for (const cached of graph.tools) {
378
+ const target = graph.targets.get(cached.id);
379
+ const sources = target ? sourcesByRunId.get(target.runId) : undefined;
380
+ const source = target ? sources?.tools.get(target.stageId) : undefined;
381
+ if (!target || !sources || !source) return undefined;
382
+ const tool: ExpandedWorkflowTool = {
383
+ ...source,
384
+ id: cached.id,
385
+ parentIds: cached.parentIds,
386
+ runId: sources.run.id,
387
+ runName: sources.run.name,
388
+ depth: target.depth,
389
+ };
390
+ tools.push(tool);
391
+ refreshedTools.set(tool.id, tool);
392
+ }
393
+
394
+ const nodes: ExpandedWorkflowNode[] = [];
395
+ for (const node of graph.nodes) {
396
+ if (node.kind === "stage") {
397
+ const stage = refreshedStages.get(node.stage.id);
398
+ if (!stage) return undefined;
399
+ nodes.push({ kind: "stage", stage });
400
+ continue;
401
+ }
402
+ const tool = refreshedTools.get(node.tool.id);
403
+ if (!tool) return undefined;
404
+ nodes.push({ kind: "tool", tool });
405
+ }
406
+ return { stages, tools, nodes, renderStages, targets: graph.targets };
407
+ }
408
+
258
409
  export function expandedStageTarget(
259
410
  graph: ExpandedWorkflowGraph,
260
411
  virtualStageIdValue: string,
@@ -262,24 +413,22 @@ export function expandedStageTarget(
262
413
  return graph.targets.get(virtualStageIdValue);
263
414
  }
264
415
 
416
+ /**
417
+ * Exact-match only. A stage id is never truncated to address a stage: at the
418
+ * root that id is a bare UUID, nested it is the `runId:nodeId` composite built
419
+ * by `virtualNodeId`, and a tool node carries `tool:<argsHash>`. Names match
420
+ * whole, so `build` never selects `build-check`.
421
+ */
265
422
  export function stageMatchesExpandedIdentifier(stage: ExpandedWorkflowStage, target: string): boolean {
266
423
  const graphTarget = stage.workflowGraphTarget;
267
424
  return (
268
- stage.id === target ||
269
- stage.name === target ||
270
- stage.id.startsWith(target) ||
271
- graphTarget.stageId === target ||
272
- graphTarget.stageId.startsWith(target) ||
273
- graphTarget.runId === target ||
274
- graphTarget.runId.startsWith(target)
425
+ stage.id === target || stage.name === target || graphTarget.stageId === target || graphTarget.runId === target
275
426
  );
276
427
  }
277
428
 
278
429
  export function expandedStageLabel(stage: ExpandedWorkflowStage): string {
279
430
  const target = stage.workflowGraphTarget;
280
431
  if (stage.nodeKind === "tool") return `${stage.name} (tool)`;
281
- const runPrefix = target.runId.slice(0, 8);
282
- const stagePrefix = target.stageId.slice(0, 8);
283
432
  const depthPrefix = target.depth > 0 ? `${childAliasFor(stage) ?? target.runName}:` : "";
284
- return `${depthPrefix}${stage.name} (${runPrefix}/${stagePrefix})`;
433
+ return `${depthPrefix}${stage.name} (${target.runId}/${target.stageId})`;
285
434
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Truncation that actually releases the untruncated source.
3
+ *
4
+ * V8 does not copy characters for `String.prototype.slice`. For a flat parent of
5
+ * 13 characters or more it allocates a **SlicedString**, which is a length, an
6
+ * offset, and a pointer to the parent. Template interpolation then wraps that in
7
+ * a **ConsString**, which keeps the same pointer. So the obvious way to bound a
8
+ * value for storage:
9
+ *
10
+ * ```ts
11
+ * `${JSON.stringify(value).slice(0, 237)}...`
12
+ * ```
13
+ *
14
+ * produces a 240-character string that reports `length === 240` while holding
15
+ * the entire serialized payload alive. The bound is on what you can read, not on
16
+ * what you retain. An 8 MiB tool result summarized to 240 characters still costs
17
+ * 8 MiB for as long as anything keeps the summary.
18
+ *
19
+ * That is invisible in every ordinary test: the value is correct, its length is
20
+ * correct, and equality holds. It only shows up as heap that never comes back.
21
+ *
22
+ * `split("").join("")` copies the code units into a fresh flat string with no
23
+ * parent pointer. It is code-unit exact, including lone surrogates, because
24
+ * `split("")` splits on UTF-16 code units rather than code points.
25
+ *
26
+ * Use this at any truncation whose result outlives the value it came from —
27
+ * memoized projections, durable checkpoints, catalogs, persisted summaries.
28
+ * A truncation that is consumed and dropped within the same turn does not need
29
+ * it, and paying for a copy there is waste.
30
+ */
31
+ export function flattenTruncatedString(value: string): string {
32
+ return value.split("").join("");
33
+ }