@inflexa-ai/harness 0.46.2 → 0.47.0

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 (271) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +3 -8
  3. package/dist/agents/conversation-agent.js +24 -4
  4. package/dist/agents/report-session-agent.d.ts +11 -14
  5. package/dist/agents/report-session-agent.js +30 -17
  6. package/dist/agents/sandbox/shared.d.ts +18 -13
  7. package/dist/agents/sandbox/shared.js +42 -27
  8. package/dist/agents/sandbox/types.d.ts +1 -1
  9. package/dist/agents/sandbox-catalog.d.ts +2 -0
  10. package/dist/agents/sandbox-catalog.js +30 -1
  11. package/dist/app/synthesize-run.d.ts +10 -6
  12. package/dist/app/synthesize-run.js +4 -2
  13. package/dist/contracts/analogy-report.d.ts +46 -2
  14. package/dist/contracts/analogy-report.js +31 -25
  15. package/dist/contracts/chat-parts.d.ts +3 -14
  16. package/dist/contracts/report-reference.d.ts +2 -1
  17. package/dist/contracts/report-reference.js +2 -1
  18. package/dist/contracts/schemas/chat-parts.d.ts +0 -27
  19. package/dist/contracts/schemas/chat-parts.js +1 -3
  20. package/dist/execution/run-canceler.d.ts +2 -0
  21. package/dist/execution/run-canceler.js +9 -0
  22. package/dist/execution/run-synthesis.d.ts +36 -8
  23. package/dist/execution/run-synthesis.js +114 -32
  24. package/dist/index.d.ts +2 -5
  25. package/dist/index.js +3 -6
  26. package/dist/input-scan/enrich.d.ts +5 -10
  27. package/dist/input-scan/enrich.js +12 -8
  28. package/dist/lib/markdown-pdf.d.ts +27 -0
  29. package/dist/lib/markdown-pdf.js +236 -0
  30. package/dist/lib/metrics.d.ts +31 -0
  31. package/dist/lib/metrics.js +64 -0
  32. package/dist/lib/pdf-text.d.ts +23 -0
  33. package/dist/lib/pdf-text.js +34 -0
  34. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  35. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  36. package/dist/loop/run-loop.d.ts +5 -0
  37. package/dist/loop/run-loop.js +30 -2
  38. package/dist/loop/tool-name-repair.d.ts +11 -0
  39. package/dist/loop/tool-name-repair.js +23 -0
  40. package/dist/loop/types.d.ts +14 -0
  41. package/dist/memory/card-builders.d.ts +1 -1
  42. package/dist/prompts/SOUL.d.ts +6 -6
  43. package/dist/prompts/SOUL.js +22 -121
  44. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  45. package/dist/prompts/analogical-reasoner.js +35 -101
  46. package/dist/prompts/briefing.d.ts +20 -11
  47. package/dist/prompts/briefing.js +36 -19
  48. package/dist/prompts/conversation.d.ts +1 -1
  49. package/dist/prompts/conversation.js +103 -383
  50. package/dist/prompts/planner.d.ts +1 -5
  51. package/dist/prompts/planner.js +45 -128
  52. package/dist/prompts/report-session.d.ts +1 -1
  53. package/dist/prompts/report-session.js +26 -57
  54. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  55. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  56. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  58. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  59. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  60. package/dist/prompts/sandbox/data-profiler.js +11 -34
  61. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  62. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  63. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  65. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  67. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  69. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  71. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  73. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  75. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  77. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  79. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/network-agent.js +3 -1
  81. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  83. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  85. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  86. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  87. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  88. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  89. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  91. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  93. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  95. package/dist/prompts/sandbox-standards.d.ts +15 -18
  96. package/dist/prompts/sandbox-standards.js +96 -305
  97. package/dist/prompts/synthesis-agent.d.ts +1 -1
  98. package/dist/prompts/synthesis-agent.js +24 -90
  99. package/dist/provenance/collector.d.ts +2 -5
  100. package/dist/provenance/collector.js +2 -5
  101. package/dist/provenance/seam.d.ts +36 -1
  102. package/dist/provenance/workspace-reads.d.ts +36 -0
  103. package/dist/provenance/workspace-reads.js +33 -0
  104. package/dist/report-model/production-resolver.d.ts +22 -52
  105. package/dist/report-model/production-resolver.js +43 -112
  106. package/dist/report-render/chart-export.d.ts +9 -0
  107. package/dist/report-render/chart-export.js +1 -1
  108. package/dist/report-render/views/chart-view.d.ts +1 -0
  109. package/dist/report-render/views/chart-view.js +1 -0
  110. package/dist/report-render/views/lineage.d.ts +1 -0
  111. package/dist/report-render/views/page-view.d.ts +1 -0
  112. package/dist/report-render/views/page-view.js +1 -0
  113. package/dist/report-render/views/prose.d.ts +1 -0
  114. package/dist/report-render/views/prose.js +1 -0
  115. package/dist/report-render/views/references-view.d.ts +1 -0
  116. package/dist/report-render/views/values.d.ts +1 -0
  117. package/dist/report-render/views/values.js +1 -0
  118. package/dist/runtime/assemble.d.ts +20 -9
  119. package/dist/runtime/assemble.js +47 -38
  120. package/dist/sandbox/client.d.ts +5 -3
  121. package/dist/sandbox/create-sandbox.d.ts +7 -12
  122. package/dist/sandbox/create-sandbox.js +15 -37
  123. package/dist/sandbox/docker-client.d.ts +1 -3
  124. package/dist/sandbox/docker-client.js +3 -5
  125. package/dist/sandbox/exec-outcome.d.ts +11 -11
  126. package/dist/sandbox/exec-outcome.js +22 -22
  127. package/dist/sandbox/exec.d.ts +5 -0
  128. package/dist/sandbox/exec.js +9 -1
  129. package/dist/sandbox/identity.d.ts +4 -16
  130. package/dist/sandbox/identity.js +5 -21
  131. package/dist/sandbox/k8s-client.d.ts +0 -1
  132. package/dist/sandbox/k8s-client.js +8 -13
  133. package/dist/sandbox/lease-names.d.ts +6 -0
  134. package/dist/sandbox/lease-names.js +12 -0
  135. package/dist/sandbox/leases.d.ts +105 -0
  136. package/dist/sandbox/leases.js +519 -0
  137. package/dist/sandbox/liveness.d.ts +5 -2
  138. package/dist/sandbox/mount-plan.d.ts +10 -1
  139. package/dist/sandbox/mount-plan.js +7 -1
  140. package/dist/sandbox/reconciler.d.ts +58 -0
  141. package/dist/sandbox/reconciler.js +153 -0
  142. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  143. package/dist/sandbox/types.d.ts +25 -18
  144. package/dist/sandbox/types.js +10 -4
  145. package/dist/schemas/plan-schemas.d.ts +0 -21
  146. package/dist/schemas/plan-schemas.js +2 -3
  147. package/dist/schemas/run-synthesis.d.ts +2 -2
  148. package/dist/schemas/run-synthesis.js +3 -3
  149. package/dist/schemas/validate-plan.d.ts +1 -14
  150. package/dist/schemas/validate-plan.js +5 -24
  151. package/dist/schemas/workflow-state.d.ts +3 -32
  152. package/dist/schemas/workflow-state.js +4 -13
  153. package/dist/state/index.d.ts +0 -1
  154. package/dist/state/index.js +0 -1
  155. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  156. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  157. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  158. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  159. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  160. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  161. package/dist/state/migrations/index.js +6 -0
  162. package/dist/state/purge-analysis.d.ts +4 -2
  163. package/dist/state/purge-analysis.js +22 -11
  164. package/dist/state/sandbox-leases.d.ts +81 -0
  165. package/dist/state/sandbox-leases.js +247 -0
  166. package/dist/state/schema.d.ts +1 -13
  167. package/dist/state/schema.js +1 -6
  168. package/dist/state/step-executions.d.ts +0 -2
  169. package/dist/state/step-executions.js +1 -4
  170. package/dist/tasks/data-profile.d.ts +40 -2
  171. package/dist/tasks/data-profile.js +89 -87
  172. package/dist/tools/ad-hoc-router.d.ts +1 -6
  173. package/dist/tools/ad-hoc-router.js +5 -48
  174. package/dist/tools/bio/comptox.js +1 -1
  175. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  176. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  177. package/dist/tools/bio/keys.d.ts +1 -1
  178. package/dist/tools/bio/pubmed.js +31 -0
  179. package/dist/tools/bio/search-gene.js +17 -0
  180. package/dist/tools/bio/search-geo-datasets.js +13 -0
  181. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  182. package/dist/tools/define-tool.d.ts +25 -2
  183. package/dist/tools/define-tool.js +11 -1
  184. package/dist/tools/display/show-user.js +1 -1
  185. package/dist/tools/execute-analysis.d.ts +1 -3
  186. package/dist/tools/execute-analysis.js +5 -33
  187. package/dist/tools/report-session/index.d.ts +0 -1
  188. package/dist/tools/report-session/index.js +0 -1
  189. package/dist/tools/report-session/script-tools.d.ts +30 -0
  190. package/dist/tools/report-session/script-tools.js +177 -0
  191. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  192. package/dist/tools/research/generate-analogy-report.js +111 -49
  193. package/dist/tools/research/generate-plan.d.ts +102 -7
  194. package/dist/tools/research/generate-plan.js +74 -43
  195. package/dist/tools/research/index.d.ts +4 -3
  196. package/dist/tools/research/index.js +4 -3
  197. package/dist/tools/research/inspect-run.d.ts +0 -1
  198. package/dist/tools/research/inspect-run.js +5 -10
  199. package/dist/tools/research/search-arxiv.js +15 -0
  200. package/dist/tools/research/search-github-repos.js +13 -0
  201. package/dist/tools/research/search-semantic-scholar.js +16 -0
  202. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  203. package/dist/tools/research/synthesis-lookups.js +29 -0
  204. package/dist/tools/research/web-fetch.d.ts +1 -1
  205. package/dist/tools/research/web-fetch.js +14 -12
  206. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  207. package/dist/tools/sandbox/lease-tools.js +60 -0
  208. package/dist/tools/script/codemode-worker.d.ts +5 -0
  209. package/dist/tools/script/codemode-worker.js +5 -0
  210. package/dist/tools/script/file-tools.d.ts +100 -0
  211. package/dist/tools/script/file-tools.js +377 -0
  212. package/dist/tools/script/metrics.d.ts +21 -0
  213. package/dist/tools/script/metrics.js +38 -0
  214. package/dist/tools/script/quickjs.wasm +0 -0
  215. package/dist/tools/script/run-script.d.ts +76 -0
  216. package/dist/tools/script/run-script.js +326 -0
  217. package/dist/tools/script/scratch.d.ts +81 -0
  218. package/dist/tools/script/scratch.js +142 -0
  219. package/dist/tools/start-report-session.js +1 -1
  220. package/dist/tools/workspace/edit-file.js +2 -1
  221. package/dist/tools/workspace/execute-command.d.ts +9 -13
  222. package/dist/tools/workspace/execute-command.js +23 -19
  223. package/dist/tools/workspace/file-stat.js +1 -1
  224. package/dist/tools/workspace/grep.d.ts +5 -2
  225. package/dist/tools/workspace/grep.js +19 -10
  226. package/dist/tools/workspace/mutator.d.ts +48 -4
  227. package/dist/tools/workspace/mutator.js +43 -12
  228. package/dist/tools/workspace/read-file.d.ts +5 -2
  229. package/dist/tools/workspace/read-file.js +15 -11
  230. package/dist/tools/workspace/read-surface.d.ts +17 -0
  231. package/dist/tools/workspace/read-surface.js +17 -0
  232. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  233. package/dist/tools/workspace/run-exec.d.ts +12 -13
  234. package/dist/tools/workspace/run-exec.js +13 -13
  235. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  236. package/dist/tools/workspace/scan-inputs.js +2 -3
  237. package/dist/tools/workspace/show-file.js +1 -1
  238. package/dist/tools/workspace/show-plan.js +4 -3
  239. package/dist/tools/workspace/write-file.js +2 -1
  240. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  241. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  242. package/dist/workflows/execute-analysis.d.ts +17 -35
  243. package/dist/workflows/execute-analysis.js +13 -107
  244. package/dist/workflows/pause.js +29 -22
  245. package/dist/workflows/sandbox-step.d.ts +16 -31
  246. package/dist/workflows/sandbox-step.js +36 -71
  247. package/dist/workflows/test/sql-writes.d.ts +0 -1
  248. package/dist/workflows/test/sql-writes.js +0 -2
  249. package/dist/workspace/filesystem.d.ts +2 -2
  250. package/dist/workspace/filesystem.js +10 -5
  251. package/dist/workspace/paths.d.ts +19 -1
  252. package/dist/workspace/paths.js +22 -1
  253. package/package.json +6 -3
  254. package/dist/prompts/literature-reviewer.d.ts +0 -1
  255. package/dist/prompts/literature-reviewer.js +0 -114
  256. package/dist/sandbox/reaper.d.ts +0 -81
  257. package/dist/sandbox/reaper.js +0 -161
  258. package/dist/sandbox/step-sandbox.d.ts +0 -47
  259. package/dist/sandbox/step-sandbox.js +0 -53
  260. package/dist/state/active-sandboxes.d.ts +0 -33
  261. package/dist/state/active-sandboxes.js +0 -65
  262. package/dist/tasks/extract-values-script.d.ts +0 -64
  263. package/dist/tasks/extract-values-script.js +0 -180
  264. package/dist/tasks/extract-values.d.ts +0 -77
  265. package/dist/tasks/extract-values.js +0 -146
  266. package/dist/tools/report-session/derive-table.d.ts +0 -164
  267. package/dist/tools/report-session/derive-table.js +0 -419
  268. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  269. package/dist/tools/research/literature-reviewer.js +0 -107
  270. package/dist/workflows/metrics.d.ts +0 -23
  271. package/dist/workflows/metrics.js +0 -38
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Internal helper used by dependency-bearing tools to drive one sandbox exec
3
- * through the `SandboxClient`. It wraps each sandbox event in a
4
- * `data-sandbox-event` part, thus `execute_command` and the container readout
5
- * of `scan_inputs` report progress in one shape.
2
+ * Internal helper of the tools that run a sandbox command through the lease of their step. It wraps each sandbox
3
+ * event in a `data-sandbox-event` part, thus `execute_command` and the container readout of `scan_inputs` report
4
+ * progress in one shape.
6
5
  *
