@inflexa-ai/harness 0.46.2 → 0.48.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 (279) hide show
  1. package/README.md +2 -2
  2. package/dist/agents/conversation-agent.d.ts +7 -6
  3. package/dist/agents/conversation-agent.js +26 -4
  4. package/dist/agents/report-session-agent.d.ts +14 -14
  5. package/dist/agents/report-session-agent.js +40 -26
  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 +5 -6
  25. package/dist/index.js +5 -7
  26. package/dist/input-scan/enrich.d.ts +5 -10
  27. package/dist/input-scan/enrich.js +12 -8
  28. package/dist/lib/hooks.d.ts +1 -1
  29. package/dist/lib/markdown-pdf.d.ts +27 -0
  30. package/dist/lib/markdown-pdf.js +236 -0
  31. package/dist/lib/metrics.d.ts +31 -0
  32. package/dist/lib/metrics.js +64 -0
  33. package/dist/lib/pdf-text.d.ts +23 -0
  34. package/dist/lib/pdf-text.js +34 -0
  35. package/dist/lib/regulatory-corpus/fetch-fda-guidance.js +3 -5
  36. package/dist/lib/regulatory-corpus/fetch-ich.js +3 -5
  37. package/dist/loop/chat-stream.js +2 -1
  38. package/dist/loop/run-loop.d.ts +5 -0
  39. package/dist/loop/run-loop.js +30 -2
  40. package/dist/loop/tool-name-repair.d.ts +11 -0
  41. package/dist/loop/tool-name-repair.js +23 -0
  42. package/dist/loop/types.d.ts +19 -0
  43. package/dist/memory/card-builders.d.ts +1 -1
  44. package/dist/prompts/SOUL.d.ts +6 -6
  45. package/dist/prompts/SOUL.js +22 -121
  46. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  47. package/dist/prompts/analogical-reasoner.js +35 -101
  48. package/dist/prompts/briefing.d.ts +20 -11
  49. package/dist/prompts/briefing.js +36 -19
  50. package/dist/prompts/conversation.d.ts +1 -1
  51. package/dist/prompts/conversation.js +103 -383
  52. package/dist/prompts/planner.d.ts +1 -5
  53. package/dist/prompts/planner.js +45 -128
  54. package/dist/prompts/report-session.d.ts +1 -1
  55. package/dist/prompts/report-session.js +29 -58
  56. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  57. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +3 -3
  58. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  59. package/dist/prompts/sandbox/cheminformatics-agent.js +12 -47
  60. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  61. package/dist/prompts/sandbox/chromatin-agent.js +4 -5
  62. package/dist/prompts/sandbox/data-profiler.js +11 -34
  63. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  64. package/dist/prompts/sandbox/dna-methylation-agent.js +3 -1
  65. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  66. package/dist/prompts/sandbox/drug-repurposing-agent.js +23 -73
  67. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  68. package/dist/prompts/sandbox/enrichment-agent.js +3 -1
  69. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  70. package/dist/prompts/sandbox/genomic-variant-agent.js +3 -4
  71. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  72. package/dist/prompts/sandbox/immune-profiling-agent.js +5 -4
  73. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  74. package/dist/prompts/sandbox/metabolomics-agent.js +3 -1
  75. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  76. package/dist/prompts/sandbox/microbiome-agent.js +3 -1
  77. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  78. package/dist/prompts/sandbox/multi-omics-integration-agent.js +3 -1
  79. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  80. package/dist/prompts/sandbox/multimodal-sc-agent.js +3 -1
  81. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  82. package/dist/prompts/sandbox/network-agent.js +3 -1
  83. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  84. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +4 -7
  85. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  86. package/dist/prompts/sandbox/proteomics-agent.js +3 -1
  87. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  88. package/dist/prompts/sandbox/scientific-executor.js +0 -12
  89. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/single-cell-agent.js +3 -1
  91. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -1
  93. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/statistical-modeling-agent.js +4 -4
  95. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  96. package/dist/prompts/sandbox/translational-safety-agent.js +10 -29
  97. package/dist/prompts/sandbox-standards.d.ts +15 -18
  98. package/dist/prompts/sandbox-standards.js +96 -305
  99. package/dist/prompts/synthesis-agent.d.ts +1 -1
  100. package/dist/prompts/synthesis-agent.js +24 -90
  101. package/dist/provenance/collector.d.ts +2 -5
  102. package/dist/provenance/collector.js +2 -5
  103. package/dist/provenance/seam.d.ts +36 -1
  104. package/dist/provenance/workspace-reads.d.ts +36 -0
  105. package/dist/provenance/workspace-reads.js +33 -0
  106. package/dist/report-model/production-resolver.d.ts +22 -52
  107. package/dist/report-model/production-resolver.js +43 -112
  108. package/dist/report-render/chart-export.d.ts +9 -0
  109. package/dist/report-render/chart-export.js +1 -1
  110. package/dist/report-render/views/chart-view.d.ts +1 -0
  111. package/dist/report-render/views/chart-view.js +1 -0
  112. package/dist/report-render/views/lineage.d.ts +1 -0
  113. package/dist/report-render/views/page-view.d.ts +1 -0
  114. package/dist/report-render/views/page-view.js +1 -0
  115. package/dist/report-render/views/prose.d.ts +1 -0
  116. package/dist/report-render/views/prose.js +1 -0
  117. package/dist/report-render/views/references-view.d.ts +1 -0
  118. package/dist/report-render/views/values.d.ts +1 -0
  119. package/dist/report-render/views/values.js +1 -0
  120. package/dist/runtime/assemble.d.ts +32 -12
  121. package/dist/runtime/assemble.js +49 -38
  122. package/dist/sandbox/client.d.ts +5 -3
  123. package/dist/sandbox/create-sandbox.d.ts +7 -12
  124. package/dist/sandbox/create-sandbox.js +15 -37
  125. package/dist/sandbox/docker-client.d.ts +1 -3
  126. package/dist/sandbox/docker-client.js +3 -5
  127. package/dist/sandbox/exec-outcome.d.ts +11 -11
  128. package/dist/sandbox/exec-outcome.js +22 -22
  129. package/dist/sandbox/exec.d.ts +5 -0
  130. package/dist/sandbox/exec.js +9 -1
  131. package/dist/sandbox/identity.d.ts +4 -16
  132. package/dist/sandbox/identity.js +5 -21
  133. package/dist/sandbox/k8s-client.d.ts +0 -1
  134. package/dist/sandbox/k8s-client.js +8 -13
  135. package/dist/sandbox/lease-names.d.ts +6 -0
  136. package/dist/sandbox/lease-names.js +12 -0
  137. package/dist/sandbox/leases.d.ts +105 -0
  138. package/dist/sandbox/leases.js +519 -0
  139. package/dist/sandbox/liveness.d.ts +5 -2
  140. package/dist/sandbox/mount-plan.d.ts +10 -1
  141. package/dist/sandbox/mount-plan.js +7 -1
  142. package/dist/sandbox/reconciler.d.ts +58 -0
  143. package/dist/sandbox/reconciler.js +153 -0
  144. package/dist/sandbox/sandbox-step-translate.d.ts +1 -1
  145. package/dist/sandbox/types.d.ts +25 -18
  146. package/dist/sandbox/types.js +10 -4
  147. package/dist/schemas/plan-schemas.d.ts +0 -21
  148. package/dist/schemas/plan-schemas.js +2 -3
  149. package/dist/schemas/run-synthesis.d.ts +2 -2
  150. package/dist/schemas/run-synthesis.js +3 -3
  151. package/dist/schemas/validate-plan.d.ts +1 -14
  152. package/dist/schemas/validate-plan.js +5 -24
  153. package/dist/schemas/workflow-state.d.ts +3 -32
  154. package/dist/schemas/workflow-state.js +4 -13
  155. package/dist/state/index.d.ts +2 -3
  156. package/dist/state/index.js +1 -2
  157. package/dist/state/migrations/20261030120000_lease_the_sandboxes.d.ts +7 -0
  158. package/dist/state/migrations/20261030120000_lease_the_sandboxes.js +34 -0
  159. package/dist/state/migrations/20261030120100_drop_the_report_derivations.d.ts +5 -0
  160. package/dist/state/migrations/20261030120100_drop_the_report_derivations.js +9 -0
  161. package/dist/state/migrations/20261030120200_keep_the_report_derivations.d.ts +6 -0
  162. package/dist/state/migrations/20261030120200_keep_the_report_derivations.js +10 -0
  163. package/dist/state/migrations/index.js +6 -0
  164. package/dist/state/purge-analysis.d.ts +4 -2
  165. package/dist/state/purge-analysis.js +22 -11
  166. package/dist/state/sandbox-leases.d.ts +81 -0
  167. package/dist/state/sandbox-leases.js +247 -0
  168. package/dist/state/schema.d.ts +1 -13
  169. package/dist/state/schema.js +1 -6
  170. package/dist/state/step-executions.d.ts +13 -2
  171. package/dist/state/step-executions.js +23 -4
  172. package/dist/tasks/data-profile.d.ts +40 -2
  173. package/dist/tasks/data-profile.js +89 -87
  174. package/dist/tools/ad-hoc-router.d.ts +1 -6
  175. package/dist/tools/ad-hoc-router.js +5 -48
  176. package/dist/tools/bio/comptox.js +1 -1
  177. package/dist/tools/bio/drug-gene-interactions.js +1 -1
  178. package/dist/tools/bio/gene-disease-evidence.js +1 -1
  179. package/dist/tools/bio/keys.d.ts +1 -1
  180. package/dist/tools/bio/pubmed.js +31 -0
  181. package/dist/tools/bio/search-gene.js +17 -0
  182. package/dist/tools/bio/search-geo-datasets.js +13 -0
  183. package/dist/tools/bio/search-regulatory-approvals.js +1 -1
  184. package/dist/tools/define-tool.d.ts +25 -2
  185. package/dist/tools/define-tool.js +11 -1
  186. package/dist/tools/display/show-user.js +1 -1
  187. package/dist/tools/execute-analysis.d.ts +1 -3
  188. package/dist/tools/execute-analysis.js +5 -33
  189. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  190. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  191. package/dist/tools/report-session/index.d.ts +0 -1
  192. package/dist/tools/report-session/index.js +0 -1
  193. package/dist/tools/report-session/script-tools.d.ts +33 -0
  194. package/dist/tools/report-session/script-tools.js +186 -0
  195. package/dist/tools/research/generate-analogy-report.d.ts +16 -9
  196. package/dist/tools/research/generate-analogy-report.js +111 -49
  197. package/dist/tools/research/generate-plan.d.ts +102 -7
  198. package/dist/tools/research/generate-plan.js +74 -43
  199. package/dist/tools/research/index.d.ts +4 -3
  200. package/dist/tools/research/index.js +4 -3
  201. package/dist/tools/research/inspect-run.d.ts +0 -1
  202. package/dist/tools/research/inspect-run.js +5 -10
  203. package/dist/tools/research/search-arxiv.js +15 -0
  204. package/dist/tools/research/search-github-repos.js +13 -0
  205. package/dist/tools/research/search-semantic-scholar.js +16 -0
  206. package/dist/tools/research/synthesis-lookups.d.ts +21 -0
  207. package/dist/tools/research/synthesis-lookups.js +29 -0
  208. package/dist/tools/research/web-fetch.d.ts +1 -1
  209. package/dist/tools/research/web-fetch.js +14 -12
  210. package/dist/tools/sandbox/lease-tools.d.ts +32 -0
  211. package/dist/tools/sandbox/lease-tools.js +60 -0
  212. package/dist/tools/script/archive.d.ts +34 -0
  213. package/dist/tools/script/archive.js +182 -0
  214. package/dist/tools/script/codemode-worker.d.ts +5 -0
  215. package/dist/tools/script/codemode-worker.js +5 -0
  216. package/dist/tools/script/file-tools.d.ts +106 -0
  217. package/dist/tools/script/file-tools.js +491 -0
  218. package/dist/tools/script/metrics.d.ts +30 -0
  219. package/dist/tools/script/metrics.js +53 -0
  220. package/dist/tools/script/quickjs.wasm +0 -0
  221. package/dist/tools/script/run-script.d.ts +76 -0
  222. package/dist/tools/script/run-script.js +413 -0
  223. package/dist/tools/script/scratch.d.ts +84 -0
  224. package/dist/tools/script/scratch.js +145 -0
  225. package/dist/tools/start-report-session.js +1 -1
  226. package/dist/tools/workspace/edit-file.js +2 -1
  227. package/dist/tools/workspace/execute-command.d.ts +9 -13
  228. package/dist/tools/workspace/execute-command.js +23 -19
  229. package/dist/tools/workspace/file-stat.js +1 -1
  230. package/dist/tools/workspace/grep.d.ts +5 -2
  231. package/dist/tools/workspace/grep.js +19 -10
  232. package/dist/tools/workspace/mutator.d.ts +51 -4
  233. package/dist/tools/workspace/mutator.js +54 -13
  234. package/dist/tools/workspace/read-file.d.ts +5 -2
  235. package/dist/tools/workspace/read-file.js +15 -11
  236. package/dist/tools/workspace/read-surface.d.ts +17 -0
  237. package/dist/tools/workspace/read-surface.js +17 -0
  238. package/dist/tools/workspace/result-bounds.d.ts +1 -0
  239. package/dist/tools/workspace/run-exec.d.ts +12 -13
  240. package/dist/tools/workspace/run-exec.js +13 -13
  241. package/dist/tools/workspace/scan-inputs.d.ts +2 -5
  242. package/dist/tools/workspace/scan-inputs.js +2 -3
  243. package/dist/tools/workspace/show-file.js +1 -1
  244. package/dist/tools/workspace/show-plan.js +4 -3
  245. package/dist/tools/workspace/write-file.js +2 -1
  246. package/dist/workflows/execute-analysis-scheduler.d.ts +0 -26
  247. package/dist/workflows/execute-analysis-scheduler.js +12 -32
  248. package/dist/workflows/execute-analysis.d.ts +17 -35
  249. package/dist/workflows/execute-analysis.js +13 -107
  250. package/dist/workflows/pause.js +29 -22
  251. package/dist/workflows/sandbox-step.d.ts +16 -31
  252. package/dist/workflows/sandbox-step.js +36 -71
  253. package/dist/workflows/test/sql-writes.d.ts +0 -1
  254. package/dist/workflows/test/sql-writes.js +0 -2
  255. package/dist/workspace/filesystem.d.ts +2 -2
  256. package/dist/workspace/filesystem.js +10 -5
  257. package/dist/workspace/paths.d.ts +19 -1
  258. package/dist/workspace/paths.js +22 -1
  259. package/dist/workspace/session-files.d.ts +35 -0
  260. package/dist/workspace/session-files.js +16 -0
  261. package/package.json +7 -3
  262. package/dist/prompts/literature-reviewer.d.ts +0 -1
  263. package/dist/prompts/literature-reviewer.js +0 -114
  264. package/dist/sandbox/reaper.d.ts +0 -81
  265. package/dist/sandbox/reaper.js +0 -161
  266. package/dist/sandbox/step-sandbox.d.ts +0 -47
  267. package/dist/sandbox/step-sandbox.js +0 -53
  268. package/dist/state/active-sandboxes.d.ts +0 -33
  269. package/dist/state/active-sandboxes.js +0 -65
  270. package/dist/tasks/extract-values-script.d.ts +0 -64
  271. package/dist/tasks/extract-values-script.js +0 -180
  272. package/dist/tasks/extract-values.d.ts +0 -77
  273. package/dist/tasks/extract-values.js +0 -146
  274. package/dist/tools/report-session/derive-table.d.ts +0 -164
  275. package/dist/tools/report-session/derive-table.js +0 -419
  276. package/dist/tools/research/literature-reviewer.d.ts +0 -42
  277. package/dist/tools/research/literature-reviewer.js +0 -107
  278. package/dist/workflows/metrics.d.ts +0 -23
  279. package/dist/workflows/metrics.js +0 -38
