@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
@@ -20,9 +20,14 @@ import {
20
20
  validStageGroup,
21
21
  workflowChildFromDraft,
22
22
  } from "./completed-catalog-stage-groups.js";
23
+ import { isDurableWorkflowResumable } from "./resume-eligibility.js";
23
24
  import { resolveDurableEntry } from "./resume-runtime.js";
24
25
  import { priorRunElapsedMs, RUN_TIMING_CHECKPOINT_NAME } from "./run-timing.js";
25
- import { groupByDurableStageKey, immutableStageGroupError } from "./stage-topology-validation.js";
26
+ import {
27
+ directChildTopologyError,
28
+ groupByDurableStageKey,
29
+ immutableStageGroupError,
30
+ } from "./stage-topology-validation.js";
26
31
  import { workflowToolOutcomeFromValue } from "./tool-outcome.js";
27
32
  import {
28
33
  DURABLE_STAGE_TOPOLOGY_VERSION,
@@ -94,9 +99,13 @@ export function completedWorkflowRunSnapshots(
94
99
  if (handle === undefined || (handle.status !== "completed" && handle.status !== "failed")) return [];
95
100
  const checkpoints = backend.listCheckpoints(entry.workflowId);
96
101
  if (checkpoints.length === 0) return [];
97
- const runs = runSnapshotsFromCheckpoints(checkpoints, handle.workflowId, handle.name, handle.updatedAt).map(
98
- (run) => ({ ...run, stages: run.stages.map(validatedStageTranscript) }),
99
- );
102
+ const runs = runSnapshotsFromCheckpoints(
103
+ checkpoints,
104
+ handle.workflowId,
105
+ handle.name,
106
+ handle.updatedAt,
107
+ COMPLETED_RECONSTRUCTION,
108
+ ).map((run) => ({ ...run, stages: run.stages.map(validatedStageTranscript) }));
100
109
  const rootIndex = runs.findIndex((run) => run.id === handle.workflowId);
101
110
  if (rootIndex < 0) return [];
102
111
  const rootDuration =
@@ -121,6 +130,59 @@ export function completedWorkflowRunSnapshots(
121
130
  return [root, ...runs.filter((_, index) => index !== rootIndex)];
122
131
  }
123
132
 
133
+ /** Rebuild one retained root, including in-progress nested runs, without relaxing topology validation. */
134
+ export function durableWorkflowRunSnapshots(
135
+ backend: DurableWorkflowBackend,
136
+ handle: import("./types.js").DurableWorkflowHandle,
137
+ ): readonly RunSnapshot[] {
138
+ const checkpoints = backend.listCheckpoints(handle.workflowId);
139
+ if (checkpoints.length === 0) {
140
+ return [rootSnapshot(backend, handle, { stages: [], toolNodes: [] })];
141
+ }
142
+ const requiresTerminalTopology = handle.status === "completed" || handle.status === "failed";
143
+ const runs = runSnapshotsFromCheckpoints(
144
+ checkpoints,
145
+ handle.workflowId,
146
+ handle.name,
147
+ handle.updatedAt,
148
+ requiresTerminalTopology ? COMPLETED_RECONSTRUCTION : ACTIVE_INSPECTION_RECONSTRUCTION,
149
+ );
150
+ const rootIndex = runs.findIndex((run) => run.id === handle.workflowId);
151
+ if (rootIndex < 0) return [];
152
+ return [rootSnapshot(backend, handle, runs[rootIndex]!), ...runs.filter((_, index) => index !== rootIndex)];
153
+ }
154
+
155
+ function rootSnapshot(
156
+ backend: DurableWorkflowBackend,
157
+ handle: import("./types.js").DurableWorkflowHandle,
158
+ graph: Pick<RunSnapshot, "stages" | "toolNodes">,
159
+ ): RunSnapshot {
160
+ const terminal = handle.status === "completed" || handle.status === "failed" || handle.status === "cancelled";
161
+ const priorDurationMs = priorRunElapsedMs(backend, handle.workflowId);
162
+ const terminalDurationMs = priorDurationMs ?? Math.max(0, handle.updatedAt - handle.createdAt);
163
+ return {
164
+ id: handle.workflowId,
165
+ name: handle.name,
166
+ inputs: { ...handle.inputs } as WorkflowInputValues,
167
+ status: handle.status,
168
+ stages: graph.stages,
169
+ toolNodes: graph.toolNodes,
170
+ startedAt: handle.createdAt,
171
+ ...(terminal
172
+ ? { endedAt: handle.updatedAt, durationMs: terminalDurationMs }
173
+ : priorDurationMs === undefined
174
+ ? {}
175
+ : { accumulatedDurationMs: priorDurationMs }),
176
+ ...(handle.error !== undefined ? { error: handle.error } : {}),
177
+ ...(handle.failureKind !== undefined ? { failureKind: handle.failureKind } : {}),
178
+ ...(handle.failureCode !== undefined ? { failureCode: handle.failureCode } : {}),
179
+ ...(handle.failureRecoverability !== undefined ? { failureRecoverability: handle.failureRecoverability } : {}),
180
+ ...(handle.failureDisposition !== undefined ? { failureDisposition: handle.failureDisposition } : {}),
181
+ ...(handle.failedToolNodeId !== undefined ? { failedToolNodeId: handle.failedToolNodeId } : {}),
182
+ resumable: isDurableWorkflowResumable(handle),
183
+ };
184
+ }
185
+
124
186
  /** Rebuild completed nested runs while a paused root is replaying cached boundaries. */
125
187
  export function durableNestedRunSnapshots(
126
188
  backend: DurableWorkflowBackend,
@@ -133,7 +195,7 @@ export function durableNestedRunSnapshots(
133
195
  rootWorkflowId,
134
196
  handle.name,
135
197
  handle.updatedAt,
136
- false,
198
+ LEGACY_ACTIVE_RECONSTRUCTION,
137
199
  ).filter((run) => run.id !== rootWorkflowId);
138
200
  }
139
201
 
@@ -224,14 +286,44 @@ function summarizeCompletedToolResult(value: WorkflowSerializableValue): string
224
286
  return serialized.length <= 240 ? serialized : `${flattenTruncatedString(serialized.slice(0, 237))}...`;
225
287
  }
226
288
 
289
+ interface ReconstructionPolicy {
290
+ readonly requireTerminal: boolean;
291
+ readonly failClosed: boolean;
292
+ readonly retainActive: boolean;
293
+ readonly failOnUnreachable: boolean;
294
+ readonly requireActiveBoundaryFingerprint: boolean;
295
+ }
296
+
297
+ const COMPLETED_RECONSTRUCTION: ReconstructionPolicy = {
298
+ requireTerminal: true,
299
+ failClosed: true,
300
+ retainActive: false,
301
+ failOnUnreachable: false,
302
+ requireActiveBoundaryFingerprint: false,
303
+ };
304
+ const ACTIVE_INSPECTION_RECONSTRUCTION: ReconstructionPolicy = {
305
+ requireTerminal: false,
306
+ failClosed: true,
307
+ retainActive: true,
308
+ failOnUnreachable: true,
309
+ requireActiveBoundaryFingerprint: true,
310
+ };
311
+ const LEGACY_ACTIVE_RECONSTRUCTION: ReconstructionPolicy = {
312
+ requireTerminal: false,
313
+ failClosed: false,
314
+ retainActive: true,
315
+ failOnUnreachable: false,
316
+ requireActiveBoundaryFingerprint: false,
317
+ };
318
+
227
319
  function runSnapshotsFromCheckpoints(
228
320
  checkpoints: readonly DurableCheckpoint[],
229
321
  rootRunId: string,
230
322
  rootRunName: string,
231
323
  fallbackCompletedAt: number,
232
- strict = true,
324
+ policy: ReconstructionPolicy = COMPLETED_RECONSTRUCTION,
233
325
  ): RunSnapshot[] {
234
- if (!validBoundaryRecordSet(checkpoints, strict)) return [];
326
+ if (!validBoundaryRecordSet(checkpoints, policy.requireTerminal, policy.requireActiveBoundaryFingerprint)) return [];
235
327
  const stageRecords = checkpoints.filter(
236
328
  (checkpoint): checkpoint is DurableStageCheckpoint => checkpoint.kind === "stage",
237
329
  );
@@ -248,7 +340,10 @@ function runSnapshotsFromCheckpoints(
248
340
  .map((records) => mergeStageGroup(records, stageRecords, checkpoints))
249
341
  .filter(
250
342
  (draft) =>
251
- !strict || draft.topology?.run === undefined || draft.endedAt !== undefined || draft.output !== undefined,
343
+ policy.retainActive ||
344
+ draft.topology?.run === undefined ||
345
+ draft.endedAt !== undefined ||
346
+ draft.output !== undefined,
252
347
  )
253
348
  .map(
254
349
  (draft): StageDraft =>
@@ -267,14 +362,46 @@ function runSnapshotsFromCheckpoints(
267
362
  .sort(compareDraftSourceOrder);
268
363
  const drafts = checkpointDrafts(checkpoints);
269
364
  const toolCheckpoints = drafts.tools;
365
+ if (policy.failClosed) {
366
+ for (const draft of candidates) {
367
+ const boundary = draft.topology?.boundary;
368
+ if (boundary === undefined || childRunIdFromDraft(draft) === undefined) continue;
369
+ if (boundary.replayScope !== draft.replayKey) return [];
370
+ const expectedChildRun = {
371
+ runId: boundary.child.runId,
372
+ runName: boundary.child.runName,
373
+ parentRunId: boundary.child.parentRunId,
374
+ parentStageId: boundary.child.parentStageId,
375
+ rootRunId: boundary.child.rootRunId,
376
+ };
377
+ const childToolIds = new Set(
378
+ toolCheckpoints.flatMap((checkpoint) =>
379
+ checkpoint.topology?.run?.runId === expectedChildRun.runId ? [checkpoint.topology.nodeId] : [],
380
+ ),
381
+ );
382
+ if (
383
+ directChildTopologyError(stageRecords, boundary.replayScope, expectedChildRun, true, childToolIds) !==
384
+ undefined
385
+ )
386
+ return [];
387
+ }
388
+ }
270
389
  if (candidates.length === 0 && toolCheckpoints.length === 0) {
271
- return strict ? [] : [syntheticRun(rootRunId, rootRunName, checkpoints.length, fallbackCompletedAt)];
390
+ const supportedNonGraphState = checkpoints.every(
391
+ (checkpoint) =>
392
+ checkpoint.kind === "ui" ||
393
+ (checkpoint.kind === "tool" && checkpoint.argsHash === RUN_TIMING_CHECKPOINT_NAME),
394
+ );
395
+ if (policy.failClosed && supportedNonGraphState) {
396
+ return [emptyGraphRun(rootRunId, rootRunName, fallbackCompletedAt)];
397
+ }
398
+ return policy.failClosed ? [] : [syntheticRun(rootRunId, rootRunName, checkpoints.length, fallbackCompletedAt)];
272
399
  }
273
400
 
274
401
  const grouped = new Map<string, StageDraft[]>();
275
402
  for (const draft of candidates) {
276
403
  if (draft.topology?.version !== DURABLE_STAGE_TOPOLOGY_VERSION) {
277
- if (strict) return [];
404
+ if (policy.failClosed) return [];
278
405
  continue;
279
406
  }
280
407
  const runId = draft.topology.run?.runId ?? rootRunId;
@@ -287,7 +414,9 @@ function runSnapshotsFromCheckpoints(
287
414
  if (!grouped.has(runId)) grouped.set(runId, []);
288
415
  }
289
416
  for (const group of grouped.values()) group.sort(compareDraftSourceOrder);
417
+ const groupedCount = grouped.size;
290
418
  retainReachableRunGroups(grouped, rootRunId);
419
+ if (policy.failOnUnreachable && grouped.size !== groupedCount) return [];
291
420
  if (!validateRunGroups(grouped, rootRunId, toolCheckpoints, checkpoints)) return [];
292
421
  const syntheticChildren: Array<{
293
422
  readonly parentRunId: string;
@@ -302,7 +431,7 @@ function runSnapshotsFromCheckpoints(
302
431
  !isSyntheticExitedChild(draft, parentRunId, child.runId, rootRunId) ||
303
432
  childScopedEvidenceExists(checkpoints, draft.replayKey, child.runId)
304
433
  ) {
305
- if (strict) return [];
434
+ if (policy.failClosed) return [];
306
435
  continue;
307
436
  }
308
437
  syntheticChildren.push({ parentRunId, owner: draft, child });
@@ -311,14 +440,10 @@ function runSnapshotsFromCheckpoints(
311
440
 
312
441
  const idMaps = new Map<string, Map<string, string>>();
313
442
  for (const [runId, runDrafts] of grouped) {
314
- const ownedToolIds = new Set(
315
- toolCheckpoints.flatMap((checkpoint) =>
316
- (checkpoint.topology?.run?.runId ?? rootRunId) === runId
317
- ? [checkpoint.topology?.nodeId ?? checkpoint.checkpointId]
318
- : [],
319
- ),
443
+ const ownedTools = toolCheckpoints.filter(
444
+ (checkpoint) => (checkpoint.topology?.run?.runId ?? rootRunId) === runId,
320
445
  );
321
- if (!validStageGroup(runDrafts, runId, ownedToolIds)) return [];
446
+ if (!validStageGroup(runDrafts, runId, rootRunId, ownedTools)) return [];
322
447
  const ids = new Map<string, string>();
323
448
  let hasIdentityConflict = false;
324
449
  runDrafts.forEach((draft) => {
@@ -331,7 +456,7 @@ function runSnapshotsFromCheckpoints(
331
456
  }
332
457
  });
333
458
  if (hasIdentityConflict) {
334
- if (strict) return [];
459
+ if (policy.failClosed) return [];
335
460
  grouped.delete(runId);
336
461
  continue;
337
462
  }
@@ -356,7 +481,7 @@ function runSnapshotsFromCheckpoints(
356
481
  (checkpoint) => checkpoint.topology?.parentIds.some((parentId) => !ids.has(parentId)) === true,
357
482
  );
358
483
  if (hasUnknownParents) {
359
- if (strict) return [];
484
+ if (policy.failClosed) return [];
360
485
  continue;
361
486
  }
362
487
  const stages = runDrafts.map((draft) =>
@@ -377,6 +502,8 @@ function runSnapshotsFromCheckpoints(
377
502
  ...toolNodes.map((tool) => tool.endedAt ?? fallbackCompletedAt),
378
503
  );
379
504
  const owner = runId === rootRunId ? undefined : boundaryOwner(grouped, runId);
505
+ const status = owner === undefined ? "completed" : childRunStatus(owner);
506
+ const terminal = status !== "running" && status !== "pending" && status !== "paused" && status !== "blocked";
380
507
  const parentRunId = run?.parentRunId ?? rootRunId;
381
508
  const boundarySourceId = grouped.get(parentRunId)?.find((draft) => childRunIdFromDraft(draft) === runId)
382
509
  ?.topology?.stageId;
@@ -390,12 +517,11 @@ function runSnapshotsFromCheckpoints(
390
517
  id: runId,
391
518
  name: run?.runName ?? rootRunName,
392
519
  inputs: {},
393
- status: owner === undefined ? "completed" : childRunStatus(owner),
520
+ status,
394
521
  stages,
395
522
  toolNodes,
396
523
  startedAt,
397
- endedAt,
398
- durationMs: Math.max(0, endedAt - startedAt),
524
+ ...(terminal ? { endedAt, durationMs: Math.max(0, endedAt - startedAt) } : {}),
399
525
  ...(run?.parentRunId !== undefined ? { parentRunId: run.parentRunId } : {}),
400
526
  ...(parentStageId !== undefined ? { parentStageId } : {}),
401
527
  ...(run?.rootRunId !== undefined ? { rootRunId: run.rootRunId } : {}),
@@ -412,14 +538,14 @@ function runSnapshotsFromCheckpoints(
412
538
  }
413
539
  for (const { parentRunId, owner, child } of syntheticChildren) {
414
540
  if (!emittedRunIds.has(parentRunId)) {
415
- if (strict) return [];
541
+ if (policy.failClosed) return [];
416
542
  continue;
417
543
  }
418
544
  const topology = owner.topology!;
419
545
  const boundary = topology.boundary!;
420
546
  const parentStageId = idMaps.get(parentRunId)?.get(topology.stageId);
421
547
  if (parentStageId === undefined) {
422
- if (strict) return [];
548
+ if (policy.failClosed) return [];
423
549
  continue;
424
550
  }
425
551
  const startedAt = owner.startedAt ?? owner.firstCompletedAt;
@@ -490,6 +616,21 @@ function stageResult(draft: StageDraft): string | undefined {
490
616
  return typeof draft.output === "string" ? draft.output : JSON.stringify(draft.output);
491
617
  }
492
618
 
619
+ function emptyGraphRun(runId: string, runName: string, completedAt: number): RunSnapshot {
620
+ return {
621
+ id: runId,
622
+ name: runName,
623
+ inputs: {},
624
+ status: "completed",
625
+ stages: [],
626
+ toolNodes: [],
627
+ startedAt: completedAt,
628
+ endedAt: completedAt,
629
+ durationMs: 0,
630
+ resumable: false,
631
+ };
632
+ }
633
+
493
634
  function syntheticRun(runId: string, runName: string, checkpointCount: number, completedAt: number): RunSnapshot {
494
635
  return {
495
636
  id: runId,
@@ -5,6 +5,7 @@ import {
5
5
  type DurableInactiveDeleteResult,
6
6
  type DurableWorkflowBackend,
7
7
  type DurableWorkflowCatalogEntries,
8
+ type DurableWorkflowHydrationResult,
8
9
  InMemoryDurableBackend,
9
10
  type WorkflowRegistrationInput,
10
11
  } from "./backend.js";
@@ -439,15 +440,21 @@ export class DbosDurableBackend implements DurableWorkflowBackend {
439
440
  }
440
441
 
441
442
  async hydrateWorkflow(workflowId: string): Promise<void> {
442
- if (this.locallyRegistered.has(workflowId)) return;
443
- const info = await this.sdk.retrieveWorkflow(workflowId);
444
- if (info !== undefined) {
445
- await this.hydrateInfo(info);
446
- return;
443
+ await this.hydrateWorkflowForInspection(workflowId);
444
+ }
445
+ async hydrateWorkflowForInspection(workflowId: string): Promise<DurableWorkflowHydrationResult> {
446
+ if (this.locallyRegistered.has(workflowId)) {
447
+ const handle = this.getLoadableWorkflow(workflowId);
448
+ return handle === undefined ? { kind: "malformed" } : { kind: "current", handle };
447
449
  }
450
+ const info = await this.sdk.retrieveWorkflow(workflowId);
451
+ if (info !== undefined) return await this.hydrateInfo(info);
448
452
  const records = await this.sdk.listStepRecords(workflowId);
449
453
  const deletion = classifyDbosDeletionTombstone(records, workflowId);
450
- if (deletion !== "absent") await this.suppressWorkflow(workflowId);
454
+ if (deletion === "absent" && records.length === 0) return { kind: "absent" };
455
+ await this.suppressWorkflow(workflowId);
456
+ if (deletion === "absent") return { kind: "malformed" };
457
+ return { kind: deletion === "current" ? "deleted" : "malformed" };
451
458
  }
452
459
  async hydrateResumableWorkflows(): Promise<void> {
453
460
  const all = await this.sdk.listAllWorkflows();
@@ -457,12 +464,17 @@ export class DbosDurableBackend implements DurableWorkflowBackend {
457
464
  }
458
465
  }
459
466
 
460
- private async hydrateInfo(info: DbosWorkflowInfo): Promise<void> {
467
+ private async hydrateInfo(info: DbosWorkflowInfo): Promise<DurableWorkflowHydrationResult> {
461
468
  const records = await this.sdk.listStepRecords(info.workflowId);
469
+ const deletion = classifyDbosDeletionTombstone(records, info.workflowId);
470
+ if (deletion !== "absent") {
471
+ await this.suppressWorkflow(info.workflowId);
472
+ return { kind: deletion === "current" ? "deleted" : "malformed" };
473
+ }
462
474
  const metadata = classifyLatestMetadata(records, info.workflowId);
463
475
  if (metadata.kind !== "current") {
464
476
  await this.suppressWorkflow(info.workflowId);
465
- return;
477
+ return { kind: "malformed" };
466
478
  }
467
479
  const checkpoints: DurableCheckpoint[] = [];
468
480
  for (const record of records) {
@@ -475,7 +487,7 @@ export class DbosDurableBackend implements DurableWorkflowBackend {
475
487
  const classified = classifyCheckpointPayload(info.workflowId, record.stepName, record.output);
476
488
  if (classified.kind === "unknown") {
477
489
  await this.suppressWorkflow(info.workflowId);
478
- return;
490
+ return { kind: "malformed" };
479
491
  }
480
492
  checkpoints.push(classified.checkpoint);
481
493
  }
@@ -485,22 +497,28 @@ export class DbosDurableBackend implements DurableWorkflowBackend {
485
497
  this.applyMetadata(info.workflowId, metadata.metadata);
486
498
  for (const checkpoint of checkpoints) this.mem.recordCheckpoint(checkpoint);
487
499
  const current = this.mem.getWorkflow(info.workflowId);
488
- if (current !== undefined) {
489
- const pendingPrompts = this.promptReservations.hydrate(
490
- info.workflowId,
491
- metadata.metadata.pendingPrompts,
492
- records,
493
- metadata.metadata.promptReservationEpoch,
494
- );
495
- // Re-register instead of setWorkflowStatus: hydration is a read-side
496
- // reconstruction and must preserve the authoritative updatedAt, which
497
- // doubles as the cross-session liveness heartbeat for running handles.
498
- this.applyMetadata(info.workflowId, {
499
- ...metadata.metadata,
500
- pendingPrompts,
501
- completedCheckpoints: current.completedCheckpoints,
502
- });
500
+ if (current === undefined) {
501
+ await this.suppressWorkflow(info.workflowId);
502
+ return { kind: "malformed" };
503
503
  }
504
+ const pendingPrompts = this.promptReservations.hydrate(
505
+ info.workflowId,
506
+ metadata.metadata.pendingPrompts,
507
+ records,
508
+ metadata.metadata.promptReservationEpoch,
509
+ );
510
+ // Re-register instead of setWorkflowStatus: hydration is a read-side
511
+ // reconstruction and must preserve the authoritative updatedAt, which
512
+ // doubles as the cross-session liveness heartbeat for running handles.
513
+ this.applyMetadata(info.workflowId, {
514
+ ...metadata.metadata,
515
+ pendingPrompts,
516
+ completedCheckpoints: current.completedCheckpoints,
517
+ });
518
+ const handle = this.getLoadableWorkflow(info.workflowId);
519
+ if (handle !== undefined) return { kind: "current", handle };
520
+ await this.suppressWorkflow(info.workflowId);
521
+ return { kind: "malformed" };
504
522
  }
505
523
 
506
524
  private async suppressWorkflow(workflowId: string): Promise<void> {
@@ -477,7 +477,8 @@ function isModelUsage(value: WorkflowSerializableValue): boolean {
477
477
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
478
478
  const usage = value as Record<string, WorkflowSerializableValue>;
479
479
  return ["input", "output", "cacheRead", "cacheWrite", "cost", "turns"].every(
480
- (key) => usage[key] === undefined || (typeof usage[key] === "number" && Number.isFinite(usage[key])),
480
+ (key) =>
481
+ usage[key] === undefined || (typeof usage[key] === "number" && Number.isFinite(usage[key]) && usage[key] >= 0),
481
482
  );
482
483
  }
483
484
 
@@ -56,16 +56,39 @@ export function recordRunTimingCheckpoint(
56
56
  run: RunSnapshot,
57
57
  options?: { readonly debounce?: boolean; readonly now?: number },
58
58
  ): boolean {
59
+ const checkpoint = runTimingCheckpoint(backend, run, options);
60
+ if (checkpoint === undefined) return false;
61
+ backend.recordCheckpoint(checkpoint);
62
+ return true;
63
+ }
64
+
65
+ /** Await the timing write so an active turn observes persistent storage faults at once. */
66
+ export async function recordRunTimingCheckpointAsync(
67
+ backend: DurableWorkflowBackend,
68
+ run: RunSnapshot,
69
+ options?: { readonly debounce?: boolean; readonly now?: number },
70
+ ): Promise<boolean> {
71
+ const checkpoint = runTimingCheckpoint(backend, run, options);
72
+ if (checkpoint === undefined) return false;
73
+ await backend.recordCheckpointAsync(checkpoint);
74
+ return true;
75
+ }
76
+
77
+ function runTimingCheckpoint(
78
+ backend: DurableWorkflowBackend,
79
+ run: RunSnapshot,
80
+ options?: { readonly debounce?: boolean; readonly now?: number },
81
+ ): DurableToolCheckpoint | undefined {
59
82
  const now = options?.now ?? Date.now();
60
83
  const elapsedMs = elapsedRunMs(run, now);
61
- if (elapsedMs <= 0) return false;
62
- if (backend.listCheckpoints(run.id).length === 0) return false;
84
+ if (elapsedMs <= 0) return undefined;
85
+ if (backend.listCheckpoints(run.id).length === 0) return undefined;
63
86
  const recorded = priorRunElapsedMs(backend, run.id);
64
87
  if (recorded !== undefined) {
65
- if (elapsedMs <= recorded) return false;
66
- if (options?.debounce === true && timingBucket(elapsedMs) === timingBucket(recorded)) return false;
88
+ if (elapsedMs <= recorded) return undefined;
89
+ if (options?.debounce === true && timingBucket(elapsedMs) === timingBucket(recorded)) return undefined;
67
90
  }
68
- const checkpoint: DurableToolCheckpoint = {
91
+ return {
69
92
  kind: "tool",
70
93
  workflowId: run.id,
71
94
  checkpointId: `run-timing:${elapsedMs}`,
@@ -74,8 +97,6 @@ export function recordRunTimingCheckpoint(
74
97
  output: { elapsedMs },
75
98
  completedAt: now,
76
99
  };
77
- backend.recordCheckpoint(checkpoint);
78
- return true;
79
100
  }
80
101
 
81
102
  /**
@@ -24,6 +24,7 @@ import type {
24
24
  DurableInactiveDeleteResult,
25
25
  DurableWorkflowBackend,
26
26
  DurableWorkflowCatalogEntries,
27
+ DurableWorkflowHydrationResult,
27
28
  WorkflowRegistrationInput,
28
29
  } from "./backend.js";
29
30
  import {
@@ -210,6 +211,9 @@ export class ScopedDurableBackend implements DurableWorkflowBackend {
210
211
  hydrateWorkflow(_workflowId: string): Promise<void> {
211
212
  return Promise.resolve();
212
213
  }
214
+ hydrateWorkflowForInspection(_workflowId: string): Promise<DurableWorkflowHydrationResult> {
215
+ return Promise.resolve({ kind: "absent" });
216
+ }
213
217
 
214
218
  hydrateResumableWorkflows(): Promise<void> {
215
219
  return Promise.resolve();
@@ -0,0 +1,110 @@
1
+ import { inspectRun, type RunDetail } from "../runs/background/run-inspect.js";
2
+ import { createStore, type Store } from "../shared/store.js";
3
+ import type { RunSnapshot } from "../shared/store-types.js";
4
+ import type { DurableWorkflowBackend } from "./backend.js";
5
+ import { durableWorkflowRunSnapshots } from "./completed-catalog.js";
6
+ import { getAtomicExecutorId } from "./dbos-sdk-handle.js";
7
+ import { isForeignLiveWorkflow, isLiveRunningWorkflow } from "./resume-eligibility.js";
8
+
9
+ export type TargetedDurableInspection =
10
+ | {
11
+ readonly kind: "found";
12
+ readonly detail: RunDetail;
13
+ readonly runs: readonly RunSnapshot[];
14
+ readonly store: Store;
15
+ }
16
+ | { readonly kind: "absent" | "deleted" | "malformed"; readonly message: string };
17
+
18
+ /**
19
+ * Hydrate and reconstruct one exact durable root for inspection only.
20
+ *
21
+ * This path never claims ownership, changes durable status, starts execution,
22
+ * or restores snapshots into the current session store.
23
+ */
24
+ export async function inspectTargetedDurableWorkflow(
25
+ backend: DurableWorkflowBackend,
26
+ workflowId: string,
27
+ now: number = Date.now(),
28
+ ): Promise<TargetedDurableInspection> {
29
+ const hydrated = backend.hydrateWorkflowForInspection
30
+ ? await backend.hydrateWorkflowForInspection(workflowId)
31
+ : await hydrateWithoutClassification(backend, workflowId);
32
+ if (hydrated.kind !== "current") return hydrationFailure(workflowId, hydrated.kind);
33
+ const handle = hydrated.handle;
34
+ if (handle.rootWorkflowId !== undefined && handle.rootWorkflowId !== handle.workflowId) {
35
+ return malformed(workflowId, "the requested id belongs to a nested workflow rather than a durable root");
36
+ }
37
+ const reconstructed = durableWorkflowRunSnapshots(backend, handle);
38
+ if (reconstructed.length === 0)
39
+ return malformed(workflowId, "durable checkpoint topology is malformed or incomplete");
40
+ const runs = structuredClone(reconstructed);
41
+
42
+ const inspectionStore = createStore();
43
+ for (const run of runs) inspectionStore.recordRunStart(structuredClone(run));
44
+ const inspected = inspectRun(workflowId, { store: inspectionStore });
45
+ if (!inspected.ok) return malformed(workflowId, "the durable checkpoint graph has no reciprocal root");
46
+
47
+ const foreignLive = isForeignLiveWorkflow(handle, getAtomicExecutorId(), now);
48
+ const live = isLiveRunningWorkflow(handle, now);
49
+ const crashed = handle.status === "running" && !live;
50
+ const resumeGuidance = crashed
51
+ ? inspected.detail.resumable === true
52
+ ? `This workflow appears to have crashed and is resumable. Resume it explicitly with /workflow resume ${workflowId}.`
53
+ : "This workflow appears to have crashed, but its retained state is not resumable."
54
+ : foreignLive
55
+ ? "This workflow is actively running in another Atomic session. Inspect it here, but control it from its owner session."
56
+ : live
57
+ ? "This workflow still has a fresh durable heartbeat. Inspect it here without starting another executor."
58
+ : terminalGuidance(handle.status, workflowId, inspected.detail.resumable === true);
59
+ return {
60
+ kind: "found",
61
+ runs,
62
+ store: inspectionStore,
63
+ detail: {
64
+ ...inspected.detail,
65
+ ...(crashed ? { status: "crashed" as const } : {}),
66
+ ...(live ? { resumable: false } : {}),
67
+ ...(foreignLive ? { ownerActiveElsewhere: true } : {}),
68
+ resumeGuidance,
69
+ },
70
+ };
71
+ }
72
+
73
+ function terminalGuidance(status: string, workflowId: string, resumable: boolean): string {
74
+ if (resumable)
75
+ return `This retained workflow is ${status} and resumable. Resume it explicitly with /workflow resume ${workflowId}.`;
76
+ return `This retained workflow is ${status} and available for read-only inspection.`;
77
+ }
78
+
79
+ async function hydrateWithoutClassification(
80
+ backend: DurableWorkflowBackend,
81
+ workflowId: string,
82
+ ): Promise<import("./backend.js").DurableWorkflowHydrationResult> {
83
+ await backend.hydrateWorkflow(workflowId);
84
+ const handle = backend.getLoadableWorkflow(workflowId);
85
+ return handle === undefined ? { kind: "absent" } : { kind: "current", handle };
86
+ }
87
+
88
+ function hydrationFailure(
89
+ workflowId: string,
90
+ kind: Exclude<TargetedDurableInspection["kind"], "found">,
91
+ ): TargetedDurableInspection {
92
+ switch (kind) {
93
+ case "absent":
94
+ return {
95
+ kind,
96
+ message: `Run not found: ${workflowId} (not present in the current session or durable store).`,
97
+ };
98
+ case "deleted":
99
+ return { kind, message: `Durable workflow was deleted: ${workflowId}` };
100
+ case "malformed":
101
+ return malformed(workflowId, "current DBOS metadata or checkpoints are malformed");
102
+ }
103
+ }
104
+
105
+ function malformed(workflowId: string, reason: string): TargetedDurableInspection {
106
+ return {
107
+ kind: "malformed",
108
+ message: `Durable workflow ${workflowId} cannot be inspected safely: ${reason}.`,
109
+ };
110
+ }
@@ -1,4 +1,4 @@
1
- import { recordRunTimingCheckpoint } from "../durable/run-timing.js";
1
+ import { recordRunTimingCheckpointAsync } from "../durable/run-timing.js";
2
2
  import { type DurableStageDeps, recordStageSessionCheckpoint } from "../durable/stage-primitive.js";
3
3
  import type { StageSessionCheckpointOptions } from "../runs/foreground/executor-types.js";
4
4
  import type { RunSnapshot, StageSnapshot } from "../shared/store-types.js";
@@ -27,7 +27,7 @@ export function createDurableStageSessionRecorder(
27
27
  if (stageRunId === input.runId) {
28
28
  await recordStageSessionCheckpoint(input.deps, snapshot, { force: options?.forceDurable === true });
29
29
  if (input.runSnapshot !== undefined) {
30
- recordRunTimingCheckpoint(input.deps.backend, input.runSnapshot, {
30
+ await recordRunTimingCheckpointAsync(input.deps.backend, input.runSnapshot, {
31
31
  debounce: options?.forceDurable !== true,
32
32
  });
33
33
  }
@@ -180,8 +180,25 @@ export class EngineRuntime {
180
180
  return {
181
181
  apply: () => {
182
182
  if (!this.childRunOptions.mcp || !hasScope) return;
183
- if (depth === 0) this.childRunOptions.mcp.setScope(stageId, allow, deny);
184
- depth += 1;
183
+ if (depth > 0) {
184
+ depth += 1;
185
+ return;
186
+ }
187
+ depth = 1;
188
+ try {
189
+ this.childRunOptions.mcp.setScope(stageId, allow, deny);
190
+ } catch (error) {
191
+ depth = 0;
192
+ try {
193
+ this.childRunOptions.mcp.clearScope(stageId);
194
+ } catch (cleanupError) {
195
+ throw new AggregateError(
196
+ [error, cleanupError],
197
+ "MCP scope setup failed and compensating cleanup failed",
198
+ );
199
+ }
200
+ throw error;
201
+ }
185
202
  },
186
203
  clear: () => {
187
204
  if (!this.childRunOptions.mcp || !hasScope) return;
@@ -159,6 +159,9 @@ export function createWorkflowExtensionRuntimeState(
159
159
  resumeDurableWorkflow(workflowId, options) {
160
160
  return runtimeRef.current.resumeDurableWorkflow(workflowId, options);
161
161
  },
162
+ inspectDurableWorkflow(workflowId) {
163
+ return runtimeRef.current.inspectDurableWorkflow(workflowId);
164
+ },
162
165
  listDurableResumable() {
163
166
  return runtimeRef.current.listDurableResumable();
164
167
  },