7
- * Not a registered tool — `defineTool` is called by the user-facing tools
8
- * (`execute-command.ts`) which wrap this.
6
+ * Not a registered tool — `defineTool` is called by the user-facing tools (`execute-command.ts`) which wrap this.
9
7
  */
10
- export function runSandboxExec(args) {
11
- return args.sandboxClient.exec(args.sandbox, {
12
- command: args.command,
13
- cwd: args.cwd,
14
- env: args.env,
15
- timeoutSeconds: args.timeoutSeconds,
16
- }, (event) => args.emit({ type: "data-sandbox-event", data: { event } }), args.deadlineMs);
8
+ import { err, ok } from "neverthrow";
9
+ /** The error of an exec with no lease. The order "acquire, then exec" is a rule of the code, not of a prompt. */
10
+ export const NO_SANDBOX_ERROR = "No sandbox is held. Call acquire_sandbox with the size that the command needs, then run the command again.";
11
+ export async function runSandboxExec(args) {
12
+ const ran = await args.lease.exec({ command: args.command, cwd: args.cwd, env: args.env, timeoutSeconds: args.timeoutSeconds }, (event) => args.emit({ type: "data-sandbox-event", data: { event } }));
13
+ if (ran.isOk())
14
+ return ok(ran.value);
15
+ const detail = ran.error.detail === undefined ? "" : ` (${ran.error.detail})`;
16
+ return err({ error: `${NO_SANDBOX_ERROR}${detail}`, retryable: false });
17
17
  }
@@ -12,17 +12,14 @@
12
12
  * footer-indexed container reaches into the sandbox (see `input-scan/enrich.ts`).
13
13
  */
14
14
  import { z } from "zod";
15
- import type { SandboxClient } from "../../sandbox/client.js";
16
- import type { SandboxRef } from "../../sandbox/types.js";
15
+ import type { BoundLease } from "../../sandbox/leases.js";
17
16
  import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
18
17
  import { type Tool } from "../define-tool.js";
19
18
  export type ScanInputsDeps = {
20
19
  readonly workspaceFs: WorkspaceFilesystem;
21
20
  readonly analysisId: string;
22
21
  /** Omit to skip the container readouts; the prefix readouts run regardless. */
23
- readonly sandboxClient?: SandboxClient;
24
- readonly sandbox?: SandboxRef;
25
- readonly deadlineMs: () => number;
22
+ readonly lease?: BoundLease;
26
23
  };
27
24
  type ScanInputsOutput = {
28
25
  readonly path: string;
@@ -32,6 +32,7 @@ const ScanInputsInputSchema = z.object({
32
32
  export function createScanInputsTool(deps) {
33
33
  return defineTool({
34
34
  id: "scan_inputs",
35
+ ownStep: true,
35
36
  description: "Scan a directory of input files and report what is OBSERVABLE about it: per-format counts, " +
36
37
  "the shapes its filenames form (sets of files whose names differ only at marked positions), " +
37
38
  "the distinct values each varying position takes, how those positions co-occur, value overlap " +
@@ -55,10 +56,8 @@ export function createScanInputsTool(deps) {
55
56
  shapes: scan.manifest.shapes,
56
57
  session: ctx.session,
57
58
  fs: deps.workspaceFs,
58
- sandboxClient: deps.sandboxClient,
59
- sandbox: deps.sandbox,
59
+ lease: deps.lease,
60
60
  mountRoot: `/${deps.analysisId}`,
61
- deadlineMs: deps.deadlineMs(),
62
61
  emit: ctx.emit,
63
62
  });
64
63
  const manifest = { ...scan.manifest, shapes };
@@ -30,7 +30,7 @@ const ShowFileInputSchema = z.object({
30
30
  });
31
31
  export const showFileTool = defineTool({
32
32
  id: "show_file",
33
- description: "Display EXISTING analysis artifacts in chat by path — images, CSVs, PDFs, notebooks, logs. " +
33
+ description: "Display EXISTING analysis artifacts in chat by path — images, CSVs, PDFs, notebooks, logs, and a `.pdb` or `.cif` structure that a step wrote. " +
34
34
  "The card references the file directly and its content is fetched when the user views it: you never read, " +
35
35
  "inline, or paste the bytes. Pick this tool by what you are referencing, not by how the output looks: " +
36
36
  "NOT for content you synthesized (a chart, a table, a snippet you invented — use `show_user`), NOT for a stored plan (`show_plan`). " +
@@ -26,12 +26,13 @@ export function createShowPlanTool(pool) {
26
26
  return defineTool({
27
27
  id: "show_plan",
28
28
  description: "Display a STORED plan in chat by its planId. The plan is loaded from storage and embedded in the card, " +
29
- "so the UI renders it directly. Use it to present the plan `generate_plan` just produced, and when the user " +
30
- "asks to see an earlier plan again. " +
29
+ "so the UI renders it directly. Use it when the user asks to see an earlier plan again: `generate_plan` shows the card " +
30
+ "of the plan that it makes. " +
31
31
  "Pick this tool by what you are referencing, not by how the output looks: " +
32
32
  "NOT for a plan you are drafting in conversation (that is `generate_plan`), NOT for other synthesized content " +
33
33
  "(`show_user`), NOT for analysis files (`show_file`). Never re-type a plan's steps into a `show_user` markdown " +
34
- "card — that renders a second, divergent copy of the same plan.",
34
+ "card — that renders a second, divergent copy of the same plan. " +
35
+ "`shown: false` with `plan_not_found` means that the planId is no longer valid: generate a new plan, because a retry of the same planId fails the same way.",
35
36
  inputSchema: ShowPlanInputSchema,
36
37
  describeCall: ({ planId }) => planId,
37
38
  execute: async ({ planId, title }, ctx) => {
@@ -27,7 +27,8 @@ export function createWriteFileTool(deps) {
27
27
  "missing parent directories. On success it returns `status: ok`, the absolute " +
28
28
  "'/<analysisId>/...' path, and `bytesWritten`. A path outside the working directory " +
29
29
  "returns an `out_of_prefix` data variant (no I/O), one escaping the analysis tree returns " +
30
- "`out_of_scope`, and a symbolic link on the path returns `symlink_denied`.",
30
+ "`out_of_scope`, and a symbolic link on the path returns `symlink_denied`. A path under the staged " +
31
+ "inputs returns `read_only` with the reason.",
31
32
  inputSchema: WriteFileInputSchema,
32
33
  // The path only. `content` is a whole file and must never ride a display
33
34
  // channel, which the emit-site length cap enforces regardless.
@@ -17,7 +17,6 @@
17
17
  * pure means parent recovery replays the same dispatch sequence given the
18
18
  * same persisted child outcomes.
19
19
  */
20
- import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
21
20
  /**
22
21
  * The minimum shape the scheduler needs from a plan step. Larger workflow-state
23
22
  * schemas are a superset of this, keeping the scheduler decoupled.
@@ -26,22 +25,6 @@ export type PlanStep = {
26
25
  readonly id: string;
27
26
  readonly depends_on: readonly string[];
28
27
  };
29
- /** Budget-admission inputs — the snapshotted machine budget from the workflow
30
- * input plus each step's plan-declared resources. */
31
- export type BudgetAdmission = {
32
- readonly budget: MachineBudget;
33
- readonly resourcesByStepId: Readonly<Record<string, ResourceSpec>>;
34
- };
35
- /** Partition of the dependency-satisfied steps under budget admission. */
36
- export type ScheduledSteps = {
37
- /** Steps to start now — they fit the remaining budget capacity. */
38
- readonly admit: readonly string[];
39
- /** Dependency-satisfied steps held until in-flight steps free capacity. */
40
- readonly heldForCapacity: readonly string[];
41
- /** Steps whose declared resources exceed the budget outright — they could
42
- * never be admitted even against an empty budget. */
43
- readonly neverFits: readonly string[];
44
- };
45
28
  /** Thrown when the plan DAG contains a dependency cycle. */
46
29
  export declare class CycleError extends Error {
47
30
  readonly involvedSteps: readonly string[];
@@ -71,17 +54,8 @@ export declare function validatePlanDag<S extends PlanStep>(steps: readonly S[])
71
54
  * completion (or on parent recovery) and dispatches each returned id via
72
55
  * `DBOS.startWorkflow`. Order in the returned array follows the plan's
73
56
  * declared order for deterministic dispatch.
74
- *
75
- * With `admission`, the dependency-satisfied steps are additionally gated on
76
- * the machine budget and partitioned instead of returned flat: a step is
77
- * admitted only while the declared resources of the in-flight steps
78
- * (`startedSet`) plus every step admitted earlier in the round leave room for
79
- * it. Admission is greedy in plan order with skip-over — a candidate that does
80
- * not fit never blocks a later, smaller one. Determinism holds because every
81
- * input derives from the workflow input and checkpointed completion state.
82
57
  */
83
58
  export declare function scheduleReady(steps: readonly PlanStep[], completedSet: ReadonlySet<string>, startedSet?: ReadonlySet<string>): string[];
84
- export declare function scheduleReady(steps: readonly PlanStep[], completedSet: ReadonlySet<string>, startedSet: ReadonlySet<string>, admission: BudgetAdmission): ScheduledSteps;
85
59
  /**
86
60
  * Compute a depth per step. A step with no dependencies is level 0; a step
87
61
  * with deps is `max(level(dep)) + 1`. Used by the UI for column layout in
@@ -113,7 +113,17 @@ function detectCycle(steps) {
113
113
  }
114
114
  }
115
115
  }
116
- export function scheduleReady(steps, completedSet, startedSet = new Set(), admission) {
116
+ // ── Scheduling ───────────────────────────────────────────────────────
117
+ /**
118
+ * Return every step that is now eligible to start: all of its `depends_on`
119
+ * are in `completedSet`, and the step itself is not yet in `startedSet`.
120
+ *
121
+ * Pure function — the parent workflow recomputes this after every child
122
+ * completion (or on parent recovery) and dispatches each returned id via
123
+ * `DBOS.startWorkflow`. Order in the returned array follows the plan's
124
+ * declared order for deterministic dispatch.
125
+ */
126
+ export function scheduleReady(steps, completedSet, startedSet = new Set()) {
117
127
  const ready = [];
118
128
  for (const step of steps) {
119
129
  if (startedSet.has(step.id) || completedSet.has(step.id))
@@ -122,37 +132,7 @@ export function scheduleReady(steps, completedSet, startedSet = new Set(), admis
122
132
  ready.push(step.id);
123
133
  }
124
134
  }
125
- if (!admission)
126
- return ready;
127
- const { budget, resourcesByStepId } = admission;
128
- // A step with no declared resources weighs nothing — it is never throttled,
129
- // matching the unbudgeted legacy behavior for that step.
130
- const weightOf = (id) => resourcesByStepId[id] ?? { cpu: 0, memoryGb: 0 };
131
- let usedCpu = 0;
132
- let usedMemoryGb = 0;
133
- for (const id of startedSet) {
134
- const w = weightOf(id);
135
- usedCpu += w.cpu;
136
- usedMemoryGb += w.memoryGb;
137
- }
138
- const admit = [];
139
- const heldForCapacity = [];
140
- const neverFits = [];
141
- for (const id of ready) {
142
- const w = weightOf(id);
143
- if (w.cpu > budget.cpu || w.memoryGb > budget.memoryGb) {
144
- neverFits.push(id);
145
- }
146
- else if (usedCpu + w.cpu <= budget.cpu && usedMemoryGb + w.memoryGb <= budget.memoryGb) {
147
- admit.push(id);
148
- usedCpu += w.cpu;
149
- usedMemoryGb += w.memoryGb;
150
- }
151
- else {
152
- heldForCapacity.push(id);
153
- }
154
- }
155
- return { admit, heldForCapacity, neverFits };
135
+ return ready;
156
136
  }
157
137
  // ── Topological levels (UI layout) ───────────────────────────────────
158
138
  /**
@@ -37,9 +37,6 @@
37
37
  * part, and are never dispatched. In-flight siblings and independent
38
38
  * ready steps continue; the loop drains and the run finalises (typically
39
39
  * `partial`).
40
- * - the halt cascade (cancel in-flight via
41
- * `Promise.allSettled(handles.map(h => DBOS.cancelWorkflow(h.workflowID)))`,
42
- * stop scheduling) survives only for the `neverFits` plan-validation guard.
43
40
  * 4. Pause (work-pause spec)
44
41
  * - a step pauses in its own workflow, and the parent waits for its result as
45
42
  * for a step that runs. The other steps go on, and the run row stays
@@ -65,8 +62,9 @@
65
62
  * - WRITES NOTHING to the conversation thread (results are pull-only
66
63
  * via `inspectRun`)
67
64
  */
65
+ import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
66
+ import type { ScriptRuntime } from "../tools/script/run-script.js";
68
67
  import type { Querier } from "../state/db.js";
69
- import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
70
68
  import type { RunSession } from "../auth/types.js";
71
69
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
72
70
  import type { Logger } from "../lib/logger.js";
@@ -113,24 +111,10 @@ export type ExecuteAnalysisInput = {
113
111
  * plan row (a mid-run plan edit cannot change a running run's instructions).
114
112
  */
115
113
  readonly planStepById: Readonly<Record<string, AnalysisStep>>;
114
+ /** The request that the plan answers, which the seed of each step renders. Absent when the plan stores none. */
115
+ readonly request?: string;
116
116
  /** Per-step agent id (sandbox catalog entry). */
117
117
  readonly agentByStepId: Readonly<Record<string, string>>;
118
- /** Per-step planner-estimated sandbox resource request. */
119
- readonly resourcesByStepId: Readonly<Record<string, ResourceSpec>>;
120
- /**
121
- * Per-step execution timeout in seconds, from the plan's `step.timeout`.
122
- * Only present for steps that declared one; the child falls back to
123
- * `DEFAULT_STEP_TIMEOUT_SECONDS` when a step is absent here.
124
- */
125
- readonly timeoutByStepId?: Readonly<Record<string, number>>;
126
- /**
127
- * Machine resource budget snapshotted by `executePlan` at the async edge.
128
- * The scheduler admits dependency-satisfied steps only while the declared
129
- * resources of concurrently running steps fit within it. Absent (no policy
130
- * configured, or a workflow persisted before this field existed), every
131
- * dependency-satisfied step starts immediately — the legacy fan-out.
132
- */
133
- readonly budget?: MachineBudget;
134
118
  /**
135
119
  * Replay-stable run-level synthesis decision. Absent preserves the legacy
136
120
  * behavior for workflows persisted before this field existed.
@@ -159,12 +143,10 @@ export type ExecuteAnalysisResult = {
159
143
  readonly canceledSteps: readonly string[];
160
144
  };
161
145
  /**
162
- * A step's in-run display state. Wider than the ledger's `StepExecutionStatus`: `queued` is a
163
- * scheduler-only condition (dependency-satisfied but held for budget capacity) that never reaches a
164
- * database row, and `skipped` is asserted mid-run for a doomed dependent whose ledger row stays
165
- * `pending` until the terminal sweep.
146
+ * A step's in-run display state. Wider than the ledger's `StepExecutionStatus`: `skipped` is asserted
147
+ * mid-run for a doomed dependent whose ledger row stays `pending` until the terminal sweep.
166
148
  */
167
- export type StepRuntimeStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
149
+ export type StepRuntimeStatus = "pending" | "running" | "completed" | "failed" | "skipped";
168
150
  /**
169
151
  * One step's state inside a {@link RunObservation}. `id` is the plan slug the ledger and the
170
152
  * dependency edges use; `name` is the human phrase the plan author wrote, which is the only field
@@ -221,21 +203,21 @@ export type ExecuteAnalysisDeps = {
221
203
  readonly sandboxStepCallable: SandboxStepCallable;
222
204
  /** Workspace-root resolution seam — run dir creation + synthesis disk reads. */
223
205
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
206
+ /** The read path of the workspace. The synthesizer reads the files of the run through it. */
207
+ readonly workspaceFs: WorkspaceFilesystem;
224
208
  /** Model id for the synthesizer agent loop. */
225
209
  readonly synthesisModel: string;
226
- /** The `literature_reviewer` tool of the synthesis. The runtime registers its sub-agent one time. */
227
- readonly literatureReviewer: Tool;
210
+ /** The lookup tools of the synthesis (`createSynthesisLookupTools`). */
211
+ readonly synthesisLookups: readonly Tool[];
212
+ /** Where the VM of `run_script` comes from, for the lookups of the synthesizer. */
213
+ readonly scriptRuntime?: ScriptRuntime;
228
214
  /** Run-level billing-bracket seam (managed: external bracket; OSS: no-op). */
229
215
  readonly runCharge: RunCharge;
230
216
  /** The delays of the timer of an outage. Absent: the default of the pause. */
231
217
  readonly pauseRetryDelaysMs?: readonly number[];
232
218
  /** Run-authorization seam — the terminal path revokes through `revoke`. */
233
219
  readonly runAuthorizer: RunAuthorizer;
234
- /**
235
- * LLM usage-accounting seam for the in-body synthesizer loop (and the
236
- * literature reviewer it delegates to); omitted falls back to the no-op
237
- * recorder.
238
- */
220
+ /** LLM usage-accounting seam for the in-body synthesizer loop; omitted falls back to the no-op recorder. */
239
221
  readonly usageRecorder?: UsageRecorder;
240
222
  /** The store of the loops of the run synthesis and of their read tools. */
241
223
  readonly toolOutputStore?: ToolOutputStore;
@@ -297,7 +279,7 @@ export type ExecuteAnalysisDeps = {
297
279
  * level computation and the seed (`prompt`, composed by `composeStepSeed` at
298
280
  * dispatch); the rest comes from `ExecuteAnalysisInput`. The body fills in
299
281
  * `runSession` via `forStep` after this returns. Exported for the
300
- * projection/timeout-propagation tests.
282
+ * projection tests.
301
283
  */
302
284
  export declare function buildChildInput(args: {
303
285
  input: ExecuteAnalysisInput;
@@ -388,8 +370,8 @@ export declare function synthesisRowUpdate(outcome: {
388
370
  * quick-display findings for the run-completed card; empty when synthesis is
389
371
  * skipped or produced no synthesizable content. A genuine failure re-throws,
390
372
  * failing the run (D10). It runs in the body of the run workflow: each read and
391
- * write is a step, each model call is a step, and each reviewer call starts a
392
- * child workflow. Exported so integration tests can drive the wired synthesis
373
+ * write is a step, each model call is a step, and each lookup call is a step.
374
+ * Exported so integration tests can drive the wired synthesis
393
375
  * without a full parent body.
394
376
  */
395
377
  export declare function synthesizeFindings(args: {
@@ -37,9 +37,6 @@
37
37
  * part, and are never dispatched. In-flight siblings and independent
38
38
  * ready steps continue; the loop drains and the run finalises (typically
39
39
  * `partial`).
40
- * - the halt cascade (cancel in-flight via
41
- * `Promise.allSettled(handles.map(h => DBOS.cancelWorkflow(h.workflowID)))`,
42
- * stop scheduling) survives only for the `neverFits` plan-validation guard.
43
40
  * 4. Pause (work-pause spec)
44
41
  * - a step pauses in its own workflow, and the parent waits for its result as
45
42
  * for a step that runs. The other steps go on, and the run row stays
@@ -81,7 +78,6 @@ import { recordRunCompleted, recordStepCompleted, stepOutcomeOf } from "../lib/m
81
78
  import { describeDbError, withTransaction } from "../lib/db-result.js";
82
79
  import { unwrapOrThrow } from "../lib/result.js";
83
80
  import { countArtifactsByRuns, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, queryStepsByRun, seedStepExecutions, setRunSynthesisOutcome, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
84
- import { clearPausesOfWork } from "../state/paused-work.js";
85
81
  import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
86
82
  import { flattenDataPart } from "../contracts/chat-frame.js";
87
83
  import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
@@ -90,7 +86,6 @@ import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toS
90
86
  import { isChatDataPart, synthesisEmission } from "../sandbox/sandbox-step-translate.js";
91
87
  import { synthesizeRun } from "../app/synthesize-run.js";
92
88
  import { computeTopologicalLevels, scheduleReady, validatePlanDag } from "./execute-analysis-scheduler.js";
93
- import { recordCancelledChild } from "./metrics.js";
94
89
  import { pauseAtGate, workPause } from "./pause.js";
95
90
  import { presentGet } from "../lib/present.js";
96
91
  // ── In-body orchestration helpers ────────────────────────────────────
@@ -183,14 +178,10 @@ function observeRunGuarded(deps, observation) {
183
178
  * level computation and the seed (`prompt`, composed by `composeStepSeed` at
184
179
  * dispatch); the rest comes from `ExecuteAnalysisInput`. The body fills in
185
180
  * `runSession` via `forStep` after this returns. Exported for the
186
- * projection/timeout-propagation tests.
181
+ * projection tests.
187
182
  */
188
183
  export function buildChildInput(args) {
189
184
  const { input, stepId, level, runId, prompt, templateBinding } = args;
190
- const resources = input.resourcesByStepId[stepId];
191
- if (!resources) {
192
- throw new Error(`executeAnalysis: step "${stepId}" missing from resourcesByStepId — every step must declare resources`);
193
- }
194
185
  const planStep = input.planStepById[stepId];
195
186
  if (!planStep) {
196
187
  throw new Error(`executeAnalysis: step "${stepId}" missing from planStepById — every step must carry its plan data`);
@@ -213,8 +204,6 @@ export function buildChildInput(args) {
213
204
  templateBinding,
214
205
  level,
215
206
  prompt,
216
- resources,
217
- timeoutSeconds: input.timeoutByStepId?.[stepId],
218
207
  };
219
208
  }
220
209
  // ── Seed composition (at dispatch, not at plan submission) ────────────
@@ -303,6 +292,7 @@ export async function gatherStepPackage(args) {
303
292
  profile: profile?.result ?? null,
304
293
  upstream,
305
294
  memory: { goal: memory.goal, constraints: memory.constraints },
295
+ request: input.request,
306
296
  ...contract.briefing,
307
297
  },
308
298
  templateBinding: contract.binding,
@@ -504,8 +494,8 @@ export function synthesisRowUpdate(outcome, durationMs) {
504
494
  * quick-display findings for the run-completed card; empty when synthesis is
505
495
  * skipped or produced no synthesizable content. A genuine failure re-throws,
506
496
  * failing the run (D10). It runs in the body of the run workflow: each read and
507
- * write is a step, each model call is a step, and each reviewer call starts a
508
- * child workflow. Exported so integration tests can drive the wired synthesis
497
+ * write is a step, each model call is a step, and each lookup call is a step.
498
+ * Exported so integration tests can drive the wired synthesis
509
499
  * without a full parent body.
510
500
  */
511
501
  export function synthesizeFindings(args) {
@@ -536,8 +526,10 @@ export function synthesizeFindings(args) {
536
526
  provider: deps.provider,
537
527
  embedding: deps.embedding,
538
528
  resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
529
+ workspaceFs: deps.workspaceFs,
539
530
  synthesisModel: deps.synthesisModel,
540
- literatureReviewer: deps.literatureReviewer,
531
+ synthesisLookups: deps.synthesisLookups,
532
+ scriptRuntime: deps.scriptRuntime,
541
533
  usageRecorder: deps.usageRecorder,
542
534
  toolOutputStore: deps.toolOutputStore,
543
535
  }, {
@@ -845,12 +837,6 @@ async function runSchedulerLoop(args) {
845
837
  // exactly without a second channel. A child that settled by throwing carries
846
838
  // no result and therefore contributes nothing — same rule as its duration.
847
839
  const runUsage = {};
848
- // Child workflow ids the parent itself cancelled (the neverFits halt cascade). Their `getResult`
849
- // rejects with a DBOS cancellation error and lands in the settlement error branch — but the step was
850
- // CANCELED by us, not failed on its own merits. Keyed here (deterministic: `cancelInFlight` runs the
851
- // same way on a replay) so that branch records it as canceled everywhere, matching how a child that
852
- // returns a graceful `{status:"canceled"}` is already handled.
853
- const canceledByParent = new Set();
854
840
  const stepRuntime = new Map();
855
841
  for (const step of input.steps) {
856
842
  stepRuntime.set(step.id, { status: "pending" });
@@ -872,7 +858,7 @@ async function runSchedulerLoop(args) {
872
858
  * can never become dependency-satisfied and will never be dispatched.
873
859
  * Stream-only — ledger rows stay `pending` until the terminal sweep flips
874
860
  * them to `skipped`. Dependents of an unfinished step are necessarily
875
- * `pending` (never queued/running); an already-`skipped` one is not
861
+ * `pending` (never running); an already-`skipped` one is not
876
862
  * re-walked.
877
863
  */
878
864
  const markDependentsSkipped = (failedStepId) => {
@@ -887,9 +873,6 @@ async function runSchedulerLoop(args) {
887
873
  }
888
874
  }
889
875
  };
890
- // Halts scheduling entirely — reserved for the `neverFits` guard. An ordinary
891
- // step failure does NOT set it: it dooms only that step's transitive dependents.
892
- let halted = false;
893
876
  let failureReason = null;
894
877
  const stepIndexById = new Map(input.steps.map((s, i) => [s.id, i]));
895
878
  const emitDagSnapshot = async () => {
@@ -932,59 +915,14 @@ async function runSchedulerLoop(args) {
932
915
  observeRunGuarded(deps, buildRunObservation({ input, runId, status: "running", stepStates: stepRuntime }));
933
916
  };
934
917
  const dispatchReady = async () => {
935
- if (halted)
936
- return;
937
918
  const inFlightStepIds = new Set([...inFlight.values()].map((h) => h.stepId));
938
919
  // A settled-but-not-completed step (failed / blocked / canceled) is not
939
920
  // in `completed` and not in flight, so `scheduleReady` would offer it
940
- // again — filter it out of the candidate list. This also stops its
941
- // declared resources from weighing against the budget: settled steps
942
- // hold no capacity.
921
+ // again — filter it out of the candidate list.
943
922
  const candidates = input.steps.filter((s) => !failed.has(s.id) && !canceled.has(s.id));
944
- let admit;
945
- let queuedChanged = false;
946
- if (input.budget) {
947
- const scheduled = scheduleReady(candidates, completed, inFlightStepIds, {
948
- budget: input.budget,
949
- resourcesByStepId: input.resourcesByStepId,
950
- });
951
- if (scheduled.neverFits.length > 0) {
952
- // An over-budget step can never be admitted — plan-time validation
953
- // makes this unreachable for new plans; stored plans fail loudly
954
- // instead of waiting forever. The stored plan is invalid against
955
- // this machine, so the run halts outright — unlike an ordinary
956
- // step failure, which dooms only its dependents.
957
- for (const stepId of scheduled.neverFits) {
958
- const r = input.resourcesByStepId[stepId];
959
- const declared = r ? `${r.cpu} CPU / ${r.memoryGb} GB` : "undeclared";
960
- failed.add(stepId);
961
- stepRuntime.set(stepId, {
962
- status: "failed",
963
- error: `step resources (${declared}) exceed the machine budget (${input.budget.cpu} CPU / ${input.budget.memoryGb} GB)`,
964
- });
965
- }
966
- halted = true;
967
- failureReason = `step "${String(scheduled.neverFits[0])}" exceeds the machine resource budget`;
968
- await cancelInFlight(inFlight, "fail_fast", canceledByParent);
969
- await emitDagSnapshot();
970
- return;
971
- }
972
- for (const stepId of scheduled.heldForCapacity) {
973
- if (presentGet(stepRuntime, stepId, "the runtime of the step").status !== "queued") {
974
- stepRuntime.set(stepId, { status: "queued" });
975
- queuedChanged = true;
976
- }
977
- }
978
- admit = scheduled.admit;
979
- }
980
- else {
981
- admit = scheduleReady(candidates, completed, inFlightStepIds);
982
- }
983
- if (admit.length === 0) {
984
- if (queuedChanged)
985
- await emitDagSnapshot();
923
+ const admit = scheduleReady(candidates, completed, inFlightStepIds);
924
+ if (admit.length === 0)
986
925
  return;
987
- }
988
926
  for (const stepId of admit) {
989
927
  const idx = presentGet(stepIndexById, stepId, "the index of the step");
990
928
  const childWorkflowId = `${workflowId}-${idx}`;
@@ -1072,8 +1010,7 @@ async function runSchedulerLoop(args) {
1072
1010
  // failed or blocked — a blocker (see the harness-sandbox-agents spec)
1073
1011
  // is treated exactly like a failure: the step declared it cannot
1074
1012
  // deliver, so only its transitive dependents can never run. In-flight
1075
- // siblings and independent ready steps continue; the dispatch round
1076
- // below also lets a budget-held step claim the freed capacity.
1013
+ // siblings and independent ready steps continue.
1077
1014
  failed.add(settled.stepId);
1078
1015
  failureReason ??= r.error ?? (r.status === "blocked" ? "step_blocked" : "step_failed");
1079
1016
  stepRuntime.set(settled.stepId, {
@@ -1088,26 +1025,8 @@ async function runSchedulerLoop(args) {
1088
1025
  await dispatchReady();
1089
1026
  }
1090
1027
  }
1091
- else if (canceledByParent.has(settled.childId)) {
1092
- // The child threw because the PARENT cancelled it (the neverFits halt cascade), not
1093
- // because it failed on its own — its `getResult` rejects with a DBOS cancellation error.
1094
- // Record it as canceled, mirroring the graceful `{status:"canceled"}` branch above so the
1095
- // `canceled` set, the terminal result, and the `step_completed` provenance all agree. No
1096
- // halt / cancel cascade here: the run is already halted (that is why this child was cancelled).
1097
- canceled.add(settled.stepId);
1098
- stepRuntime.set(settled.stepId, {
1099
- // `stepRuntime` (the DAG snapshot) has no canceled tier; the graceful-cancel branch above
1100
- // likewise renders canceled steps as "failed", so this stays consistent with it.
1101
- status: "failed",
1102
- error: errorMessageOf(settled.err) ?? textOf(settled.err),
1103
- });
1104
- stepStatus = "canceled";
1105
- stepDurationMs = undefined;
1106
- await emitDagSnapshot();
1107
- }
1108
1028
  else {
1109
- // The child threw on its own (not a parent-driven cancel). A throw is
1110
- // treated exactly like a step failure:
1029
+ // The child threw. A throw is treated exactly like a step failure:
1111
1030
  // dependents doomed, siblings continue.
1112
1031
  failed.add(settled.stepId);
1113
1032
  failureReason ??= errorMessageOf(settled.err) ?? textOf(settled.err);
@@ -1148,17 +1067,6 @@ async function runSchedulerLoop(args) {
1148
1067
  usage: hasReportedUsage(runUsage) ? { ...runUsage } : undefined,
1149
1068
  };
1150
1069
  }
1151
- async function cancelInFlight(inFlight, cause, canceledByParent) {
1152
- const ids = [...inFlight.keys()];
1153
- // Record every id BEFORE the cancel so the settlement loop classifies the resulting
1154
- // DBOS-cancellation rejection as canceled rather than failed (see `canceledByParent`).
1155
- for (const childId of ids)
1156
- canceledByParent.add(childId);
1157
- await Promise.allSettled(ids.map((childId) => DBOS.cancelWorkflow(childId)));
1158
- for (let i = 0; i < ids.length; i++) {
1159
- recordCancelledChild({ cause });
1160
- }
1161
- }
1162
1070
  // ── (6) collectAndComplete ────────────────────────────────────────────
1163
1071
  /** The waits between the attempts are 1, 2, 4, and 8 seconds. */
1164
1072
  const FINAL_STATUS_WRITE_RETRY = { retriesAllowed: true, maxAttempts: 5, intervalSeconds: 1, backoffRate: 2 };
@@ -1294,8 +1202,6 @@ async function collectAndComplete(args) {
1294
1202
  }
1295
1203
  // Sweep never-started steps to `skipped`. Log-don't-rollback, like every other finalisation step here.
1296
1204
  await ledgerStep(logger, "sweep-pending-steps", () => sweepPendingStepExecutions(deps.pool, runId));
1297
- // A step that the neverFits halt canceled in its pause leaves its row, thus the end of the run clears each row of the run.
1298
- await ledgerStep(logger, "clear-run-pauses", () => clearPausesOfWork(deps.pool, runId));
1299
1205
  // The run end was emitted above. These branches only fan out the UI stream part, whose
1300
1206
  // completed/failed shapes genuinely differ — the provenance record does not.
1301
1207
  if (status === "completed" || status === "partial") {
@@ -14,6 +14,7 @@ import { DBOS } from "@dbos-inc/dbos-sdk";
14
14
  import { z } from "zod";
15
15
  import { createNoopLogger } from "../lib/console-logger.js";
16
16
  import { deliverNotice } from "../lib/hooks.js";
17
+ import { trackWorkPaused } from "../lib/metrics.js";
17
18
  import { unwrapOrThrow } from "../lib/result.js";
18
19
  import { isTurnCredentialMissing } from "../loop/turn-credentials.js";
19
20
  import { modelFailureOf } from "../providers/errors.js";
@@ -123,30 +124,36 @@ export function workPause(scope) {
123
124
  await suspendCredential("pause.suspend");
124
125
  await scope.emit({ type: "data-pause", id: workflowId, ...work, state, cause, attempt, retryAt });
125
126
  log.warn("work paused", { workflowId, ...work, state, cause: cause.kind, attempt });
126
- const waited = await waitForMessage(topic, delayMs, signal);
127
127
  let end;
128
- if (waited === "hold") {
129
- // A person held the work: the timer stops, and the work waits for a resume.
130
- await DBOS.runStep(async () => unwrapOrThrow(await recordPause(scope.pool, {
131
- workflowId,
132
- ...work,
133
- analysisId: scope.analysisId,
134
- threadId: scope.threadId,
135
- state: "paused",
136
- cause,
137
- attempt,
138
- topic,
139
- pausedAt: new Date(pausedAt).toISOString(),
140
- runSession: scope.run?.session,
141
- })), { name: "pause.hold" });
142
- await suspendCredential("pause.hold.suspend");
143
- await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "paused", cause, attempt });
144
- log.info("work held", { workflowId, ...work, attempt });
145
- const held = await waitForMessage(topic, undefined, signal);
146
- end = held === "hold" ? "resume" : held;
128
+ const untrack = trackWorkPaused({ workKind: scope.workKind });
129
+ try {
130
+ const waited = await waitForMessage(topic, delayMs, signal);
131
+ if (waited === "hold") {
132
+ // A person held the work: the timer stops, and the work waits for a resume.
133
+ await DBOS.runStep(async () => unwrapOrThrow(await recordPause(scope.pool, {
134
+ workflowId,
135
+ ...work,
136
+ analysisId: scope.analysisId,
137
+ threadId: scope.threadId,
138
+ state: "paused",
139
+ cause,
140
+ attempt,
141
+ topic,
142
+ pausedAt: new Date(pausedAt).toISOString(),
143
+ runSession: scope.run?.session,
144
+ })), { name: "pause.hold" });
145
+ await suspendCredential("pause.hold.suspend");
146
+ await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "paused", cause, attempt });
147
+ log.info("work held", { workflowId, ...work, attempt });
148
+ const held = await waitForMessage(topic, undefined, signal);
149
+ end = held === "hold" ? "resume" : held;
150
+ }
151
+ else {
152
+ end = waited;
153
+ }
147
154
  }
148
- else {
149
- end = waited;
155
+ finally {
156
+ untrack();
150
157
  }
151
158
  await DBOS.runStep(async () => unwrapOrThrow(await clearPause(scope.pool, workflowId)), { name: "pause.clear" });
152
159
  await scope.emit({ type: "data-pause", id: workflowId, ...work, state: "running" });