@@ -28,9 +28,11 @@ import { workPause } from "../workflows/pause.js";
28
28
  import { hasBrowserUrl } from "../lib/chrome.js";
29
29
  import { createStaticEyes } from "../lib/eyes.js";
30
30
  import { registerExecuteAnalysis } from "../workflows/execute-analysis.js";
31
+ import { createSandboxLeases } from "../sandbox/leases.js";
32
+ import { createNoopLogger } from "../lib/console-logger.js";
33
+ import { dbosClientFor } from "../execution/dbos-workflow-purger.js";
31
34
  import { registerSandboxStep } from "../workflows/sandbox-step.js";
32
35
  import { registerDataProfileWorkflow, stopDataProfile } from "../tasks/data-profile.js";
33
- import { createExtractionArm, runExtraction } from "../tasks/extract-values.js";
34
36
  import { createCitationResolver } from "../citations/resolve.js";
35
37
  import { createThreadStore } from "../memory/thread-store.js";
36
38
  import { createWorkingMemory } from "../memory/working-memory.js";
@@ -38,7 +40,7 @@ import { createArtifactReadStore, createProductionResolver } from "../report-mod
38
40
  import { createReportVersionStore } from "../state/report-versions.js";
39
41
  import { createToolOutputStore } from "../state/tool-outputs.js";
40
42
  import { createAskGateway } from "../tools/approval/gateway.js";
41
- import { createLiteratureReviewerTool } from "../tools/research/literature-reviewer.js";
43
+ import { createSynthesisLookupTools } from "../tools/research/synthesis-lookups.js";
42
44
  const _assertDomainError = true;
43
45
  /**
44
46
  * Wrap a type→agent registry as the resolution surface. Held apart from
@@ -83,7 +85,7 @@ export function resolveCompositionEyes(seam, chrome) {
83
85
  return undefined;
84
86
  }
85
87
  export function assembleCoreRuntime(deps) {
86
- const { conversation, workflows: wf, resourcePolicy } = deps;
88
+ const { conversation, workflows: wf } = deps;
87
89
  const usageRecorder = deps.usageRecorder ?? createNoopUsageRecorder();
88
90
  const citationResolver = createCitationResolver({
89
91
  ...deps.citationResolverConfig,
@@ -102,8 +104,21 @@ export function assembleCoreRuntime(deps) {
102
104
  logger: conversation.logger,
103
105
  }).afterFailure;
104
106
  const registerPausingSubAgent = (name, body) => registerSubAgent(name, body, subAgentPauses);
107
+ // One sandbox module for each runtime: one budget, one lease workflow, and one reconciler.
108
+ const leases = createSandboxLeases({
109
+ pool: conversation.pool,
110
+ client: deps.sandbox.client,
111
+ resourcePolicy: deps.resourcePolicy,
112
+ resolveWorkspaceRoot: conversation.resolveWorkspaceRoot,
113
+ runAuthorizer: conversation.runAuthorizer,
114
+ idlePeriodMs: deps.sandbox.idlePeriodMs,
115
+ pauseRetryDelaysMs,
116
+ logger: conversation.logger,
117
+ dbosClient: () => dbosClientFor(conversation.pool, conversation.logger ?? createNoopLogger()),
118
+ });
105
119
  const sandboxStep = registerSandboxStep({
106
120
  ...wf.sandboxStep,
121
+ leases,
107
122
  citationResolver,
108
123
  usageRecorder,
109
124
  toolOutputStore,
@@ -111,24 +126,18 @@ export function assembleCoreRuntime(deps) {
111
126
  runAuthorizer: conversation.runAuthorizer,
112
127
  });
113
128
  const { bioKeys: synthesisBioKeys, ...executeAnalysisDeps } = wf.buildExecuteAnalysis(sandboxStep);
114
- const literatureReviewer = createLiteratureReviewerTool({
115
- provider: executeAnalysisDeps.provider,
116
- model: executeAnalysisDeps.synthesisModel,
117
- bioKeys: synthesisBioKeys,
118
- citationResolver,
119
- usageRecorder,
120
- toolOutputStore,
121
- logger: executeAnalysisDeps.logger,
122
- }, registerPausingSubAgent);
129
+ const synthesisLookups = createSynthesisLookupTools({ bioKeys: synthesisBioKeys, citationResolver, logger: executeAnalysisDeps.logger });
123
130
  const executeAnalysis = registerExecuteAnalysis({
124
131
  ...executeAnalysisDeps,
125
- literatureReviewer,
132
+ synthesisLookups,
133
+ scriptRuntime: conversation.scriptRuntime,
134
+ workspaceFs: conversation.workspaceFs,
126
135
  usageRecorder,
127
136
  toolOutputStore,
128
137
  workingMemory: createWorkingMemory(conversation.pool),
129
138
  pauseRetryDelaysMs,
130
139
  });
131
- const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, usageRecorder, toolOutputStore, pauseRetryDelaysMs });
140
+ const dataProfile = registerDataProfileWorkflow({ ...wf.dataProfile, leases, usageRecorder, toolOutputStore, pauseRetryDelaysMs });
132
141
  // The session runtime binds the per-session state to the thread behind the
133
142
  // tool boundary, thus one instance serves every report thread. It comes before
134
143
  // the conversation agent, because `start_report_session` takes its anchor
@@ -147,7 +156,6 @@ export function assembleCoreRuntime(deps) {
147
156
  ...conversation,
148
157
  executeAnalysisWorkflow: executeAnalysis,
149
158
  anchorReportSession: reportSession.ensureSessionState.bind(reportSession),
150
- resourcePolicy,
151
159
  usageRecorder,
152
160
  toolOutputStore,
153
161
  citationResolver,
@@ -155,6 +163,7 @@ export function assembleCoreRuntime(deps) {
155
163
  // The start tool passes it to the spawn, which emits the creation of a
156
164
  // report session. The same seam reaches the report agent below.
157
165
  provenance: deps.provenance,
166
+ syncSessionFile: deps.syncSessionFile,
158
167
  registerSubAgent: registerPausingSubAgent,
159
168
  });
160
169
  // The report agent is a singleton over the conversation deps, the same way the
@@ -164,21 +173,16 @@ export function assembleCoreRuntime(deps) {
164
173
  const reportThreads = createThreadStore(conversation.pool);
165
174
  // The read cache of the production report path, one for each recent analysis. A preview
166
175
  // and the record that follows it are two tool calls over one draft, thus they meet the
167
- // same artifacts. The store carries the streamed hash and the extracted rows between
168
- // them, and each resolver still binds its own extraction arm below. Thus the auth of a
169
- // call never outlives the call, and one unchanged file costs one hash and one container.
176
+ // same artifacts. The store carries the streamed hash and the parsed rows between them,
177
+ // thus one unchanged file costs one hash and one parse.
170
178
  const reportArtifactReads = createArtifactReadStore();
171
179
  // The reference resolver of the report path, and its production wiring. A resolver reads
172
180
  // the pinned artifacts of one analysis, thus the factory binds one analysis and one caller
173
- // for each tool call. The extraction arm falls through to an extraction pass on the sandbox
174
- // rail of the profile for an over-cap file or a host parse fault. The pass runs inside the
175
- // step of the tool call. The cap gates the host read, and the resolver defaults it to 16 MiB.
181
+ // for each tool call. The cap gates the host read, and the resolver defaults it to 16 MiB.
176
182
  const makeProductionReportResolver = async (scope) => {
177
- const arm = createExtractionArm((requests) => runExtraction({ sandboxClient: wf.dataProfile.sandboxClient, logger: wf.dataProfile.logger }, scope, requests));
178
183
  return createProductionResolver({
179
184
  workspaceRoot: await conversation.resolveWorkspaceRoot(scope.analysisId),
180
185
  analysisId: scope.analysisId,
181
- extractionArm: arm,
182
186
  readCache: reportArtifactReads.forAnalysis(scope.analysisId),
183
187
  cap: deps.reportHostReadCapBytes,
184
188
  });
@@ -192,21 +196,19 @@ export function assembleCoreRuntime(deps) {
192
196
  gateway: reportSession.gateway,
193
197
  resolveWorkspaceRoot: conversation.resolveWorkspaceRoot,
194
198
  store: reportVersionStore,
199
+ derivations: reportSession.derivations,
195
200
  threads: reportThreads,
196
201
  chrome: conversation.chrome,
197
- // The derivation tool draws the rail of the value tier: the same sandbox client. Thus a
198
- // session derivation and an extraction run on one substrate, and neither one mints a run
199
- // of the analysis.
200
- derivations: reportSession.derivations,
201
- sandboxClient: wf.dataProfile.sandboxClient,
202
202
  makeResolver: makeReportResolver,
203
203
  eyes,
204
204
  resolvePageAsset: deps.resolveReportPageAsset,
205
205
  makeSessionPages: deps.makeSessionPagePublisher,
206
206
  resolvePageUrl: deps.resolveReportPageUrl,
207
207
  provenance: deps.provenance,
208
+ syncSessionFile: deps.syncSessionFile,
208
209
  logger: conversation.logger,
209
210
  toolOutputStore,
211
+ scriptRuntime: conversation.scriptRuntime,
210
212
  });
211
213
  // The single registration point for every typed agent. A future thread type's
212
214
  // agent plugs in here at assembly with no embedder change. `conversation` is
@@ -232,8 +234,18 @@ export function assembleCoreRuntime(deps) {
232
234
  credentialFor: deps.credentialFor,
233
235
  pauseRetryDelaysMs,
234
236
  });
237
+ const runCanceler = createRunCanceler({
238
+ pool: conversation.pool,
239
+ runCharge: executeAnalysisDeps.runCharge,
240
+ runAuthorizer: conversation.runAuthorizer,
241
+ // The end of a canceled run goes where the run engine emits the rest of the run.
242
+ provenance: executeAnalysisDeps.provenance,
243
+ logger: conversation.logger,
244
+ releaseLeasesOf: (workflowIds) => leases.releaseOwnedBy(workflowIds),
245
+ });
235
246
  return {
236
247
  agents: agentResolver,
248
+ runCanceler,
237
249
  askGateway,
238
250
  chatTurns,
239
251
  work: createWork({
@@ -241,15 +253,15 @@ export function assembleCoreRuntime(deps) {
241
253
  runAuthorizer: conversation.runAuthorizer,
242
254
  logger: conversation.logger,
243
255
  stopTurn: (turnId) => chatTurns.stop(turnId),
244
- runCanceler: createRunCanceler({
245
- pool: conversation.pool,
246
- runCharge: executeAnalysisDeps.runCharge,
247
- runAuthorizer: conversation.runAuthorizer,
248
- // The end of a canceled run goes where the run engine emits the rest of the run.
249
- provenance: executeAnalysisDeps.provenance,
250
- logger: conversation.logger,
251
- }),
252
- stopProfile: (analysisId, workflowId) => stopDataProfile({ pool: conversation.pool, logger: conversation.logger, notifyDataProfileSettled: wf.dataProfile.notifyDataProfileSettled }, analysisId, workflowId),
256
+ runCanceler,
257
+ stopProfile: async (analysisId, workflowId) => {
258
+ await stopDataProfile({ pool: conversation.pool, logger: conversation.logger, notifyDataProfileSettled: wf.dataProfile.notifyDataProfileSettled }, analysisId, workflowId);
259
+ // A canceled body runs no teardown, thus the lease of the profile ends here and not at its owner check.
260
+ await leases.releaseOwnedBy([workflowId]).catch((failure) => {
261
+ const logger = conversation.logger ?? createNoopLogger();
262
+ logger.error("the sandbox lease of a stopped profile did not end", { analysisId, ...logger.errorFields(failure) });
263
+ });
264
+ },
253
265
  }),
254
266
  reportSession: { ensureSessionState: reportSession.ensureSessionState.bind(reportSession) },
255
267
  workflows: {
@@ -263,7 +275,6 @@ export function assembleCoreRuntime(deps) {
263
275
  executeAnalysisWorkflow: executeAnalysis,
264
276
  runAuthorizer: conversation.runAuthorizer,
265
277
  runLauncher: conversation.runLauncher,
266
- resourcePolicy,
267
278
  extendAnalysisFarm: conversation.extendAnalysisFarm,
268
279
  logger: conversation.logger,
269
280
  provenance: deps.provenance,
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import type { ResultAsync } from "neverthrow";
21
21
  import type { SpawnSession } from "../auth/types.js";
22
+ import type { ResourceLimits } from "../config/resource-limits.js";
22
23
  import type { SandboxError } from "./sandbox-error.js";
23
24
  import type { ExecEmit, ExecRequest, ExecResult, ManagedSandbox, SandboxIdentity, SandboxLiveness, SandboxRef, SandboxSpec, ToolchainSource } from "./types.js";
24
25
  export type SandboxClient = {
@@ -31,6 +32,8 @@ export type SandboxClient = {
31
32
  * optional field would carry two meanings, undeclared and unknown.
32
33
  */
33
34
  readonly toolchainSource: ToolchainSource;
35
+ /** The per-machine ceilings that the client clamps each spawn to. The sandbox module clamps a lease request to the same values. */
36
+ readonly resourceLimits: ResourceLimits;
34
37
  /**
35
38
  * DBOS step (`sandbox.create`) — the spawn half of the two-step create
36
39
  * (see the harness-sandbox-exec spec). Launches the sandbox-base container/Job under the
@@ -59,9 +62,8 @@ export type SandboxClient = {
59
62
  * same step gets the same id, and the sandbox gives it the result of the
60
63
  * first. Outside a workflow it throws.
61
64
  *
62
- * `execId` replaces that id. A caller that runs more than one exec in one
63
- * step, or an exec outside a workflow, gives its own id
64
- * (`sandbox/step-sandbox.ts`).
65
+ * `execId` replaces that id. The sandbox leases give an id under the salt
66
+ * of the lease (`sandbox/leases.ts`).
65
67
  *
66
68
  * A recovered step submits again under the same id. The sandbox dedups the
67
69
  * submit on the id, thus the poll attaches to the exec that already ran or
@@ -2,17 +2,11 @@
2
2
  * Backend-selected `SandboxClient` factory.
3
3
  *
4
4
  * Docker for local dev, K8s for production — the single place the
5
- * `SANDBOX_BACKEND` env decision lives. Injected at the composition root
6
- * as a construction-time dependency (see the harness-durable-runtime spec); callers do NOT import a
7
- * backend module directly.
8
- *
9
- * Registry write/clear callbacks are wired here so the per-backend
10
- * `createSandbox` / `teardown` implementations stay free of state-layer
11
- * coupling (they just call the closures we hand them). `exec` is
5
+ * `SANDBOX_BACKEND` env decision lives. The embedder makes the client and gives
6
+ * it to `assembleCoreRuntime`, and only the sandbox module calls it. `exec` is
12
7
  * backend-agnostic — it only needs the SandboxRef.
13
8
  */
14
9
  import type { V1Toleration } from "@kubernetes/client-node";
15
- import type { Querier } from "../state/db.js";
16
10
  import type { Logger } from "../lib/logger.js";
17
11
  import { type ResourceLimits } from "../config/resource-limits.js";
18
12
  import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
@@ -30,8 +24,6 @@ export type SandboxBackendConfig = {
30
24
  readonly namespace: string;
31
25
  };
32
26
  export type CreateSandboxClientConfig = {
33
- /** App pool — used for registry writes inside the create/teardown steps. */
34
- pool: Querier;
35
27
  /** Backend default + namespace fallback source. */
36
28
  env: SandboxBackendConfig;
37
29
  /** Default sandbox-base image when the workflow doesn't override per-step. */
@@ -174,6 +166,9 @@ export declare function precreateStepTree(deps: {
174
166
  resolveWorkspaceRoot: ResolveWorkspaceRoot;
175
167
  stepTreeAccess?: "world-writable";
176
168
  }, coords: MountPlanCoords): Promise<void>;
169
+ /** Pre-create the directory of the exec records of the lease, which each machine of the lease mounts read-write. */
170
+ export declare function precreateRecordDir(deps: {
171
+ resolveWorkspaceRoot: ResolveWorkspaceRoot;
172
+ stepTreeAccess?: "world-writable";
173
+ }, coords: MountPlanCoords): Promise<void>;
177
174
  export declare function createSandboxClient(config: CreateSandboxClientConfig): SandboxClient;
178
- /** Re-exports so consumers import once from this module. */
179
- export { clearSandboxRef } from "../state/index.js";
@@ -2,13 +2,8 @@
2
2
  * Backend-selected `SandboxClient` factory.
3
3
  *
4
4
  * Docker for local dev, K8s for production — the single place the
5
- * `SANDBOX_BACKEND` env decision lives. Injected at the composition root
6
- * as a construction-time dependency (see the harness-durable-runtime spec); callers do NOT import a
7
- * backend module directly.
8
- *
9
- * Registry write/clear callbacks are wired here so the per-backend
10
- * `createSandbox` / `teardown` implementations stay free of state-layer
11
- * coupling (they just call the closures we hand them). `exec` is
5
+ * `SANDBOX_BACKEND` env decision lives. The embedder makes the client and gives
6
+ * it to `assembleCoreRuntime`, and only the sandbox module calls it. `exec` is
12
7
  * backend-agnostic — it only needs the SandboxRef.
13
8
  */
14
9
  import { chmod, mkdir } from "node:fs/promises";
@@ -20,13 +15,11 @@ import { recordSandboxExec } from "../lib/metrics.js";
20
15
  import { ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
21
16
  import { clampResources } from "../config/resource-limits.js";
22
17
  import { tailWritePrefix } from "../workspace/paths.js";
23
- import { tryMutation } from "../lib/db-result.js";
24
18
  import { unwrapOrThrow } from "../lib/result.js";
25
- import { setSandboxRef } from "../state/index.js";
26
19
  import { capExecStreams, EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
27
20
  import { createDockerSandboxOps } from "./docker-client.js";
28
21
  import { runExec, stepExecId } from "./exec.js";
29
- import { noteExecOutcome, sandboxExecOutcomeOf, summarizeExec } from "./exec-outcome.js";
22
+ import { sandboxExecOutcomeOf } from "./exec-outcome.js";
30
23
  import { createK8sSandboxOps } from "./k8s-client.js";
31
24
  import { mountCoordsOf, sandboxWriteTail } from "./mount-plan.js";
32
25
  import { SandboxFailure } from "./sandbox-error.js";
@@ -65,6 +58,13 @@ export async function precreateStepTree(deps, coords) {
65
58
  await Promise.all(write.subdirs.map((sub) => chmod(join(writeDir, sub), 0o777)));
66
59
  }
67
60
  }
61
+ /** Pre-create the directory of the exec records of the lease, which each machine of the lease mounts read-write. */
62
+ export async function precreateRecordDir(deps, coords) {
63
+ const recordDir = tailWritePrefix({ workspaceRoot: await deps.resolveWorkspaceRoot(coords.analysisId), tail: coords.recordTail });
64
+ await mkdir(recordDir, { recursive: true });
65
+ if (deps.stepTreeAccess === "world-writable")
66
+ await chmod(recordDir, 0o777);
67
+ }
68
68
  export function createSandboxClient(config) {
69
69
  const backend = config.backend ?? config.env.backend;
70
70
  // Normalized once, here, and handed to the backends and to the client
@@ -100,9 +100,6 @@ export function createSandboxClient(config) {
100
100
  * unwraps below stay on the plain bridge: they carry a `DbError`.
101
101
  */
102
102
  const failing = (e) => new SandboxFailure(e);
103
- const registerSandbox = async (session, spec, ref) => {
104
- unwrapOrThrow(await setSandboxRef(config.pool, session.runFrame.runId, session.runFrame.stepId, ref));
105
- };
106
103
  const ops = backend === "k8s"
107
104
  ? createK8sSandboxOps({
108
105
  image: config.image,
@@ -120,7 +117,6 @@ export function createSandboxClient(config) {
120
117
  tolerations: config.tolerations,
121
118
  runtimeClassName: config.runtimeClassName,
122
119
  logger: config.logger,
123
- registerSandbox,
124
120
  })
125
121
  : createDockerSandboxOps({
126
122
  image: config.image,
@@ -133,27 +129,9 @@ export function createSandboxClient(config) {
133
129
  platform: config.platform,
134
130
  engineSocketPath: config.engineSocketPath,
135
131
  logger: config.logger,
136
- registerSandbox,
137
132
  });
138
- // `teardown` needs the registry-clear closure but the SandboxRef alone
139
- // doesn't carry runId/stepId. Wrap the backend impl so callers pass the
140
- // ref + step coordinates and the wrapper does the clear after the
141
- // backend-level removal.
142
133
  const teardown = async (ref) => {
143
134
  unwrapOrThrow((await ops.teardown(ref)).mapErr(failing));
144
- // Sandboxes are 1-per-step (see `sandbox-step.ts` body — each child
145
- // workflow calls `createSandbox` once then `teardown` once), so the
146
- // broad WHERE clause matches exactly one row. If that invariant ever
147
- // changes (e.g. a step reuses a sibling's sandbox), this clear must
148
- // be scoped to (run, step) to avoid wiping prior steps' provenance.
149
- unwrapOrThrow(await tryMutation("createSandbox.teardownClearSandboxRef", async () => {
150
- await config.pool.query({
151
- text: `UPDATE cortex_step_executions
152
- SET sandbox_ref = NULL
153
- WHERE sandbox_ref->>'sandboxId' = $1`,
154
- values: [ref.sandboxId],
155
- });
156
- }));
157
135
  };
158
136
  // One arbiter for both surfaces: the client's `isAlive` method and the
159
137
  // poll loop's escalation probe are the same backend inspect.
@@ -185,10 +163,12 @@ export function createSandboxClient(config) {
185
163
  // The config states an engine fact (binds preserve host ownership);
186
164
  // which remediation that fact demands — today, a world-writable step
187
165
  // write tree — is harness-owned and chosen here, not by the embedder.
188
- const precreated = await ResultAsync.fromPromise(precreateStepTree({
166
+ const treeDeps = {
189
167
  resolveWorkspaceRoot: config.resolveWorkspaceRoot,
190
168
  stepTreeAccess: config.engineBindOwnership === "host-preserved" ? "world-writable" : undefined,
191
- }, mountCoordsOf(session, spec)), createFailed("createSandbox.precreateStepTree"));
169
+ };
170
+ const coords = mountCoordsOf(session, spec);
171
+ const precreated = await ResultAsync.fromPromise(Promise.all([precreateStepTree(treeDeps, coords), precreateRecordDir(treeDeps, coords)]), createFailed("createSandbox.precreateStepTree"));
192
172
  if (precreated.isErr())
193
173
  return err(precreated.error);
194
174
  // Clamp to cluster ceilings so the pod is always quota-admissible.
@@ -197,6 +177,7 @@ export function createSandboxClient(config) {
197
177
  };
198
178
  return {
199
179
  toolchainSource,
180
+ resourceLimits: config.resourceLimits,
200
181
  createSandbox: (session, spec, identity) => new ResultAsync(createSandbox(session, spec, identity)),
201
182
  // The one place an ExecResult crosses from the wire into the process, so
202
183
  // the cap lands here rather than at each consumer. Every downstream use —
@@ -223,7 +204,6 @@ export function createSandboxClient(config) {
223
204
  peakMemoryBytes: result.usage?.peakMemoryBytes,
224
205
  cpuMillis: result.usage?.cpuMillis,
225
206
  });
226
- noteExecOutcome(ref.sandboxId, summarizeExec(result));
227
207
  return result;
228
208
  }, { name: "sandbox.exec" }),
229
209
  isAlive,
@@ -233,5 +213,3 @@ export function createSandboxClient(config) {
233
213
  listManagedSandboxes: async () => unwrapOrThrow((await ops.listManagedSandboxes()).mapErr(failing)),
234
214
  };
235
215
  }
236
- /** Re-exports so consumers import once from this module. */
237
- export { clearSandboxRef } from "../state/index.js";
@@ -143,11 +143,9 @@ export type DockerClientConfig = {
143
143
  /**
144
144
  * Optional logger so a store degradation (a resolved farm whose `inflexa.lock`
145
145
  * is absent or invalid by sandbox-create time) is observable instead of a
146
- * silent libs-mount drop. Matches the `reaper`/`watchdog` logger seam.
146
+ * silent libs-mount drop.
147
147
  */
148
148
  logger?: Logger;
149
- /** Hook called after the registry row is written. */
150
- registerSandbox: (session: SpawnSession, spec: SandboxSpec, ref: SandboxRef) => Promise<void>;
151
149
  };
152
150
  /**
153
151
  * dockerode connection options for a configured engine socket, or `undefined`
@@ -319,6 +319,7 @@ export function createDockerSandboxOps(config) {
319
319
  ...(libsMounted && farm && plan.farmPath ? [`${farm.farmPath}:${plan.farmPath}:ro`] : []),
320
320
  ...(libsMounted && cacheBindable && farm?.cachePath && plan.cachePath ? [`${farm.cachePath}:${plan.cachePath}:rw`] : []),
321
321
  ...(refsMounted && config.refStorePath ? [`${config.refStorePath}:${String(plan.refsPath)}:ro`] : []),
322
+ `${tailWritePrefix({ workspaceRoot: hostTreePath, tail: plan.recordTail })}:${plan.recordsPath}:rw`,
322
323
  ...cpuBinds,
323
324
  ];
324
325
  const createFailed = (status, cause) => ({
@@ -348,7 +349,7 @@ export function createDockerSandboxOps(config) {
348
349
  Env: env,
349
350
  User: SANDBOX_ROOT_USER,
350
351
  WorkingDir: plan.workingDir,
351
- Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.childWorkflowId }),
352
+ Labels: mergeLabels(hostLabels, { ...harnessLabels(session, sandboxId), [OWNER_WORKFLOW_LABEL]: spec.ownerWorkflowId }),
352
353
  ExposedPorts: { [`${SANDBOX_SERVER_PORT}/tcp`]: {} },
353
354
  HostConfig: {
354
355
  Binds: binds,
@@ -378,7 +379,7 @@ export function createDockerSandboxOps(config) {
378
379
  };
379
380
  if (config.platform !== undefined)
380
381
  createOpts.platform = config.platform;
381
- const sandbox = await createOrAdopt(docker, createOpts, sandboxId, spec.childWorkflowId, createFailed, logger);
382
+ const sandbox = await createOrAdopt(docker, createOpts, sandboxId, spec.ownerWorkflowId, createFailed, logger);
382
383
  if (sandbox.isErr())
383
384
  return err(sandbox.error);
384
385
  if (!sandbox.value.alreadyRunning) {
@@ -407,9 +408,6 @@ export function createDockerSandboxOps(config) {
407
408
  port,
408
409
  backend: "docker",
409
410
  };
410
- const registered = await trySandbox(() => config.registerSandbox(session, spec, ref), createFailed);
411
- if (registered.isErr())
412
- return err(registered.error);
413
411
  return ok(ref);
414
412
  })());
415
413
  },
@@ -8,13 +8,13 @@
8
8
  * keeps that text on the sandbox side of the boundary. A summary gives the
9
9
  * SIZE of stderr and never a byte of it.
10
10
  *
11
- * The summary of the last exec of a sandbox lives in one process-local cell,
12
- * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
- * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
- * body reads it without a new dependency through the agent factory. A replay
15
- * re-runs the await loop and writes the cell again, thus a recovered body
16
- * reads the summary of the same exec. The map is capped, so a sandbox whose
17
- * step never reaches the read site cannot hold memory.
11
+ * The summary of the last exec of a lease lives in one process-local cell,
12
+ * keyed by the lease key. A step holds one lease key, thus the cell is
13
+ * step-scoped in effect and the step body reads it without a new dependency
14
+ * through the agent factory. The cell keeps the summary across a machine
15
+ * replacement. A replay re-runs the await loop and writes the cell again, thus
16
+ * a recovered body reads the summary of the same exec. The map is capped, so a
17
+ * lease whose step never reaches the read site cannot hold memory.
18
18
  */
19
19
  import type { SandboxExecOutcome } from "../lib/metrics.js";
20
20
  import type { ExecResult } from "./types.js";
@@ -39,7 +39,7 @@ export type ExecOutcomeSummary = {
39
39
  };
40
40
  /** Reduce a result to its summary. */
41
41
  export declare function summarizeExec(result: ExecResult): ExecOutcomeSummary;
42
- /** Keep `summary` as the last known exec of `sandboxId`. */
43
- export declare function noteExecOutcome(sandboxId: string, summary: ExecOutcomeSummary): void;
44
- /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
45
- export declare function lastExecOutcome(sandboxId: string): ExecOutcomeSummary | undefined;
42
+ /** Keep `summary` as the last known exec of the lease `leaseKey`. */
43
+ export declare function noteExecOutcome(leaseKey: string, summary: ExecOutcomeSummary): void;
44
+ /** The last known exec of the lease `leaseKey`, or `undefined` when it ran none. */
45
+ export declare function lastExecOutcome(leaseKey: string): ExecOutcomeSummary | undefined;
@@ -8,13 +8,13 @@
8
8
  * keeps that text on the sandbox side of the boundary. A summary gives the
9
9
  * SIZE of stderr and never a byte of it.
10
10
  *
11
- * The summary of the last exec of a sandbox lives in one process-local cell,
12
- * keyed by sandbox id. A sandbox belongs to one step (see the teardown of
13
- * `create-sandbox.ts`), thus the cell is step-scoped in effect and the step
14
- * body reads it without a new dependency through the agent factory. A replay
15
- * re-runs the await loop and writes the cell again, thus a recovered body
16
- * reads the summary of the same exec. The map is capped, so a sandbox whose
17
- * step never reaches the read site cannot hold memory.
11
+ * The summary of the last exec of a lease lives in one process-local cell,
12
+ * keyed by the lease key. A step holds one lease key, thus the cell is
13
+ * step-scoped in effect and the step body reads it without a new dependency
14
+ * through the agent factory. The cell keeps the summary across a machine
15
+ * replacement. A replay re-runs the await loop and writes the cell again, thus
16
+ * a recovered body reads the summary of the same exec. The map is capped, so a
17
+ * lease whose step never reaches the read site cannot hold memory.
18
18
  */
19
19
  /** The reason that names a machine the kernel killed for its memory limit. */
20
20
  const OOM_KILLED = "sandbox-oom-killed";
@@ -41,25 +41,25 @@ export function summarizeExec(result) {
41
41
  };
42
42
  }
43
43
  /**
44
- * How many sandboxes keep a summary. A host runs far fewer sandboxes at once
45
- * than this, thus the cap only drops the summary of a step that ended long ago.
44
+ * How many leases keep a summary. A host holds far fewer leases at once than
45
+ * this, thus the cap only drops the summary of a step that ended long ago.
46
46
  */
47
- const MAX_TRACKED_SANDBOXES = 256;
48
- const lastExecBySandbox = new Map();
49
- /** Keep `summary` as the last known exec of `sandboxId`. */
50
- export function noteExecOutcome(sandboxId, summary) {
47
+ const MAX_TRACKED_LEASES = 256;
48
+ const lastExecByLease = new Map();
49
+ /** Keep `summary` as the last known exec of the lease `leaseKey`. */
50
+ export function noteExecOutcome(leaseKey, summary) {
51
51
  // Delete before set so the key moves to the end of the insertion order and
52
- // the eviction below drops the least recently written sandbox.
53
- lastExecBySandbox.delete(sandboxId);
54
- lastExecBySandbox.set(sandboxId, summary);
55
- while (lastExecBySandbox.size > MAX_TRACKED_SANDBOXES) {
56
- const oldest = lastExecBySandbox.keys().next();
52
+ // the eviction below drops the least recently written lease.
53
+ lastExecByLease.delete(leaseKey);
54
+ lastExecByLease.set(leaseKey, summary);
55
+ while (lastExecByLease.size > MAX_TRACKED_LEASES) {
56
+ const oldest = lastExecByLease.keys().next();
57
57
  if (oldest.done)
58
58
  break;
59
- lastExecBySandbox.delete(oldest.value);
59
+ lastExecByLease.delete(oldest.value);
60
60
  }
61
61
  }
62
- /** The last known exec of `sandboxId`, or `undefined` when it ran none. */
63
- export function lastExecOutcome(sandboxId) {
64
- return lastExecBySandbox.get(sandboxId);
62
+ /** The last known exec of the lease `leaseKey`, or `undefined` when it ran none. */
63
+ export function lastExecOutcome(leaseKey) {
64
+ return lastExecByLease.get(leaseKey);
65
65
  }
@@ -52,6 +52,11 @@ export type ExecDeps = {
52
52
  * makes the pair unique, thus no caller mints an exec id.
53
53
  */
54
54
  export declare function stepExecId(): string;
55
+ /**
56
+ * The exec id of a step under a salt that no machine receives. The id is the same on each run of the step, but the
57
+ * ids that a command can read in the exec records do not give the id of a later exec.
58
+ */
59
+ export declare function saltedStepExecId(salt: string): string;
55
60
  /**
56
61
  * Run one exec to its terminal result.
57
62
  *
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { errorMessageOf } from "../lib/thrown.js";
16
16
  import { textOf } from "../lib/unknown-text.js";
17
+ import { createHash } from "node:crypto";
17
18
  import { setTimeout as sleep } from "node:timers/promises";
18
19
  import { DBOS, Error as DBOSErrors, StatusString } from "@dbos-inc/dbos-sdk";
19
20
  import { untracedFetch } from "../lib/otel-spans.js";
@@ -62,6 +63,13 @@ export function stepExecId() {
62
63
  }
63
64
  return `${workflowId}:${stepId}`;
64
65
  }
66
+ /**
67
+ * The exec id of a step under a salt that no machine receives. The id is the same on each run of the step, but the
68
+ * ids that a command can read in the exec records do not give the id of a later exec.
69
+ */
70
+ export function saltedStepExecId(salt) {
71
+ return createHash("sha256").update(`${salt}:${stepExecId()}`).digest("hex");
72
+ }
65
73
  /**
66
74
  * Run one exec to its terminal result.
67
75
  *
@@ -177,7 +185,7 @@ async function pollExec(ref, execId, emit, deadlineMs, deps) {
177
185
  if (resp.cursor > cursor)
178
186
  cursor = resp.cursor;
179
187
  if (resp.result !== undefined)
180
- return resp.result;
188
+ return resp.result.lost === undefined ? resp.result : syntheticFailureResult(execId, "sandbox-replaced");
181
189
  }
182
190
  if (deps.isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
183
191
  const verdict = await probeLiveness(deps.isAlive, ref);
@@ -1,22 +1,10 @@
1
1
  /**
2
- * Sandbox identity minting — the durable half of the two-step create (ADR
3
- * 0016). Backend-agnostic: the `sandboxId` is minted here, checkpointed by the
4
- * `sandbox.mint` DBOS step, then handed to the backend's spawn step. Because the
5
- * identity is durable before any machine exists, a crash between spawn and the
6
- * spawn-step checkpoint is recoverable — the re-run adopts the existing machine
7
- * under the same name.
2
+ * The name of a sandbox machine. It comes from a key: the id of the lease workflow and the machine generation. Thus
3
+ * a rerun of the spawn step adopts the machine of the earlier attempt, and no step checkpoints a name before a spawn.
8
4
  */
9
5
  import type { SandboxIdentity } from "./types.js";
10
- /** `sbx-{run8}-{rand8}` — run prefix aids `kubectl` triage; the 8-hex random
11
- * suffix makes within-run collisions across a run's concurrent steps
12
- * negligible. Names are NOT the idempotency mechanism (the checkpoint is), and
13
- * adoption is owner-label-guarded, but a unique name keeps a collision from
14
- * ever reaching that guard. */
15
- export declare function mintSandboxIdentity(runId: string): SandboxIdentity;
16
6
  /**
17
- * `sbx-{run8}-{hash12}` — the name of a sandbox whose identity comes from a
18
- * key, not from a checkpoint. The same key gives the same name, thus a rerun
19
- * of a step adopts the machine of the earlier attempt. The key is hashed,
20
- * because a workflow id can hold characters that a container name refuses.
7
+ * `sbx-{run8}-{hash12}`. The run prefix aids `kubectl` triage. The same key gives the same name, and the key is
8
+ * hashed, because a workflow id can hold characters that a container name refuses.
21
9
  */
22
10
  export declare function keyedSandboxIdentity(runId: string, key: string): SandboxIdentity;