@wardby/cli 0.5.3 → 0.7.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 (763) hide show
  1. package/.env.example +33 -0
  2. package/README.md +25 -14
  3. package/deploy/local/docker-compose.quickstart-coding.yml +13 -0
  4. package/dist/claude-coding-worker/context.d.ts +22 -0
  5. package/dist/claude-coding-worker/context.js +92 -0
  6. package/dist/claude-coding-worker/driver.d.ts +27 -3
  7. package/dist/claude-coding-worker/driver.js +161 -20
  8. package/dist/claude-coding-worker/main.js +34 -6
  9. package/dist/claude-coding-worker/sdk.js +25 -5
  10. package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
  11. package/dist/claude-coding-worker/tool-socket.js +111 -0
  12. package/dist/claude-coding-worker/transcript.d.ts +18 -0
  13. package/dist/claude-coding-worker/transcript.js +77 -0
  14. package/dist/cli-help.d.ts +1 -1
  15. package/dist/cli-help.js +6 -2
  16. package/dist/cli-native-mode.d.ts +9 -0
  17. package/dist/cli-native-mode.js +20 -0
  18. package/dist/cli.d.ts +3 -1
  19. package/dist/cli.js +161 -11
  20. package/dist/coding/claude-context.d.ts +45 -0
  21. package/dist/coding/claude-context.js +434 -0
  22. package/dist/coding/continuation-wording.d.ts +35 -0
  23. package/dist/coding/continuation-wording.js +73 -0
  24. package/dist/coding/linked-issue-note.d.ts +23 -0
  25. package/dist/coding/linked-issue-note.js +47 -0
  26. package/dist/coding/merge-wording.d.ts +28 -0
  27. package/dist/coding/merge-wording.js +78 -0
  28. package/dist/coding/pr-description-note.d.ts +9 -0
  29. package/dist/coding/pr-description-note.js +32 -0
  30. package/dist/coding/profile.d.ts +50 -118
  31. package/dist/coding/profile.js +51 -4
  32. package/dist/coding/protected-path-wording.d.ts +8 -0
  33. package/dist/coding/protected-path-wording.js +1 -1
  34. package/dist/coding/protocol.d.ts +292 -270
  35. package/dist/coding/protocol.js +322 -15
  36. package/dist/coding/redact.d.ts +3 -0
  37. package/dist/coding/redact.js +12 -0
  38. package/dist/coding/registry/allowlist.d.ts +12 -0
  39. package/dist/coding/registry/allowlist.js +14 -1
  40. package/dist/coding/registry/audit-mode.d.ts +153 -0
  41. package/dist/coding/registry/audit-mode.js +304 -0
  42. package/dist/coding/registry/lockfile-shared.d.ts +36 -0
  43. package/dist/coding/registry/lockfile-shared.js +128 -0
  44. package/dist/coding/registry/lockfiles.js +1 -1
  45. package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
  46. package/dist/coding/registry/npm-lockfile-formats.js +41 -0
  47. package/dist/coding/registry/npm-lockfile.d.ts +8 -4
  48. package/dist/coding/registry/npm-lockfile.js +4 -6
  49. package/dist/coding/registry/npm.js +78 -8
  50. package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
  51. package/dist/coding/registry/pnpm-lockfile.js +189 -0
  52. package/dist/coding/registry/pypi-plan.d.ts +50 -0
  53. package/dist/coding/registry/pypi-plan.js +406 -0
  54. package/dist/coding/registry/pypi-requirements.d.ts +58 -0
  55. package/dist/coding/registry/pypi-requirements.js +155 -0
  56. package/dist/coding/registry/pypi.d.ts +3 -27
  57. package/dist/coding/registry/pypi.js +40 -154
  58. package/dist/coding/registry/refusal-hints.d.ts +20 -0
  59. package/dist/coding/registry/refusal-hints.js +61 -0
  60. package/dist/coding/registry/refusal-wording.d.ts +11 -0
  61. package/dist/coding/registry/refusal-wording.js +56 -0
  62. package/dist/coding/registry/report.d.ts +70 -8
  63. package/dist/coding/registry/report.js +112 -5
  64. package/dist/coding/registry/types.d.ts +58 -8
  65. package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
  66. package/dist/coding/registry/yarn-lockfile.js +367 -0
  67. package/dist/coding/services/catalog.d.ts +37 -351
  68. package/dist/coding/transcript/claude-code.d.ts +13 -0
  69. package/dist/coding/transcript/claude-code.js +273 -0
  70. package/dist/coding/transcript/download-protocol.d.ts +30 -0
  71. package/dist/coding/transcript/download-protocol.js +18 -0
  72. package/dist/coding/transcript/format.d.ts +20 -0
  73. package/dist/coding/transcript/format.js +32 -0
  74. package/dist/coding/transcript/recorder.d.ts +51 -0
  75. package/dist/coding/transcript/recorder.js +225 -0
  76. package/dist/coding/transcript/resume/checkout.d.ts +31 -0
  77. package/dist/coding/transcript/resume/checkout.js +203 -0
  78. package/dist/coding/transcript/resume/command.d.ts +17 -0
  79. package/dist/coding/transcript/resume/command.js +176 -0
  80. package/dist/coding/transcript/resume/download.d.ts +20 -0
  81. package/dist/coding/transcript/resume/download.js +153 -0
  82. package/dist/coding/transcript/resume/launch.d.ts +38 -0
  83. package/dist/coding/transcript/resume/launch.js +128 -0
  84. package/dist/coding/transcript/resume/session-file.d.ts +13 -0
  85. package/dist/coding/transcript/resume/session-file.js +79 -0
  86. package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
  87. package/dist/coding/transcript/resume/stand-in.js +49 -0
  88. package/dist/coding-worker/artifact.d.ts +16 -3
  89. package/dist/coding-worker/artifact.js +5 -3
  90. package/dist/coding-worker/debug-trace.d.ts +6 -5
  91. package/dist/coding-worker/debug-trace.js +13 -18
  92. package/dist/coding-worker/dependency-cache.d.mts +229 -0
  93. package/dist/coding-worker/dependency-cache.mjs +2123 -0
  94. package/dist/coding-worker/driver.d.ts +4 -3
  95. package/dist/coding-worker/driver.js +118 -38
  96. package/dist/coding-worker/errors.d.ts +1 -0
  97. package/dist/coding-worker/errors.js +32 -0
  98. package/dist/coding-worker/sdk.d.ts +6 -0
  99. package/dist/coding-worker/sdk.js +1 -0
  100. package/dist/coding-worker/skills.d.ts +23 -0
  101. package/dist/coding-worker/skills.js +117 -0
  102. package/dist/coding-worker/task-note.d.ts +11 -0
  103. package/dist/coding-worker/task-note.js +13 -0
  104. package/dist/coding-worker/types.d.ts +13 -0
  105. package/dist/config/dependency-cache.d.ts +23 -0
  106. package/dist/config/dependency-cache.js +32 -0
  107. package/dist/config/github-allowed-accounts.d.ts +15 -0
  108. package/dist/config/github-allowed-accounts.js +36 -0
  109. package/dist/config/low-power.d.ts +12 -0
  110. package/dist/config/low-power.js +18 -0
  111. package/dist/config/mobile.d.ts +12 -0
  112. package/dist/config/mobile.js +18 -0
  113. package/dist/config/providers.d.ts +65 -3
  114. package/dist/config/providers.js +147 -11
  115. package/dist/config/test-simulators.d.ts +82 -0
  116. package/dist/config/test-simulators.js +305 -0
  117. package/dist/core/async-context.d.ts +2 -0
  118. package/dist/core/async-context.js +12 -0
  119. package/dist/core/budget-events.d.ts +108 -0
  120. package/dist/core/budget-events.js +346 -0
  121. package/dist/core/budget-groups.d.ts +50 -1
  122. package/dist/core/budget-groups.js +236 -27
  123. package/dist/core/budget-stopped-head.d.ts +46 -0
  124. package/dist/core/budget-stopped-head.js +81 -0
  125. package/dist/core/budget-topup.d.ts +51 -0
  126. package/dist/core/budget-topup.js +156 -0
  127. package/dist/core/budget-wording.d.ts +16 -2
  128. package/dist/core/budget-wording.js +40 -2
  129. package/dist/core/coding-queue.d.ts +2 -1
  130. package/dist/core/coding-queue.js +7 -0
  131. package/dist/core/cron.d.ts +19 -1
  132. package/dist/core/cron.js +17 -0
  133. package/dist/core/datastores.js +1 -1
  134. package/dist/core/db.d.ts +16 -6
  135. package/dist/core/db.js +12 -10
  136. package/dist/core/deferred-coding.d.ts +14 -0
  137. package/dist/core/deferred-coding.js +62 -0
  138. package/dist/core/delegation-child.d.ts +25 -0
  139. package/dist/core/delegation-child.js +37 -0
  140. package/dist/core/dispatch.d.ts +53 -0
  141. package/dist/core/dispatch.js +167 -57
  142. package/dist/core/engine-native.js +10 -6
  143. package/dist/core/error-code.d.ts +38 -0
  144. package/dist/core/error-code.js +168 -0
  145. package/dist/core/grants.d.ts +19 -6
  146. package/dist/core/grants.js +55 -6
  147. package/dist/core/host-events.d.ts +67 -5
  148. package/dist/core/host-events.js +571 -37
  149. package/dist/core/host-status.d.ts +85 -3
  150. package/dist/core/host-status.js +298 -20
  151. package/dist/core/issue-bridge.d.ts +50 -6
  152. package/dist/core/issue-bridge.js +151 -20
  153. package/dist/core/issue-events.d.ts +1 -1
  154. package/dist/core/issue-events.js +16 -11
  155. package/dist/core/issue-status.d.ts +1 -1
  156. package/dist/core/issue-status.js +68 -29
  157. package/dist/core/logger.d.ts +5 -0
  158. package/dist/core/logger.js +14 -1
  159. package/dist/core/lost-run-children.d.ts +6 -0
  160. package/dist/core/lost-run-children.js +32 -0
  161. package/dist/core/mark-ready.d.ts +34 -0
  162. package/dist/core/mark-ready.js +152 -0
  163. package/dist/core/markdown-code.d.ts +8 -0
  164. package/dist/core/markdown-code.js +107 -0
  165. package/dist/core/memory-tools.js +7 -0
  166. package/dist/core/merge-order-check.d.ts +130 -0
  167. package/dist/core/merge-order-check.js +343 -0
  168. package/dist/core/notification-dispatcher.d.ts +70 -0
  169. package/dist/core/notification-dispatcher.js +368 -0
  170. package/dist/core/notification-templates.d.ts +19 -0
  171. package/dist/core/notification-templates.js +139 -0
  172. package/dist/core/notifications.d.ts +10 -0
  173. package/dist/core/notifications.js +38 -0
  174. package/dist/core/pg-auth.d.ts +22 -0
  175. package/dist/core/pg-auth.js +152 -0
  176. package/dist/core/power-composition.d.ts +16 -0
  177. package/dist/core/power-composition.js +37 -0
  178. package/dist/core/power-idle.d.ts +14 -0
  179. package/dist/core/power-idle.js +30 -0
  180. package/dist/core/power.d.ts +71 -0
  181. package/dist/core/power.js +202 -0
  182. package/dist/core/pull-request-state-sync.js +2 -2
  183. package/dist/core/reconciler.d.ts +8 -5
  184. package/dist/core/reconciler.js +37 -6
  185. package/dist/core/related-pull-requests.d.ts +59 -7
  186. package/dist/core/related-pull-requests.js +140 -25
  187. package/dist/core/reply-parts.d.ts +21 -0
  188. package/dist/core/reply-parts.js +73 -0
  189. package/dist/core/repo-access.d.ts +15 -0
  190. package/dist/core/repo-access.js +66 -0
  191. package/dist/core/review-fix.d.ts +38 -1
  192. package/dist/core/review-fix.js +209 -7
  193. package/dist/core/review-host-checks.d.ts +1 -1
  194. package/dist/core/review-host-tools.d.ts +23 -0
  195. package/dist/core/review-host-tools.js +156 -14
  196. package/dist/core/review-reads.d.ts +55 -0
  197. package/dist/core/review-reads.js +233 -0
  198. package/dist/core/run-status.d.ts +12 -0
  199. package/dist/core/run-status.js +12 -0
  200. package/dist/core/runner.d.ts +384 -5
  201. package/dist/core/runner.js +1244 -372
  202. package/dist/core/schedule-start.d.ts +12 -0
  203. package/dist/core/schedule-start.js +14 -0
  204. package/dist/core/secret-broker-config.d.ts +39 -0
  205. package/dist/core/secret-broker-config.js +112 -0
  206. package/dist/core/secret-guard.d.ts +46 -0
  207. package/dist/core/secret-guard.js +104 -0
  208. package/dist/core/secret-guarded-stores.d.ts +12 -0
  209. package/dist/core/secret-guarded-stores.js +41 -0
  210. package/dist/core/secret-spans.d.ts +50 -0
  211. package/dist/core/secret-spans.js +94 -0
  212. package/dist/core/secrets.d.ts +39 -4
  213. package/dist/core/secrets.js +148 -40
  214. package/dist/core/stop-run.d.ts +46 -0
  215. package/dist/core/stop-run.js +61 -0
  216. package/dist/core/workflow-event-recorder.d.ts +14 -0
  217. package/dist/core/workflow-event-recorder.js +120 -0
  218. package/dist/core/workflow-events.d.ts +94 -0
  219. package/dist/core/workflow-events.js +48 -0
  220. package/dist/core/workflow-run-events.d.ts +18 -0
  221. package/dist/core/workflow-run-events.js +75 -0
  222. package/dist/core/zod-json-schema.d.ts +4 -0
  223. package/dist/core/zod-json-schema.js +39 -0
  224. package/dist/core/zod3-window.d.ts +35 -0
  225. package/dist/core/zod3-window.js +124 -0
  226. package/dist/generated/prisma/browser.d.ts +92 -3
  227. package/dist/generated/prisma/client.d.ts +92 -3
  228. package/dist/generated/prisma/commonInputTypes.d.ts +364 -54
  229. package/dist/generated/prisma/enums.d.ts +37 -0
  230. package/dist/generated/prisma/enums.js +31 -0
  231. package/dist/generated/prisma/internal/class.d.ts +132 -0
  232. package/dist/generated/prisma/internal/class.js +4 -4
  233. package/dist/generated/prisma/internal/prismaNamespace.d.ts +1332 -142
  234. package/dist/generated/prisma/internal/prismaNamespace.js +227 -5
  235. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +234 -0
  236. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +227 -5
  237. package/dist/generated/prisma/models/Agent.d.ts +464 -7
  238. package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
  239. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +301 -1
  240. package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
  241. package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
  242. package/dist/generated/prisma/models/CodingRun.d.ts +1841 -1
  243. package/dist/generated/prisma/models/DeferredReview.d.ts +78 -1
  244. package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
  245. package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
  246. package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
  247. package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
  248. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +1713 -0
  249. package/dist/generated/prisma/models/NativeGatewayCall.js +1 -0
  250. package/dist/generated/prisma/models/NativeGatewaySession.d.ts +1551 -0
  251. package/dist/generated/prisma/models/NativeGatewaySession.js +1 -0
  252. package/dist/generated/prisma/models/NativeWarmWorker.d.ts +1296 -0
  253. package/dist/generated/prisma/models/NativeWarmWorker.js +1 -0
  254. package/dist/generated/prisma/models/NotificationChannel.d.ts +1543 -0
  255. package/dist/generated/prisma/models/NotificationChannel.js +1 -0
  256. package/dist/generated/prisma/models/NotificationDelivery.d.ts +1502 -0
  257. package/dist/generated/prisma/models/NotificationDelivery.js +1 -0
  258. package/dist/generated/prisma/models/NotificationThread.d.ts +1090 -0
  259. package/dist/generated/prisma/models/NotificationThread.js +1 -0
  260. package/dist/generated/prisma/models/RegistryAllowance.d.ts +41 -1
  261. package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
  262. package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
  263. package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
  264. package/dist/generated/prisma/models/Run.d.ts +1619 -58
  265. package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
  266. package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
  267. package/dist/generated/prisma/models/RunHostCheck.d.ts +36 -1
  268. package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
  269. package/dist/generated/prisma/models/RunTranscript.js +1 -0
  270. package/dist/generated/prisma/models/Secret.d.ts +37 -1
  271. package/dist/generated/prisma/models/SecretBrokerChange.d.ts +1101 -0
  272. package/dist/generated/prisma/models/SecretBrokerChange.js +1 -0
  273. package/dist/generated/prisma/models/WorkflowEvent.d.ts +1422 -0
  274. package/dist/generated/prisma/models/WorkflowEvent.js +1 -0
  275. package/dist/generated/prisma/models.d.ts +12 -0
  276. package/dist/help/build.js +4 -2
  277. package/dist/help/catalog.d.ts +1 -1
  278. package/dist/help/catalog.js +31 -6
  279. package/dist/help-index.json +2584 -146
  280. package/dist/import/create.js +49 -3
  281. package/dist/import/neutral-schema.d.ts +50 -277
  282. package/dist/import/neutral-schema.js +7 -4
  283. package/dist/knowledge/check.d.ts +1 -1
  284. package/dist/knowledge/cli.d.ts +11 -1
  285. package/dist/knowledge/cli.js +37 -4
  286. package/dist/knowledge/concept.d.ts +3 -19
  287. package/dist/knowledge/location.d.ts +31 -0
  288. package/dist/knowledge/location.js +78 -0
  289. package/dist/mcp/auth/access.d.ts +11 -2
  290. package/dist/mcp/auth/access.js +15 -1
  291. package/dist/mcp/auth/ownership.d.ts +7 -7
  292. package/dist/mcp/auth/repo-authorization.js +14 -0
  293. package/dist/mcp/auth/resource-server.d.ts +2 -1
  294. package/dist/mcp/auth/resource-server.js +7 -2
  295. package/dist/mcp/auth/self-hosted/browser.d.ts +1 -1
  296. package/dist/mcp/auth/self-hosted/browser.js +19 -6
  297. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  298. package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
  299. package/dist/mcp/context.d.ts +6 -0
  300. package/dist/mcp/host-events/github-ingress.d.ts +25 -1
  301. package/dist/mcp/host-events/github-ingress.js +70 -1
  302. package/dist/mcp/index.d.ts +19 -0
  303. package/dist/mcp/index.js +117 -20
  304. package/dist/mcp/server.d.ts +13 -0
  305. package/dist/mcp/server.js +33 -14
  306. package/dist/mcp/tools/agents.d.ts +4 -0
  307. package/dist/mcp/tools/agents.js +277 -24
  308. package/dist/mcp/tools/datastore.js +17 -8
  309. package/dist/mcp/tools/grants.js +9 -3
  310. package/dist/mcp/tools/memory.js +11 -1
  311. package/dist/mcp/tools/notification-channels.d.ts +2 -0
  312. package/dist/mcp/tools/notification-channels.js +293 -0
  313. package/dist/mcp/tools/packages.d.ts +2 -0
  314. package/dist/mcp/tools/packages.js +195 -0
  315. package/dist/mcp/tools/power.d.ts +2 -0
  316. package/dist/mcp/tools/power.js +77 -0
  317. package/dist/mcp/tools/repositories.js +18 -5
  318. package/dist/mcp/tools/run-access.d.ts +16 -0
  319. package/dist/mcp/tools/run-access.js +25 -0
  320. package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
  321. package/dist/mcp/tools/run-transcript-download.js +129 -0
  322. package/dist/mcp/tools/run-transcript.d.ts +19 -0
  323. package/dist/mcp/tools/run-transcript.js +70 -0
  324. package/dist/mcp/tools/runs.d.ts +0 -6
  325. package/dist/mcp/tools/runs.js +41 -14
  326. package/dist/mcp/tools/scheduling.js +3 -1
  327. package/dist/mcp/tools/secret-broker-compat.d.ts +12 -0
  328. package/dist/mcp/tools/secret-broker-compat.js +23 -0
  329. package/dist/mcp/tools/secret-elicitation-form.d.ts +22 -2
  330. package/dist/mcp/tools/secret-elicitation-form.js +205 -10
  331. package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
  332. package/dist/mcp/tools/secret-elicitation-server.js +10 -54
  333. package/dist/mcp/tools/secret-elicitation.d.ts +38 -3
  334. package/dist/mcp/tools/secret-elicitation.js +110 -12
  335. package/dist/mcp/tools/secrets.js +186 -9
  336. package/dist/mcp/tools/stop-run.d.ts +2 -0
  337. package/dist/mcp/tools/stop-run.js +82 -0
  338. package/dist/mcp/tools/subagents.js +2 -2
  339. package/dist/mcp/tools/tools.d.ts +2 -2
  340. package/dist/mcp/tools/tools.js +47 -11
  341. package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
  342. package/dist/mcp/transport/stdio-link-host.js +67 -0
  343. package/dist/mcp/transport/streamable-http.d.ts +13 -0
  344. package/dist/mcp/transport/streamable-http.js +41 -1
  345. package/dist/native-worker/composition.d.ts +23 -0
  346. package/dist/native-worker/composition.js +77 -0
  347. package/dist/native-worker/docker-isolation.d.ts +65 -0
  348. package/dist/native-worker/docker-isolation.js +183 -0
  349. package/dist/native-worker/docker-launcher.d.ts +110 -0
  350. package/dist/native-worker/docker-launcher.js +297 -0
  351. package/dist/native-worker/gateway-server.d.ts +77 -0
  352. package/dist/native-worker/gateway-server.js +412 -0
  353. package/dist/native-worker/gateway.d.ts +70 -0
  354. package/dist/native-worker/gateway.js +263 -0
  355. package/dist/native-worker/http-server.d.ts +24 -0
  356. package/dist/native-worker/http-server.js +113 -0
  357. package/dist/native-worker/http-transport.d.ts +17 -0
  358. package/dist/native-worker/http-transport.js +88 -0
  359. package/dist/native-worker/kubernetes-isolation.d.ts +90 -0
  360. package/dist/native-worker/kubernetes-isolation.js +218 -0
  361. package/dist/native-worker/kubernetes-launcher.d.ts +106 -0
  362. package/dist/native-worker/kubernetes-launcher.js +375 -0
  363. package/dist/native-worker/launch.d.ts +62 -0
  364. package/dist/native-worker/launch.js +49 -0
  365. package/dist/native-worker/ledger.d.ts +125 -0
  366. package/dist/native-worker/ledger.js +245 -0
  367. package/dist/native-worker/loopback.d.ts +12 -0
  368. package/dist/native-worker/loopback.js +44 -0
  369. package/dist/native-worker/main.d.ts +8 -0
  370. package/dist/native-worker/main.js +85 -0
  371. package/dist/native-worker/protocol.d.ts +296 -0
  372. package/dist/native-worker/protocol.js +246 -0
  373. package/dist/native-worker/sandbox-executor.d.ts +83 -0
  374. package/dist/native-worker/sandbox-executor.js +284 -0
  375. package/dist/native-worker/stderr-redaction.d.ts +77 -0
  376. package/dist/native-worker/stderr-redaction.js +237 -0
  377. package/dist/native-worker/stdio.d.ts +29 -0
  378. package/dist/native-worker/stdio.js +189 -0
  379. package/dist/native-worker/warm-delivery.d.ts +16 -0
  380. package/dist/native-worker/warm-delivery.js +33 -0
  381. package/dist/native-worker/warm-pool-ledger.d.ts +39 -0
  382. package/dist/native-worker/warm-pool-ledger.js +73 -0
  383. package/dist/native-worker/warm-pool.d.ts +107 -0
  384. package/dist/native-worker/warm-pool.js +283 -0
  385. package/dist/native-worker/worker.d.ts +17 -0
  386. package/dist/native-worker/worker.js +141 -0
  387. package/dist/observability/metrics.js +6 -1
  388. package/dist/providers/auth/authorization-server.d.ts +3 -0
  389. package/dist/providers/auth/index.js +2 -0
  390. package/dist/providers/auth/self-hosted.d.ts +14 -2
  391. package/dist/providers/auth/self-hosted.js +54 -4
  392. package/dist/providers/chat/fake.d.ts +46 -0
  393. package/dist/providers/chat/fake.js +39 -0
  394. package/dist/providers/chat/index.d.ts +4 -0
  395. package/dist/providers/chat/index.js +8 -0
  396. package/dist/providers/chat/slack.d.ts +37 -0
  397. package/dist/providers/chat/slack.js +105 -0
  398. package/dist/providers/chat/types.d.ts +42 -0
  399. package/dist/providers/chat/types.js +19 -0
  400. package/dist/providers/coding-proxy/dependency-cache/eviction.d.ts +29 -0
  401. package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
  402. package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
  403. package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
  404. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
  405. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
  406. package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
  407. package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
  408. package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
  409. package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
  410. package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
  411. package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
  412. package/dist/providers/coding-proxy/hold.d.ts +32 -0
  413. package/dist/providers/coding-proxy/hold.js +86 -0
  414. package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
  415. package/dist/providers/coding-proxy/interrupted.js +104 -0
  416. package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
  417. package/dist/providers/coding-proxy/memory-ledger.js +84 -8
  418. package/dist/providers/coding-proxy/metering.d.ts +16 -0
  419. package/dist/providers/coding-proxy/metering.js +27 -4
  420. package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
  421. package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
  422. package/dist/providers/coding-proxy/proxy.d.ts +14 -1
  423. package/dist/providers/coding-proxy/proxy.js +166 -14
  424. package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
  425. package/dist/providers/coding-proxy/registry/audit.js +94 -17
  426. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
  427. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
  428. package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
  429. package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
  430. package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
  431. package/dist/providers/coding-proxy/registry/plan.js +201 -48
  432. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
  433. package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
  434. package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
  435. package/dist/providers/coding-proxy/registry/service.js +579 -112
  436. package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
  437. package/dist/providers/coding-proxy/registry/store.js +54 -1
  438. package/dist/providers/coding-proxy/runtime.d.ts +4 -0
  439. package/dist/providers/coding-proxy/runtime.js +78 -16
  440. package/dist/providers/coding-proxy/server.d.ts +6 -1
  441. package/dist/providers/coding-proxy/server.js +45 -1
  442. package/dist/providers/coding-proxy/types.d.ts +65 -1
  443. package/dist/providers/datastore/types.d.ts +5 -0
  444. package/dist/providers/engine/types.d.ts +36 -2
  445. package/dist/providers/engine/types.js +39 -0
  446. package/dist/providers/executor/composition.d.ts +31 -1
  447. package/dist/providers/executor/composition.js +81 -8
  448. package/dist/providers/executor/container.d.ts +385 -9
  449. package/dist/providers/executor/container.js +1234 -99
  450. package/dist/providers/executor/dbos-status.d.ts +31 -0
  451. package/dist/providers/executor/dbos-status.js +64 -0
  452. package/dist/providers/executor/dbos.d.ts +91 -0
  453. package/dist/providers/executor/dbos.js +319 -26
  454. package/dist/providers/executor/deferred.d.ts +44 -0
  455. package/dist/providers/executor/deferred.js +100 -0
  456. package/dist/providers/executor/in-process.d.ts +9 -1
  457. package/dist/providers/executor/in-process.js +14 -4
  458. package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
  459. package/dist/providers/executor/linked-issue-reader.js +30 -0
  460. package/dist/providers/executor/routing.d.ts +39 -10
  461. package/dist/providers/executor/routing.js +77 -32
  462. package/dist/providers/executor/types.d.ts +25 -0
  463. package/dist/providers/index.d.ts +2 -1
  464. package/dist/providers/issue-tracker/types.d.ts +17 -0
  465. package/dist/providers/issue-tracker/types.js +39 -1
  466. package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
  467. package/dist/providers/jobs/claude-tool-setup.js +5 -2
  468. package/dist/providers/jobs/docker-isolation.d.ts +8 -0
  469. package/dist/providers/jobs/docker-isolation.js +17 -0
  470. package/dist/providers/jobs/docker.d.ts +1 -0
  471. package/dist/providers/jobs/docker.js +29 -4
  472. package/dist/providers/jobs/fake-kubernetes-api.d.ts +23 -2
  473. package/dist/providers/jobs/fake-kubernetes-api.js +45 -1
  474. package/dist/providers/jobs/kubernetes-api.d.ts +25 -1
  475. package/dist/providers/jobs/kubernetes-api.js +17 -0
  476. package/dist/providers/jobs/kubernetes-client.d.ts +10 -1
  477. package/dist/providers/jobs/kubernetes-client.js +61 -3
  478. package/dist/providers/jobs/kubernetes-dry-run-fixture.d.ts +6 -1
  479. package/dist/providers/jobs/kubernetes-dry-run-fixture.js +6 -3
  480. package/dist/providers/jobs/kubernetes-isolation.d.ts +5 -1
  481. package/dist/providers/jobs/kubernetes-isolation.js +23 -1
  482. package/dist/providers/jobs/kubernetes-platform.d.ts +4 -1
  483. package/dist/providers/jobs/kubernetes-platform.js +38 -1
  484. package/dist/providers/jobs/kubernetes-preflight.d.ts +2 -1
  485. package/dist/providers/jobs/kubernetes-preflight.js +4 -2
  486. package/dist/providers/jobs/kubernetes.d.ts +42 -2
  487. package/dist/providers/jobs/kubernetes.js +178 -19
  488. package/dist/providers/jobs/metadata-probes.d.ts +11 -0
  489. package/dist/providers/jobs/metadata-probes.js +13 -0
  490. package/dist/providers/jobs/types.d.ts +42 -0
  491. package/dist/providers/jobs/types.js +13 -1
  492. package/dist/providers/llm/anthropic.js +5 -1
  493. package/dist/providers/llm/bedrock.d.ts +0 -12
  494. package/dist/providers/llm/bedrock.js +4 -0
  495. package/dist/providers/llm/claude-messages.d.ts +3 -1
  496. package/dist/providers/llm/claude-messages.js +4 -1
  497. package/dist/providers/llm/claude-provider.d.ts +2 -2
  498. package/dist/providers/llm/claude-provider.js +2 -2
  499. package/dist/providers/llm/openai.d.ts +2 -2
  500. package/dist/providers/llm/openai.js +6 -2
  501. package/dist/providers/llm/routing.d.ts +2 -2
  502. package/dist/providers/llm/routing.js +2 -2
  503. package/dist/providers/llm/types.d.ts +12 -2
  504. package/dist/providers/review-host/github-events.d.ts +9 -1
  505. package/dist/providers/review-host/github-events.js +34 -12
  506. package/dist/providers/review-host/github.d.ts +39 -2
  507. package/dist/providers/review-host/github.js +274 -29
  508. package/dist/providers/review-host/local.d.ts +8 -4
  509. package/dist/providers/review-host/local.js +49 -31
  510. package/dist/providers/review-host/patch-pages.d.ts +41 -0
  511. package/dist/providers/review-host/patch-pages.js +176 -0
  512. package/dist/providers/review-host/review-format.d.ts +19 -1
  513. package/dist/providers/review-host/review-format.js +38 -7
  514. package/dist/providers/review-host/types.d.ts +157 -2
  515. package/dist/providers/review-host/types.js +8 -0
  516. package/dist/providers/storage/gcs.d.ts +63 -0
  517. package/dist/providers/storage/gcs.js +69 -0
  518. package/dist/providers/storage/index.d.ts +15 -0
  519. package/dist/providers/storage/index.js +14 -0
  520. package/dist/providers/storage/local.d.ts +24 -0
  521. package/dist/providers/storage/local.js +92 -0
  522. package/dist/providers/storage/meter.d.ts +14 -0
  523. package/dist/providers/storage/meter.js +34 -0
  524. package/dist/providers/storage/s3.d.ts +41 -0
  525. package/dist/providers/storage/s3.js +107 -0
  526. package/dist/providers/storage/types.d.ts +37 -3
  527. package/dist/providers/storage/types.js +21 -4
  528. package/dist/providers/vcs/git.d.ts +33 -2
  529. package/dist/providers/vcs/git.js +324 -22
  530. package/dist/providers/vcs/github-remote.d.ts +26 -3
  531. package/dist/providers/vcs/github-remote.js +124 -14
  532. package/dist/providers/vcs/github.d.ts +246 -12
  533. package/dist/providers/vcs/github.js +521 -61
  534. package/dist/providers/vcs/index.js +8 -1
  535. package/dist/providers/vcs/pr-description.d.ts +127 -0
  536. package/dist/providers/vcs/pr-description.js +270 -0
  537. package/dist/providers/vcs/remote.d.ts +33 -1
  538. package/dist/providers/vcs/routing.d.ts +4 -2
  539. package/dist/providers/vcs/routing.js +6 -0
  540. package/dist/providers/vcs/types.d.ts +80 -3
  541. package/dist/quickstart/coding-seed.d.ts +21 -3
  542. package/dist/quickstart/coding-seed.js +283 -3
  543. package/dist/quickstart/images.d.ts +2 -0
  544. package/dist/quickstart/images.js +7 -1
  545. package/dist/quickstart/reviewer-prompt.d.ts +1 -1
  546. package/dist/quickstart/reviewer-prompt.js +6 -6
  547. package/dist/quickstart-images.json +1 -1
  548. package/dist/sandbox/brokered-fetch.d.ts +23 -0
  549. package/dist/sandbox/brokered-fetch.js +37 -0
  550. package/dist/sandbox/generated/zod.bundle.js +62 -1
  551. package/dist/sandbox/generated/zod3.bundle.js +5 -0
  552. package/dist/sandbox/host-functions.d.ts +35 -0
  553. package/dist/sandbox/host-functions.js +150 -91
  554. package/dist/sandbox/prelude.js +7 -1
  555. package/dist/sandbox/run-in-sandbox.d.ts +19 -0
  556. package/dist/sandbox/run-in-sandbox.js +28 -10
  557. package/dist/sandbox/safe-fetch.d.ts +2 -0
  558. package/dist/sandbox/safe-fetch.js +3 -1
  559. package/dist/sandbox/secret-broker-sigv4.d.ts +2 -0
  560. package/dist/sandbox/secret-broker-sigv4.js +49 -0
  561. package/dist/sandbox/secret-broker.d.ts +34 -0
  562. package/dist/sandbox/secret-broker.js +161 -0
  563. package/dist/sandbox/tool-capabilities.d.ts +10 -30
  564. package/dist/sandbox/user-tool.d.ts +14 -0
  565. package/dist/sandbox/user-tool.js +32 -0
  566. package/dist/sandbox/zod-params.d.ts +37 -5
  567. package/dist/sandbox/zod-params.js +181 -26
  568. package/dist/serve.js +25 -0
  569. package/dist/viewer/api-schema.d.ts +372 -2454
  570. package/dist/viewer/api-schema.js +29 -0
  571. package/dist/viewer/build-schemas.js +10 -3
  572. package/dist/viewer/event-bus.d.ts +6 -0
  573. package/dist/viewer/event-bus.js +16 -7
  574. package/dist/viewer/graph.d.ts +5 -2
  575. package/dist/viewer/graph.js +7 -2
  576. package/dist/viewer/http.d.ts +0 -1
  577. package/dist/viewer/http.js +2 -2
  578. package/dist/viewer/infra.js +25 -2
  579. package/dist/viewer/limits.d.ts +4 -0
  580. package/dist/viewer/limits.js +4 -0
  581. package/dist/viewer/openapi.d.ts +15 -0
  582. package/dist/viewer/openapi.js +170 -0
  583. package/dist/wardby-bin.js +10 -0
  584. package/docs/README.md +7 -0
  585. package/docs/agent-recipes.md +114 -27
  586. package/docs/architecture-runtime.md +16 -1
  587. package/docs/code-review-agents.md +689 -45
  588. package/docs/coding-agent-setup.md +602 -12
  589. package/docs/coding-packages.md +1037 -92
  590. package/docs/coding-services.md +30 -13
  591. package/docs/coding-worker-byo-images.md +218 -6
  592. package/docs/coding-worker-isolation.md +173 -44
  593. package/docs/getting-started-gke.md +332 -129
  594. package/docs/getting-started-identity-provider.md +31 -11
  595. package/docs/getting-started.md +14 -4
  596. package/docs/jira-agents.md +22 -9
  597. package/docs/knowledge/blob-not-found-semantics.md +41 -0
  598. package/docs/knowledge/bounded-file-descriptor.md +52 -0
  599. package/docs/knowledge/delegation-admission-slots.md +52 -0
  600. package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
  601. package/docs/knowledge/import-rename-target.md +52 -0
  602. package/docs/knowledge/index.md +57 -0
  603. package/docs/knowledge/log.md +9 -0
  604. package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
  605. package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
  606. package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
  607. package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
  608. package/docs/knowledge/overview-cli.md +74 -0
  609. package/docs/knowledge/overview-coding-proxy.md +72 -0
  610. package/docs/knowledge/overview-coding.md +72 -0
  611. package/docs/knowledge/overview-config.md +60 -0
  612. package/docs/knowledge/overview-core.md +72 -0
  613. package/docs/knowledge/overview-deploy.md +83 -0
  614. package/docs/knowledge/overview-mcp.md +61 -0
  615. package/docs/knowledge/overview-prisma.md +72 -0
  616. package/docs/knowledge/overview-providers.md +89 -0
  617. package/docs/knowledge/overview-sandbox.md +83 -0
  618. package/docs/knowledge/overview-surfaces.md +83 -0
  619. package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
  620. package/docs/knowledge/proxy-settlement-projections.md +41 -0
  621. package/docs/knowledge/run-pricing-first-writer.md +43 -0
  622. package/docs/knowledge/schedule-claim-transaction.md +41 -0
  623. package/docs/knowledge/secret-entry-replica-contract.md +52 -0
  624. package/docs/knowledge/terminal-run-first-writer.md +41 -0
  625. package/docs/knowledge/transactional-agent-authorization.md +52 -0
  626. package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
  627. package/docs/knowledge/warm-input-publication.md +30 -0
  628. package/docs/knowledge/workspace-staging-failure.md +54 -0
  629. package/docs/knowledge.md +54 -14
  630. package/docs/native-sandbox.md +731 -0
  631. package/docs/observability.md +4 -0
  632. package/docs/release-verification.md +9 -0
  633. package/docs/security-deployment.md +146 -14
  634. package/docs/slack-notifications.md +268 -0
  635. package/docs/tool-secrets.md +284 -0
  636. package/docs/viewer-api.md +46 -5
  637. package/docs/viewer-api.openapi.yml +1404 -0
  638. package/help/admin-viewer.md +29 -3
  639. package/help/agent-recipes.md +27 -0
  640. package/help/architecture-agent.md +11 -6
  641. package/help/brokered-secrets.md +79 -0
  642. package/help/build-claude-code-image.md +131 -0
  643. package/help/build-worker-image.md +6 -7
  644. package/help/builder-agent.md +19 -6
  645. package/help/code-review-agents.md +243 -10
  646. package/help/coding-packages.md +156 -6
  647. package/help/cost-attribution.md +9 -5
  648. package/help/creating-agents.md +14 -1
  649. package/help/dependency-cache.md +143 -0
  650. package/help/deploy-gke.md +23 -9
  651. package/help/deployment-targets.md +25 -14
  652. package/help/errors/budget-exhausted.md +187 -0
  653. package/help/errors/budget-group-exhausted.md +5 -1
  654. package/help/errors/coding-provider-not-configured.md +2 -2
  655. package/help/errors/coding-stream-quota-exceeded.md +33 -0
  656. package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
  657. package/help/errors/continuation-closed.md +9 -12
  658. package/help/errors/database-auth-invalid.md +28 -0
  659. package/help/errors/delegation-conflict.md +24 -0
  660. package/help/errors/file-list-truncated.md +45 -0
  661. package/help/errors/full-read-required.md +49 -0
  662. package/help/errors/github-account-not-allowed.md +29 -0
  663. package/help/errors/graph-incomplete.md +43 -0
  664. package/help/errors/graph-limit.md +43 -0
  665. package/help/errors/low-power-disabled.md +29 -0
  666. package/help/errors/low-power-scale-forbidden.md +35 -0
  667. package/help/errors/low-power-wake-timeout.md +30 -0
  668. package/help/errors/merge-conflict.md +49 -0
  669. package/help/errors/mobile-disabled.md +31 -0
  670. package/help/errors/native-sandbox-capacity.md +22 -0
  671. package/help/errors/native-sandbox-deadline-exceeded.md +19 -0
  672. package/help/errors/native-sandbox-docker-failed.md +20 -0
  673. package/help/errors/native-sandbox-gateway-error.md +20 -0
  674. package/help/errors/native-sandbox-gateway-unavailable.md +21 -0
  675. package/help/errors/native-sandbox-image-not-pinned.md +19 -0
  676. package/help/errors/native-sandbox-isolation-mismatch.md +20 -0
  677. package/help/errors/native-sandbox-launch-abandoned.md +21 -0
  678. package/help/errors/native-sandbox-network-unenforced.md +22 -0
  679. package/help/errors/native-sandbox-requires-catalog.md +31 -0
  680. package/help/errors/native-sandbox-unavailable.md +19 -0
  681. package/help/errors/native-sandbox-warm-delivery-failed.md +21 -0
  682. package/help/errors/native-sandbox-worker-exited.md +21 -0
  683. package/help/errors/native-sandbox-worker-lost.md +20 -0
  684. package/help/errors/native-sandbox-worker-unready.md +22 -0
  685. package/help/errors/package-not-allowed-pending.md +52 -0
  686. package/help/errors/package-not-allowed.md +51 -0
  687. package/help/errors/package-refusals.md +43 -0
  688. package/help/errors/patch-cursor-malformed.md +31 -0
  689. package/help/errors/run-lost.md +39 -0
  690. package/help/errors/run-transcript-download.md +44 -0
  691. package/help/errors/secret-broker-body-invalid.md +26 -0
  692. package/help/errors/secret-broker-changed.md +20 -0
  693. package/help/errors/secret-broker-config-invalid.md +30 -0
  694. package/help/errors/secret-broker-conflict.md +27 -0
  695. package/help/errors/secret-broker-destination-denied.md +28 -0
  696. package/help/errors/secret-broker-value-invalid.md +28 -0
  697. package/help/errors/secret-brokered.md +24 -0
  698. package/help/errors/secret-in-value.md +69 -0
  699. package/help/errors/secret-not-brokered.md +25 -0
  700. package/help/errors/slack-auth-failed.md +48 -0
  701. package/help/errors/slack-channel-unreachable.md +39 -0
  702. package/help/errors/slack-not-configured.md +33 -0
  703. package/help/errors/target-merge.md +40 -0
  704. package/help/fix-merge-conflicts.md +73 -0
  705. package/help/getting-started.md +4 -5
  706. package/help/github-app-setup.md +132 -0
  707. package/help/github.md +40 -1
  708. package/help/identity-and-access.md +7 -1
  709. package/help/jira.md +23 -6
  710. package/help/knowledge.md +16 -7
  711. package/help/local-repositories.md +3 -4
  712. package/help/low-power-mode.md +131 -0
  713. package/help/merge-order-check.md +122 -0
  714. package/help/mobile-app.md +54 -0
  715. package/help/native-capabilities.md +29 -1
  716. package/help/native-sandbox.md +150 -0
  717. package/help/observability.md +8 -0
  718. package/help/operating-agents.md +5 -0
  719. package/help/package-audit-mode.md +91 -0
  720. package/help/related-pull-requests.md +80 -16
  721. package/help/repo-instructions-and-skills.md +91 -0
  722. package/help/resume-runs-locally.md +131 -0
  723. package/help/review-fix-rounds.md +43 -1
  724. package/help/sharing-agents.md +59 -0
  725. package/help/slack-notifications.md +76 -0
  726. package/help/status-comments.md +31 -0
  727. package/help/stop-run.md +58 -0
  728. package/help/troubleshooting/budgets.md +28 -0
  729. package/help/troubleshooting/coding-workers.md +99 -7
  730. package/package.json +27 -13
  731. package/prisma/migrations/20261009000003_native_execution_mode/migration.sql +12 -0
  732. package/prisma/migrations/20261009000004_native_gateway_ledger/migration.sql +65 -0
  733. package/prisma/migrations/20261009000005_native_gateway_call_child_agent/migration.sql +5 -0
  734. package/prisma/migrations/20261009000006_run_deferred_start_stop/migration.sql +14 -0
  735. package/prisma/migrations/20261009000007_native_gateway_network_ready/migration.sql +5 -0
  736. package/prisma/migrations/20261010000001_workflow_notifications/migration.sql +99 -0
  737. package/prisma/migrations/20261011000001_native_gateway_session_status_index/migration.sql +2 -0
  738. package/prisma/migrations/20261012000001_native_warm_worker/migration.sql +32 -0
  739. package/prisma/migrations/20261013000001_coding_run_merge_order/migration.sql +2 -0
  740. package/prisma/migrations/20261013000001_secret_broker/migration.sql +17 -0
  741. package/prisma/migrations/20261013000002_deferred_review_request/migration.sql +4 -0
  742. package/prisma/migrations/20261014000000_repo_skills/migration.sql +15 -0
  743. package/prisma/migrations/20261015000001_coding_run_merge_order_index/migration.sql +2 -0
  744. package/prisma/migrations/20261015000002_no_change_rereview/migration.sql +2 -0
  745. package/prisma/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
  746. package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
  747. package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
  748. package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
  749. package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
  750. package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
  751. package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
  752. package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
  753. package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
  754. package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
  755. package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
  756. package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
  757. package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
  758. package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
  759. package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
  760. package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
  761. package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
  762. package/prisma/schema.prisma +631 -146
  763. package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
@@ -16,17 +16,26 @@
16
16
  * down) lives inside the engine now — there is exactly one place that
17
17
  * reasons about cost, not one here plus one in the engine.
18
18
  */
19
+ import { createHash, randomBytes } from "node:crypto";
19
20
  import { z } from "zod";
20
- import { runStepInline } from "../providers/engine/types.js";
21
+ import { Prisma } from "#prisma";
22
+ import { RunOwnershipLostError, STEPS_CHANGED_REASON, StepReplayMismatchError, runStepInline, } from "../providers/engine/types.js";
21
23
  import { isLlmEffort } from "../providers/llm/types.js";
22
- import { validateParams } from "../sandbox/zod-params.js";
23
- import { runInSandbox } from "../sandbox/run-in-sandbox.js";
24
+ import { createPrivilegedHost } from "../sandbox/host-functions.js";
25
+ import { consoleRedactionValues } from "../sandbox/secret-broker.js";
26
+ import { runUserToolCall } from "../sandbox/user-tool.js";
27
+ import { logUnexpectedError, runSandboxedEngine, SandboxRunCancelledError, } from "../native-worker/gateway.js";
28
+ import { NATIVE_WORKER_PROTOCOL_VERSION } from "../native-worker/protocol.js";
24
29
  import { asStringArray, asPrefixMap } from "../sandbox/tool-capabilities.js";
25
30
  import { scopeDatastore } from "../providers/datastore/scoped.js";
26
31
  import { buildSecretsAccessor, scopeSecretsAccessor } from "./secrets.js";
32
+ import { loadAgentSecretValues, maskNamedSecrets, POSTING_TOOL_NAMES, SECRET_CHECK_UNAVAILABLE_MESSAGE, WITHHELD_TEXT_NOTICE, } from "./secret-guard.js";
27
33
  import { buildSharedDatastoreAccessor, scopeSharedDatastoreAccessor } from "./datastores.js";
28
34
  import { effectiveBudgetForRun } from "./budget-groups.js";
35
+ import { stopChildrenOfLostRun } from "./lost-run-children.js";
29
36
  import { createDelegationSiblings } from "./delegation-siblings.js";
37
+ import { findDelegationChild } from "./delegation-child.js";
38
+ import { DRIVABLE } from "./run-status.js";
30
39
  import { createSerialGate } from "./serial-gate.js";
31
40
  import { withRunHeartbeat } from "./run-heartbeat.js";
32
41
  import { ContinuationRefusedError, dispatchRun } from "./dispatch.js";
@@ -36,6 +45,7 @@ import { DELEGATE_TOOL_PREFIX, duplicateToolNames } from "./tool-names.js";
36
45
  import { SUBAGENT_MEMORY_GET_TOOL, PARENT_MEMORY_GET_TOOL, handleSubAgentMemoryGet, handleParentMemoryGet, } from "./subagent-memory-tools.js";
37
46
  import { prisma as defaultDb } from "./db.js";
38
47
  import { logger } from "./logger.js";
48
+ import { CHILD_RUN_FAILURE_CODES, declaredCodeOf } from "./error-code.js";
39
49
  import { loadCodingConcurrencyConfig } from "../config/providers.js";
40
50
  import { REVIEW_HOST_TOOL_DEFS, REVIEW_HOST_TOOL_NAMES, handleReviewHostTool, } from "./review-host-tools.js";
41
51
  import { closeOpenHostCheck } from "./review-host-checks.js";
@@ -47,15 +57,21 @@ import { ISSUE_TRACKER_TOOL_DEFS, ISSUE_TRACKER_TOOL_NAMES, handleIssueTrackerTo
47
57
  import { completeIssueStatus } from "./issue-status.js";
48
58
  import { fileIssue } from "./issue-dedupe.js";
49
59
  import { fileSelfDefect } from "./self-defects.js";
60
+ import { emitRunFinishedEvents } from "./workflow-run-events.js";
50
61
  import { startReviewFixAfterReview } from "./review-fix.js";
51
62
  import { updateRelatedPullRequests } from "./related-pull-requests.js";
63
+ import { syncMergeOrderChecksAfterRun } from "./merge-order-check.js";
64
+ import { markReadyAfterRun } from "./mark-ready.js";
65
+ import { startDeferredForRequest } from "./host-events.js";
52
66
  import { recordNativeModelUsage } from "./model-usage.js";
53
67
  import { pinNativeRunPricing } from "./run-pricing.js";
54
68
  import { RoutingLlmProvider } from "../providers/llm/routing.js";
55
69
  import { ModelUnavailableError } from "../providers/llm/catalog-types.js";
56
70
  import { trackRun } from "./in-flight-runs.js";
71
+ import { ReviewReadLedger } from "./review-reads.js";
57
72
  import { createRepoAccessGate, requiredLevel } from "./repo-access.js";
58
- const runnerLog = logger.child({ module: "runner" });
73
+ import { loadBudgetEvent, ownerBudgetSentence } from "./budget-events.js";
74
+ export const runnerLog = logger.child({ module: "runner" });
59
75
  /**
60
76
  * Sub-agent dispatch uses one synthetic tool per declared child, named by the
61
77
  * boundName it's attached
@@ -115,20 +131,177 @@ function delegateToolDef(boundName, parallel) {
115
131
  type: "string",
116
132
  description: "Coding sub-agents only. The run id of a prior coding run whose branch/PR this dispatch should push a new commit onto, instead of opening a fresh one — use when the task is a revision to an existing PR (a plan/spec update, or a code-review follow-up), even across a different sub-agent than the one that opened it.",
117
133
  },
134
+ mergeOrder: {
135
+ type: "integer",
136
+ minimum: 1,
137
+ maximum: 99,
138
+ description: "Coding sub-agents only. This repository's step in the merge order of the whole request (1 = merge first). " +
139
+ "Give dependencies lower numbers than the changes that use them (e.g. a service 1, the API that calls it 2, the client 3); " +
140
+ "equal numbers have no order between them. Set it whenever one change spans several repositories.",
141
+ },
118
142
  },
119
143
  required: ["task"],
120
144
  additionalProperties: false,
121
145
  },
122
146
  };
123
147
  }
148
+ /** Every mergeOrder validation failure (wrong type, non-integer, or out of range) names the same bound. */
149
+ const MERGE_ORDER_RANGE_MESSAGE = "mergeOrder must be an integer from 1 to 99.";
124
150
  const DelegateArgs = z
125
151
  .object({
126
152
  task: z.string(),
127
153
  datastoreRef: z.object({ name: z.string(), key: z.string() }).strict().optional(),
128
154
  grantParentMemoryKeys: z.array(z.string()).optional(),
129
155
  continuePriorRun: z.string().optional(),
156
+ mergeOrder: z
157
+ .number({ error: MERGE_ORDER_RANGE_MESSAGE })
158
+ .int(MERGE_ORDER_RANGE_MESSAGE)
159
+ .min(1, MERGE_ORDER_RANGE_MESSAGE)
160
+ .max(99, MERGE_ORDER_RANGE_MESSAGE)
161
+ .optional(),
130
162
  })
131
163
  .strict();
164
+ /** The tool result note for a native child's delegation when the caller passed mergeOrder: it does not apply. */
165
+ const MERGE_ORDER_IGNORED_NOTE = "mergeOrder applies only to coding sub-agents; ignored";
166
+ /** A delegate_to_* call's arguments, or the tool result refusing them. */
167
+ export function parseDelegateArgs(argsJson) {
168
+ let parsed;
169
+ try {
170
+ parsed = JSON.parse(argsJson || "{}");
171
+ }
172
+ catch (err) {
173
+ return {
174
+ refusal: JSON.stringify({
175
+ error: "invalid_arguments_json",
176
+ message: err instanceof Error ? err.message : String(err),
177
+ }),
178
+ };
179
+ }
180
+ try {
181
+ return { args: DelegateArgs.parse(parsed) };
182
+ }
183
+ catch (err) {
184
+ if (err instanceof z.ZodError) {
185
+ return {
186
+ refusal: JSON.stringify({
187
+ error: "validation_failed",
188
+ message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
189
+ }),
190
+ };
191
+ }
192
+ throw err;
193
+ }
194
+ }
195
+ /**
196
+ * At most `limit` dispatches per run (default one: a classifier deciding plan-vs-implement
197
+ * commits to exactly one child), and never the same child twice, so a retry can't leave two
198
+ * children racing on the same task. A delegation waiting for budget holds its place.
199
+ */
200
+ export function delegationLimitRefusal(input) {
201
+ const { priorChildAgentIds, waitingChildAgentIds, childAgentId, boundName, limit } = input;
202
+ if (priorChildAgentIds.includes(childAgentId) || waitingChildAgentIds.includes(childAgentId)) {
203
+ return JSON.stringify({
204
+ error: "already_dispatched",
205
+ message: `This run already delegated to "${boundName}"; a run delegates to each sub-agent at most once.`,
206
+ });
207
+ }
208
+ if (priorChildAgentIds.length < limit && priorChildAgentIds.length + waitingChildAgentIds.length >= limit) {
209
+ // Only a waiting delegation (parallelDelegations) holds the remaining place.
210
+ return JSON.stringify({
211
+ error: "already_dispatched",
212
+ message: `This run reached its limit of ${limit} delegations: ${priorChildAgentIds.length} made and ${waitingChildAgentIds.length} waiting for budget.`,
213
+ });
214
+ }
215
+ if (priorChildAgentIds.length >= limit) {
216
+ return JSON.stringify({
217
+ error: "already_dispatched",
218
+ message: limit === 1
219
+ ? "This run already delegated to a sub-agent; only one delegation is allowed per run."
220
+ : `This run already made ${limit} delegations, the most this agent allows per run.`,
221
+ });
222
+ }
223
+ return null;
224
+ }
225
+ const DELEGATION_CHILD_SELECT = {
226
+ id: true,
227
+ kind: true,
228
+ budgetGroupId: true,
229
+ budgetUsd: true,
230
+ ownerId: true,
231
+ nativeExecutionMode: true,
232
+ codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
233
+ };
234
+ /**
235
+ * The live authorization of one delegation, and the child agent as it is now. Owners are re-read
236
+ * on every delegation (resource-sharing grants spec §3.4.4, N1): the child must have the parent's
237
+ * current owner, or that owner must still hold execute on the child. A revoked grant or a
238
+ * make_owner transfer stops the very next call.
239
+ */
240
+ export async function authorizeDelegation(db, parentAgentId, edge, boundName, args) {
241
+ const [parentNow, childAgent] = await Promise.all([
242
+ db.agent.findUnique({ where: { id: parentAgentId }, select: { ownerId: true } }),
243
+ db.agent.findUniqueOrThrow({ where: { id: edge.childAgentId }, select: DELEGATION_CHILD_SELECT }),
244
+ ]);
245
+ const parentOwnerId = parentNow?.ownerId ?? null;
246
+ const childOwnerId = childAgent.ownerId ?? null;
247
+ if (!parentNow ||
248
+ !(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
249
+ return {
250
+ refusal: JSON.stringify({
251
+ error: "subagent_not_authorized",
252
+ message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
253
+ }),
254
+ };
255
+ }
256
+ if (parentOwnerId !== childOwnerId) {
257
+ // Across owners the edge carries execute and nothing more: no
258
+ // model-chosen memory grant, no continuation of another run's PR,
259
+ // and no task text the caller couldn't give the child directly --
260
+ // the trigger_agent rule (review I3): a coding child only with its
261
+ // owner's allowWebhookTaskOverride opt-in, a native child never
262
+ // (it runs its owner's fixed prompt; pass an empty task).
263
+ const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
264
+ ? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
265
+ : args.continuePriorRun !== undefined
266
+ ? "continuePriorRun is only allowed when the sub-agent has the same owner."
267
+ : childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
268
+ ? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
269
+ : childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
270
+ ? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
271
+ : null;
272
+ if (refusal)
273
+ return { refusal: JSON.stringify({ error: "cross_owner_not_allowed", message: refusal }) };
274
+ }
275
+ if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
276
+ return {
277
+ refusal: JSON.stringify({
278
+ error: "continuation_requires_coding_agent",
279
+ message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
280
+ }),
281
+ };
282
+ }
283
+ return { childAgent };
284
+ }
285
+ /** A native child's task text: the delegated task, with any datastore reference as a note. */
286
+ export function delegationTaskOverride(args) {
287
+ return args.datastoreRef
288
+ ? `${args.task}\n\n[Referenced datastore: name="${args.datastoreRef.name}", key="${args.datastoreRef.key}" — use your datastore tools to read it.]`
289
+ : args.task.trim() !== ""
290
+ ? args.task
291
+ : undefined;
292
+ }
293
+ /** The tool result a native child's terminal run reports to its parent. `note` surfaces e.g. an ignored mergeOrder. */
294
+ export function nativeChildResult(run, note) {
295
+ return JSON.stringify({
296
+ status: run.status,
297
+ finalText: run.finalText,
298
+ costUsd: Number(run.costUsd),
299
+ tokensIn: run.tokensIn,
300
+ tokensOut: run.tokensOut,
301
+ ...(run.error ? { error: run.error } : {}),
302
+ ...(note ? { note } : {}),
303
+ });
304
+ }
132
305
  /**
133
306
  * The composed review hosts, or undefined when there are none (no GitHub App
134
307
  * configured yields an empty registry). Undefined means the run never touches
@@ -161,17 +334,7 @@ function toIssueProjectLink(row) {
161
334
  maxNewIssuesPerRun: row.maxNewIssuesPerRun ?? null,
162
335
  };
163
336
  }
164
- /**
165
- * The two states a Run can still be driven out of. Every write `executeRun`
166
- * makes is filtered on these: a durable executor can have two attempts of the
167
- * same run in flight at once (a still-live original parked in a slow LLM step
168
- * and an adopted attempt the reconciler resumed elsewhere), and whichever
169
- * reaches a terminal state first must win. A conditional `updateMany` makes
170
- * that a database-level CAS rather than a race — the loser's WHERE matches
171
- * zero rows. It also stops a *later* attempt resurrecting a run the
172
- * reconciler already reaped: a `lost` row stays `lost`.
173
- */
174
- const DRIVABLE = ["pending", "running"];
337
+ // DRIVABLE (the two states a Run can still be driven out of) lives in run-status.ts.
175
338
  /**
176
339
  * Raised when a run's step boundary observed a cancellation (DbosExecutor's
177
340
  * `stop`). `executeRun`'s backstop persists these as `cancelled` with the
@@ -183,6 +346,15 @@ export class RunCancelledError extends Error {
183
346
  this.name = "RunCancelledError";
184
347
  }
185
348
  }
349
+ /** Re-exported beside RunCancelledError: thrown by a step runner when another attempt owns the run. */
350
+ export { RunOwnershipLostError };
351
+ /**
352
+ * The one log line for an attempt that lost ownership of its run. Warn, not info: each one also
353
+ * means a discarded step's spend and an owner that stalled long enough to be adopted.
354
+ */
355
+ function logRunOwnershipLost(runId, err) {
356
+ runnerLog.warn({ runId, step: err.step, event: "run_ownership_lost", discardedUsage: err.discardedUsage }, "run_ownership_lost: another execution owns this run; stopping without a terminal write (this attempt's last step is discarded)");
357
+ }
186
358
  /**
187
359
  * Terminal write + read-back. The write is conditional (see DRIVABLE), so
188
360
  * the returned row is the run's real state — this attempt's result if it won
@@ -196,6 +368,46 @@ async function finishRunClaimed(db, runId, data) {
196
368
  const updated = await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data });
197
369
  return { run: await db.run.findUniqueOrThrow({ where: { id: runId } }), claimed: updated.count > 0 };
198
370
  }
371
+ /**
372
+ * A step runner made to notice a stop. Before each step this reads the run's own row: once anything
373
+ * else has made it terminal (stop_run, an MCP task's cancel, a parent run's stop, the reconciler),
374
+ * the run stops at this boundary with a RunCancelledError. The terminal row is never rewritten
375
+ * (finishRunClaimed matches zero rows), so the stop keeps its reason. Inline runs need it because
376
+ * nothing else interrupts their steps. DbosExecutor wraps its own step runner in it too: DBOS stops
377
+ * only a workflow it was told to cancel, and stop() can name only the workflow the run records.
378
+ */
379
+ export function stopAwareStep(db, runId, inner = runStepInline, options = {}) {
380
+ const check = async (name, own) => {
381
+ const read = db.run.findUnique({ where: { id: runId }, select: { status: true, executionWorkflowId: true } });
382
+ const row = options.failOpen ? await read.catch(() => null) : await read;
383
+ if (row?.executionWorkflowId && own && options.ownership?.forkedFrom(row.executionWorkflowId, own)) {
384
+ throw new RunOwnershipLostError(name);
385
+ }
386
+ if (row && !DRIVABLE.includes(row.status)) {
387
+ throw new RunCancelledError(`Run ${row.status}: stopped before step "${name}".`);
388
+ }
389
+ };
390
+ // Each check waits for the one before it, so steps reach `inner` in the order they were called,
391
+ // as they did without a read in front: a durable runner numbers a step when it is called (DBOS
392
+ // assigns its function id then), and a replay of concurrent delegations matches steps by number.
393
+ let previous = Promise.resolve();
394
+ return async (name, fn) => {
395
+ const own = options.ownership?.workflowId();
396
+ const checked = previous.then(() => check(name, own));
397
+ previous = checked.catch(() => undefined);
398
+ await checked;
399
+ return inner(name, fn);
400
+ };
401
+ }
402
+ /**
403
+ * Why a sandbox-mode native run is not executed here. The runner only ever
404
+ * executes native runs in the control plane; a run whose snapshot says
405
+ * `sandbox` reaches it only when no native sandbox executor is composed in
406
+ * (RoutingExecutor sends it to the native executor), and must fail closed
407
+ * rather than run unisolated.
408
+ */
409
+ export const NATIVE_SANDBOX_NOT_CONFIGURED = "native_sandbox_unavailable: This agent runs with nativeExecutionMode=sandbox, but this deployment has no native " +
410
+ "sandbox executor configured, so the run was not started. Switch the agent back to control-plane mode to run it here.";
199
411
  /**
200
412
  * Why a coding run cannot start in this process: runs reach the runner only
201
413
  * when no container executor is composed in (JOB_LAUNCHER=local, e.g. every
@@ -210,12 +422,22 @@ const CODING_EXECUTOR_NOT_CONFIGURED = "Coding agents need a container executor,
210
422
  * write the terminal row, and the queue timeout only fires on a drain tick.
211
423
  */
212
424
  export const CODING_CHILD_WAIT_GRACE_SEC = 60;
425
+ /**
426
+ * How long a parent waits for a sandbox-mode native child before stopping it. Native runs have
427
+ * no run timeout of their own; until the sandbox executor brings a per-run deadline, this bounds
428
+ * a parent left waiting on a child that never finishes.
429
+ */
430
+ export const SANDBOX_CHILD_WAIT_SEC = 60 * 60;
431
+ /** The leading snake_case code of an error message (`code: sentence`); messages can carry internals. */
213
432
  /**
214
433
  * What a coding sub-agent's run tells the parent run's model. A run refused
215
434
  * over its services carries the host sentence (never the raw Run.error), so
216
- * the parent can pass it on — for a mention, that is the requester.
435
+ * the parent can pass it on — for a mention, that is the requester. A failed
436
+ * run carries only its error code, so the parent can say why without seeing internals.
217
437
  */
218
- export function codingChildResult(run, codingResult) {
438
+ export function codingChildResult(run, codingResult,
439
+ /** The child's recorded budget refusal (loadBudgetEvent), if any. */
440
+ budgetEvent) {
219
441
  // Checked before the "refused" branch: a continuation whose PR closed
220
442
  // before the run ever spent anything is stored as "refused" (a preflight
221
443
  // refusal, container.ts), not "failed" -- but it is the same
@@ -226,6 +448,10 @@ export function codingChildResult(run, codingResult) {
226
448
  : run.status === "refused"
227
449
  ? serviceRefusalSentence(run.error)
228
450
  : null;
451
+ const errorCode = run.status === "failed" ? declaredCodeOf(run.error, CHILD_RUN_FAILURE_CODES) : undefined;
452
+ const budget = budgetEvent && (run.status === "budget_exhausted" || run.status === "refused")
453
+ ? ownerBudgetSentence(budgetEvent.event, budgetEvent.agentName)
454
+ : undefined;
229
455
  return JSON.stringify({
230
456
  status: run.status,
231
457
  finalText: run.finalText,
@@ -233,6 +459,8 @@ export function codingChildResult(run, codingResult) {
233
459
  tokensIn: run.tokensIn,
234
460
  tokensOut: run.tokensOut,
235
461
  ...(refusal ? { refusal } : {}),
462
+ ...(errorCode ? { errorCode } : {}),
463
+ ...(budget ? { budget } : {}),
236
464
  ...pullRequestOf(codingResult),
237
465
  });
238
466
  }
@@ -306,40 +534,27 @@ export async function createRun(db, agentName, trigger = "manual") {
306
534
  if (agent.kind === "coding") {
307
535
  throw new Error(CODING_EXECUTOR_NOT_CONFIGURED);
308
536
  }
309
- return db.run.create({ data: { agentId: agent.id, trigger } });
310
- }
311
- /** Drives an existing Run (created by `createRun` or the scheduler) to a terminal state. */
312
- export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
313
- // Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
314
- return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
315
- }
316
- async function executeTrackedRun(runId, providers, db, onText, step) {
317
- const existingRun = await db.run.findUnique({ where: { id: runId } });
318
- if (!existingRun) {
319
- throw new Error(`Unknown run "${runId}".`);
537
+ // Refused here, before any row: a foreground run executes inline, never in a sandbox.
538
+ if (agent.nativeExecutionMode === "sandbox") {
539
+ throw new Error(NATIVE_SANDBOX_NOT_CONFIGURED);
320
540
  }
321
- // Pre-flight guard. A run that already reached a terminal state must never
322
- // be re-driven: a durable workflow re-dispatched after the reconciler
323
- // reaped its row (rollback to EXECUTOR=in-process, then roll forward) would
324
- // otherwise re-spend the whole run against a `lost` row, and a duplicate
325
- // attempt of a finished run would spend a second time for a result no write
326
- // can land. Cheaper and clearer than letting it run and discarding the
327
- // result at the conditional write.
328
- if (!DRIVABLE.includes(existingRun.status)) {
329
- runnerLog.info({ runId, status: existingRun.status }, "skipping execution of an already-terminal run");
330
- return existingRun;
331
- }
332
- const reviewHosts = configuredReviewHosts(providers.reviewHosts);
333
- const repoAccess = reviewHosts
334
- ? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
335
- : undefined;
336
- const issueTrackers = configuredIssueTrackers(providers.issueTrackers);
337
- // Pinned in one checkpointed step: on replay after a crash, the agent row
338
- // or its budget group may have changed since first execution. The
339
- // engine's control flow depends on budgetUsd and maxTurns, so they must
340
- // be pinned to the values seen on first execution or the replay's step
341
- // order diverges from the record.
342
- const loadedOrUnavailable = await step("load", async () => {
541
+ return db.run.create({ data: { agentId: agent.id, trigger, nativeExecutionMode: agent.nativeExecutionMode } });
542
+ }
543
+ /**
544
+ * The trusted load of a native run: the agent, its attached tools and their
545
+ * consented capabilities, the pinned catalog entry, the effective budget,
546
+ * sub-agent edges, and repository/issue links. Shared by in-process execution
547
+ * and the native sandbox gateway, which hands the worker this result.
548
+ *
549
+ * Pinned in one checkpointed step: on replay after a crash, the agent row
550
+ * or its budget group may have changed since first execution. The
551
+ * engine's control flow depends on budgetUsd and maxTurns, so they must
552
+ * be pinned to the values seen on first execution or the replay's step
553
+ * order diverges from the record.
554
+ */
555
+ export async function loadNativeRun(options) {
556
+ const { runId, existingRun, providers, db, step, reviewHosts, issueTrackers } = options;
557
+ return step("load", async () => {
343
558
  const agent = await db.agent.findUnique({ where: { id: existingRun.agentId } });
344
559
  if (!agent) {
345
560
  throw new Error(`Run "${runId}" references missing agent "${existingRun.agentId}".`);
@@ -486,53 +701,181 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
486
701
  (err instanceof Error && err.message.startsWith("model_unavailable:"))) {
487
702
  return { unavailable: err.message };
488
703
  }
704
+ // Thrown out of executeRun ahead of its backstop, so logged here instead.
705
+ if (err instanceof RunOwnershipLostError)
706
+ logRunOwnershipLost(runId, err);
489
707
  throw err;
490
708
  });
491
- // Fails a run that ends before its engine starts, closing what its trigger opened (the review
492
- // check, the host and issue status comments) exactly as the normal and catch paths below do.
493
- // Deliberately no self-defect: both callers are configuration states, not this run's defect (an
494
- // admin disabled or removed the model, or this deployment has no coding executor), and filing
495
- // would open one defect per affected agent rather than describe a failure of that agent.
496
- const finishEarly = async (error) => {
497
- const finished = await finishRun(db, runId, { status: "failed", error, finishedAt: new Date() });
498
- await closeOpenHostCheck(db, finished, reviewHosts);
499
- await completeHostStatus(db, finished, reviewHosts);
500
- await completeIssueStatus(db, finished, issueTrackers);
501
- return finished;
709
+ }
710
+ /**
711
+ * Every value of the secrets `names` (readable or brokered; a SigV4 secret's key and token alone
712
+ * too, the same set the in-process host redacts), as a redaction list. Rejects when ANY read
713
+ * fails: a list missing one secret would let that secret through, so a caller treats a rejection
714
+ * as "no list" and fails closed. A secret with no value contributes nothing.
715
+ */
716
+ export async function readRedactionValues(accessor, names) {
717
+ const values = await Promise.all(names.map((secret) => accessor.resolve
718
+ ? accessor.resolve(secret).then((e) => (e ? consoleRedactionValues(e) : []))
719
+ : accessor.get(secret).then((v) => (v === undefined ? [] : [v]))));
720
+ return values.flat().filter((value) => typeof value === "string");
721
+ }
722
+ /**
723
+ * One attempt's tool surface for a native run. `builtinHandler(name)` is the
724
+ * single source of truth for which names are trusted built-ins (memory,
725
+ * repo_*, jira_*, sub-agent/parent memory, delegate_to_*) and returns undefined
726
+ * for everything else, which `runUserTool` runs in the WASM sandbox. Per-attempt
727
+ * state (the delegation gate, siblings, issue creation counters) lives here.
728
+ */
729
+ export function createNativeRunTools(options) {
730
+ const { runId, existingRun, loaded, providers, db, reviewHosts, repoAccess, issueTrackers } = options;
731
+ const toolsByName = new Map(Object.entries(loaded.toolsByName));
732
+ const secretsAccessor = buildSecretsAccessor(loaded.agentId, providers.secrets, db);
733
+ const sharedDatastoreAccessor = buildSharedDatastoreAccessor(loaded.agentId, providers.datastore, db);
734
+ // jira_create_issue's per-run cap counter: shared by every tool call of this attempt (a resumed attempt
735
+ // starts a fresh one, floored by the run's recorded fingerprint creates).
736
+ const issueCreationCounters = new Map();
737
+ // What this attempt's review tools have read, for repo_publish_review's unread_files gate.
738
+ const reviewReads = options.reviewReads ?? new ReviewReadLedger();
739
+ // One per attempt: admits this run's delegations one at a time (see the delegate branch).
740
+ const delegationGate = createSerialGate();
741
+ // The children this attempt's delegations have running, and the sub-agents whose delegation is
742
+ // waiting for one of them to free budget (parallelDelegations): a waiting delegation keeps its
743
+ // place in the same-child and limit checks while the gate is open to the others.
744
+ const delegationSiblings = createDelegationSiblings();
745
+ const waitingDelegations = new Set();
746
+ /**
747
+ * Waits on a dispatched coding or sandbox-mode native child until it is terminal, its wait
748
+ * bound passes (it is then stopped), or this run is cancelled, and returns what the model sees.
749
+ * The bound is measured from the child's creation, so a re-executed delegate step (a resume, or
750
+ * a takeover under a new version) never extends how long this run waits for it.
751
+ */
752
+ const awaitDelegatedChild = async (wait) => {
753
+ const { executor, childRunId, kind, boundName } = wait;
754
+ const startedAt = Date.now();
755
+ let boundMs;
756
+ if (kind === "coding") {
757
+ const codingRun = await db.codingRun.findUnique({
758
+ where: { runId: childRunId },
759
+ select: { timeoutSec: true },
760
+ });
761
+ const { queueTimeoutSec } = loadCodingConcurrencyConfig();
762
+ boundMs = (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000;
763
+ }
764
+ else {
765
+ boundMs = SANDBOX_CHILD_WAIT_SEC * 1000;
766
+ }
767
+ const elapsedMs = Math.max(0, Date.now() - wait.createdAtMs);
768
+ // Executor.start may resolve before the child is terminal, so wait on the row.
769
+ const waited = await waitForCodingChild({
770
+ db,
771
+ executor,
772
+ childRunId,
773
+ parentRunId: runId,
774
+ boundMs: Math.max(0, boundMs - elapsedMs),
775
+ });
776
+ wait.onSettled();
777
+ runnerLog.info({
778
+ runId,
779
+ childRunId,
780
+ boundName,
781
+ outcome: waited.kind === "terminal" ? waited.run.status : waited.kind,
782
+ durationMs: Date.now() - startedAt,
783
+ }, "delegation finished");
784
+ if (waited.kind === "timed_out") {
785
+ return JSON.stringify({
786
+ error: "subagent_wait_timed_out",
787
+ runId: childRunId,
788
+ message: kind === "coding"
789
+ ? "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result."
790
+ : "The sandbox sub-agent did not finish in time; it was stopped and produced no result.",
791
+ });
792
+ }
793
+ if (waited.kind === "parent_cancelled") {
794
+ return JSON.stringify({
795
+ error: "parent_cancelled",
796
+ runId: childRunId,
797
+ message: kind === "coding"
798
+ ? "This run was cancelled while waiting for the coding sub-agent; the sub-agent was stopped."
799
+ : "This run was cancelled while waiting for the sandbox sub-agent; the sub-agent was stopped.",
800
+ });
801
+ }
802
+ if (kind === "coding") {
803
+ const stored = await db.codingRun
804
+ .findUnique({ where: { runId: childRunId }, select: { result: true } })
805
+ .catch((err) => {
806
+ runnerLog.warn({ err, childRunId }, "could not read the coding sub-run's result");
807
+ return null;
808
+ });
809
+ return codingChildResult(waited.run, stored?.result, await loadBudgetEvent(db, childRunId));
810
+ }
811
+ return nativeChildResult(waited.run, wait.mergeOrderNote);
502
812
  };
503
- if ("unavailable" in loadedOrUnavailable)
504
- return finishEarly(loadedOrUnavailable.unavailable);
505
- const loaded = loadedOrUnavailable;
506
- if (loaded.kind === "coding")
507
- return finishEarly(CODING_EXECUTOR_NOT_CONFIGURED);
508
- // Conditional on DRIVABLE rather than on `pending`: an adopted attempt
509
- // legitimately finds the row already `running`, but a terminal row must
510
- // never be flipped back to `running`.
511
- await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
512
- try {
513
- const toolsByName = new Map(Object.entries(loaded.toolsByName));
514
- const secretsAccessor = buildSecretsAccessor(loaded.agentId, providers.secrets, db);
515
- const sharedDatastoreAccessor = buildSharedDatastoreAccessor(loaded.agentId, providers.datastore, db);
516
- // jira_create_issue's per-run cap counter: shared by every tool call of this attempt (a resumed attempt
517
- // starts a fresh one, floored by the run's recorded fingerprint creates).
518
- const issueCreationCounters = new Map();
519
- // One per attempt: admits this run's delegations one at a time (see the delegate branch).
520
- const delegationGate = createSerialGate();
521
- // The children this attempt's delegations have running, and the sub-agents whose delegation is
522
- // waiting for one of them to free budget (parallelDelegations): a waiting delegation keeps its
523
- // place in the same-child and limit checks while the gate is open to the others.
524
- const delegationSiblings = createDelegationSiblings();
525
- const waitingDelegations = new Set();
526
- const runSandboxTool = async (name, argsJson) => {
527
- if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
813
+ /**
814
+ * A re-executed delegate call whose child already exists (findDelegationChild): wait on it
815
+ * again rather than refuse it as a duplicate. A control-plane native child ran inline in the
816
+ * attempt that died, so nothing drives it any more: it is stopped (or its result returned, if
817
+ * it had finished) and no new child is started.
818
+ */
819
+ const reattachDelegatedChild = async (child, boundName, argsJson) => {
820
+ const row = await db.run.findUniqueOrThrow({ where: { id: child.id } });
821
+ const childAgentRow = await db.agent.findUnique({ where: { id: row.agentId }, select: { kind: true } });
822
+ const parsedArgs = parseDelegateArgs(argsJson);
823
+ const mergeOrderNote = "args" in parsedArgs && parsedArgs.args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined;
824
+ const coding = childAgentRow?.kind === "coding";
825
+ if (!coding && row.nativeExecutionMode !== "sandbox") {
826
+ if (!DRIVABLE.includes(row.status))
827
+ return nativeChildResult(row, mergeOrderNote);
828
+ await providers.executor?.stop(child.id, "parent resumed without it").catch((err) => {
829
+ runnerLog.warn({ err, childRunId: child.id, runId }, "failed to stop an interrupted sub-agent run");
830
+ });
831
+ return JSON.stringify({
832
+ error: "subagent_interrupted",
833
+ message: "The sub-agent was interrupted by a restart and cannot be resumed; no new sub-agent run was started.",
834
+ });
835
+ }
836
+ if (!providers.executor) {
837
+ return coding
838
+ ? JSON.stringify({
839
+ error: "coding_dispatch_unavailable",
840
+ message: "This execution context has no Executor wired in, so a coding-kind sub-agent cannot be dispatched.",
841
+ })
842
+ : JSON.stringify({
843
+ error: "sandbox_dispatch_unavailable",
844
+ message: "This execution context has no Executor wired in, so a sandbox-mode sub-agent cannot be dispatched.",
845
+ });
846
+ }
847
+ // Counted as a running sibling while it is waited on, as a fresh dispatch is.
848
+ const finishSibling = delegationSiblings.start();
849
+ try {
850
+ return await awaitDelegatedChild({
851
+ executor: providers.executor,
852
+ childRunId: child.id,
853
+ createdAtMs: child.createdAt.getTime(),
854
+ kind: coding ? "coding" : "native-sandbox",
855
+ boundName,
856
+ mergeOrderNote: coding ? undefined : mergeOrderNote,
857
+ onSettled: finishSibling,
858
+ });
859
+ }
860
+ finally {
861
+ finishSibling();
862
+ }
863
+ };
864
+ const unguardedBuiltinHandler = (name) => {
865
+ if (loaded.memoryEnabled && MEMORY_TOOL_NAMES.has(name)) {
866
+ return async (argsJson) => {
528
867
  return handleMemoryTool(name, argsJson, loaded.agentId, providers.memory);
529
- }
530
- if (REVIEW_HOST_TOOL_NAMES.has(name) && loaded.repositoryLinks.length > 0 && reviewHosts && repoAccess) {
868
+ };
869
+ }
870
+ if (REVIEW_HOST_TOOL_NAMES.has(name) && loaded.repositoryLinks.length > 0 && reviewHosts && repoAccess) {
871
+ return async (argsJson) => {
531
872
  const check = await db.runHostCheck.findUnique({ where: { runId } });
532
873
  return handleReviewHostTool(name, argsJson, {
533
874
  agentId: loaded.agentId,
534
875
  links: loaded.repositoryLinks,
535
876
  hosts: reviewHosts,
877
+ reads: reviewReads,
878
+ db,
536
879
  runCheck: check && !check.completedAt
537
880
  ? {
538
881
  provider: check.provider,
@@ -581,11 +924,13 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
581
924
  });
582
925
  },
583
926
  });
584
- }
585
- // `?? []`: a load step replayed from before these links were pinned has none.
586
- // Each link is re-normalised too: one pinned before the allowlists existed lacks them.
587
- const issueProjectLinks = (loaded.issueProjectLinks ?? []).map(toIssueProjectLink);
588
- if (ISSUE_TRACKER_TOOL_NAMES.has(name) && issueProjectLinks.length > 0 && issueTrackers) {
927
+ };
928
+ }
929
+ // `?? []`: a load step replayed from before these links were pinned has none.
930
+ // Each link is re-normalised too: one pinned before the allowlists existed lacks them.
931
+ const issueProjectLinks = (loaded.issueProjectLinks ?? []).map(toIssueProjectLink);
932
+ if (ISSUE_TRACKER_TOOL_NAMES.has(name) && issueProjectLinks.length > 0 && issueTrackers) {
933
+ return async (argsJson) => {
589
934
  return handleIssueTrackerTool(name, argsJson, {
590
935
  agentId: loaded.agentId,
591
936
  links: issueProjectLinks,
@@ -605,14 +950,22 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
605
950
  recordedCreates: (projectKey) => db.issueFingerprint.count({ where: { createdByRunId: runId, issueProvider: "jira", projectKey } }),
606
951
  },
607
952
  });
608
- }
609
- if (name === "subagent_memory_get") {
953
+ };
954
+ }
955
+ if (name === "subagent_memory_get") {
956
+ return async (argsJson) => {
610
957
  return handleSubAgentMemoryGet(argsJson, loaded.agentId, db, providers.memory);
611
- }
612
- if (name === "parent_memory_get") {
958
+ };
959
+ }
960
+ if (name === "parent_memory_get") {
961
+ return async (argsJson) => {
613
962
  return handleParentMemoryGet(argsJson, runId, db, providers.memory);
614
- }
615
- if (name.startsWith(DELEGATE_TOOL_PREFIX)) {
963
+ };
964
+ }
965
+ if (name.startsWith(DELEGATE_TOOL_PREFIX)) {
966
+ return async (argsJson, toolCallId) => {
967
+ // An empty id (an adapter that had none) is no key: every such call would share it.
968
+ const callId = toolCallId || undefined;
616
969
  const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
617
970
  const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
618
971
  if (!edge) {
@@ -621,8 +974,36 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
621
974
  message: `No sub-agent is bound to name "${boundName}".`,
622
975
  });
623
976
  }
977
+ // A re-executed delegate step (a resume, or a takeover under a new version) finds the
978
+ // child this very call created and waits on it again, instead of refusing it as a duplicate.
979
+ const existingChild = await findDelegationChild(db, runId, edge.childAgentId, callId);
980
+ if (existingChild) {
981
+ runnerLog.info({ runId, childRunId: existingChild.id, boundName }, "delegation re-attached");
982
+ return reattachDelegatedChild(existingChild, boundName, argsJson);
983
+ }
624
984
  let release = await delegationGate.acquire();
625
985
  let finishSibling = () => { };
986
+ /**
987
+ * A child create that lost the (parentRunId, parentCallId) unique key to another execution of
988
+ * this same delegate step (a stalled owner overlapping an adopted attempt): wait on the child
989
+ * that execution created, as a re-executed step does. A key held by no child of this agent
990
+ * (an id colliding with another sub-agent's call) is the model's to see, never a run failure.
991
+ */
992
+ const afterDelegationKeyConflict = async (err) => {
993
+ if (!(err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002"))
994
+ throw err;
995
+ release();
996
+ const raced = await findDelegationChild(db, runId, edge.childAgentId, callId);
997
+ if (raced) {
998
+ runnerLog.info({ runId, childRunId: raced.id, boundName }, "delegation re-attached after a concurrent create");
999
+ return reattachDelegatedChild(raced, boundName, argsJson);
1000
+ }
1001
+ runnerLog.warn({ err, runId, boundName }, "delegation child create hit a unique key held by no child of this agent");
1002
+ return JSON.stringify({
1003
+ error: "delegation_conflict",
1004
+ message: "The sub-agent run could not be created: its call id is already used by another sub-agent run.",
1005
+ });
1006
+ };
626
1007
  try {
627
1008
  // At most maxDelegationsPerRun dispatches per run (default one: a
628
1009
  // classifier deciding plan-vs-implement commits to exactly one child),
@@ -630,105 +1011,24 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
630
1011
  // racing on the same task. Checked and the child row written under the
631
1012
  // run's delegation gate: delegations started together in one turn
632
1013
  // (parallelDelegations) are admitted one at a time.
633
- const limit = loaded.maxDelegationsPerRun ?? 1;
634
1014
  const priorDispatches = await db.run.findMany({ where: { parentRunId: { in: [runId] } } });
635
- if (priorDispatches.some((prior) => prior.agentId === edge.childAgentId) ||
636
- waitingDelegations.has(edge.childAgentId)) {
637
- return JSON.stringify({
638
- error: "already_dispatched",
639
- message: `This run already delegated to "${boundName}"; a run delegates to each sub-agent at most once.`,
640
- });
641
- }
642
- if (priorDispatches.length < limit && priorDispatches.length + waitingDelegations.size >= limit) {
643
- // Only a waiting delegation (parallelDelegations) holds the remaining place.
644
- return JSON.stringify({
645
- error: "already_dispatched",
646
- message: `This run reached its limit of ${limit} delegations: ${priorDispatches.length} made and ${waitingDelegations.size} waiting for budget.`,
647
- });
648
- }
649
- if (priorDispatches.length >= limit) {
650
- return JSON.stringify({
651
- error: "already_dispatched",
652
- message: limit === 1
653
- ? "This run already delegated to a sub-agent; only one delegation is allowed per run."
654
- : `This run already made ${limit} delegations, the most this agent allows per run.`,
655
- });
656
- }
657
- let parsed;
658
- try {
659
- parsed = JSON.parse(argsJson || "{}");
660
- }
661
- catch (err) {
662
- return JSON.stringify({
663
- error: "invalid_arguments_json",
664
- message: err instanceof Error ? err.message : String(err),
665
- });
666
- }
667
- let args;
668
- try {
669
- args = DelegateArgs.parse(parsed);
670
- }
671
- catch (err) {
672
- if (err instanceof z.ZodError) {
673
- return JSON.stringify({
674
- error: "validation_failed",
675
- message: err.issues.map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; "),
676
- });
677
- }
678
- throw err;
679
- }
680
- // Owners are re-read live on every delegation (resource-sharing
681
- // grants spec §3.4.4, N1): the child must have the parent's current
682
- // owner, or that owner must still hold execute on the child. A
683
- // revoked grant or a make_owner transfer stops the very next call.
684
- const [parentNow, childAgent] = await Promise.all([
685
- db.agent.findUnique({ where: { id: loaded.agentId }, select: { ownerId: true } }),
686
- db.agent.findUniqueOrThrow({
687
- where: { id: edge.childAgentId },
688
- select: {
689
- id: true,
690
- kind: true,
691
- budgetGroupId: true,
692
- budgetUsd: true,
693
- ownerId: true,
694
- codingProfile: { select: { allowWebhookTaskOverride: true, timeoutSec: true } },
695
- },
696
- }),
697
- ]);
698
- const parentOwnerId = parentNow?.ownerId ?? null;
699
- const childOwnerId = childAgent.ownerId ?? null;
700
- if (!parentNow ||
701
- !(await canDelegate(db, { ownerId: parentOwnerId }, { id: childAgent.id, ownerId: childOwnerId }))) {
702
- return JSON.stringify({
703
- error: "subagent_not_authorized",
704
- message: `The "${boundName}" sub-agent belongs to another owner who has not given this agent's owner execute access to it.`,
705
- });
706
- }
707
- if (parentOwnerId !== childOwnerId) {
708
- // Across owners the edge carries execute and nothing more: no
709
- // model-chosen memory grant, no continuation of another run's PR,
710
- // and no task text the caller couldn't give the child directly --
711
- // the trigger_agent rule (review I3): a coding child only with its
712
- // owner's allowWebhookTaskOverride opt-in, a native child never
713
- // (it runs its owner's fixed prompt; pass an empty task).
714
- const refusal = (args.grantParentMemoryKeys?.length ?? 0) > 0
715
- ? "grantParentMemoryKeys is only allowed when the sub-agent has the same owner."
716
- : args.continuePriorRun !== undefined
717
- ? "continuePriorRun is only allowed when the sub-agent has the same owner."
718
- : childAgent.kind === "coding" && !childAgent.codingProfile?.allowWebhookTaskOverride
719
- ? "This coding sub-agent belongs to another owner and does not accept task text from others (allowWebhookTaskOverride)."
720
- : childAgent.kind !== "coding" && (args.task.trim() !== "" || args.datastoreRef !== undefined)
721
- ? 'This sub-agent belongs to another owner and runs only its own instructions: delegate with task "" and no datastoreRef.'
722
- : null;
723
- if (refusal)
724
- return JSON.stringify({ error: "cross_owner_not_allowed", message: refusal });
725
- }
726
- if (args.continuePriorRun !== undefined && childAgent.kind !== "coding") {
727
- return JSON.stringify({
728
- error: "continuation_requires_coding_agent",
729
- message: "continuePriorRun is only supported when the sub-agent is coding-kind.",
730
- });
731
- }
1015
+ const limitRefusal = delegationLimitRefusal({
1016
+ priorChildAgentIds: priorDispatches.map((prior) => prior.agentId),
1017
+ waitingChildAgentIds: [...waitingDelegations],
1018
+ childAgentId: edge.childAgentId,
1019
+ boundName,
1020
+ limit: loaded.maxDelegationsPerRun ?? 1,
1021
+ });
1022
+ if (limitRefusal)
1023
+ return limitRefusal;
1024
+ const parsedArgs = parseDelegateArgs(argsJson);
1025
+ if ("refusal" in parsedArgs)
1026
+ return parsedArgs.refusal;
1027
+ const { args } = parsedArgs;
1028
+ const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
1029
+ if ("refusal" in authorized)
1030
+ return authorized.refusal;
1031
+ const { childAgent } = authorized;
732
1032
  // parallelDelegations: a child the budget would refuse (run_tree_exhausted or
733
1033
  // budget_group_exhausted:*) while a sibling still runs waits for a sibling to finish and
734
1034
  // free its hold, then tries again while siblings remain, up to the child's own wait bound
@@ -741,6 +1041,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
741
1041
  while (delegationSiblings.inFlight > 0) {
742
1042
  // Taken before the budget read, so a sibling that finishes during it still wakes this wait.
743
1043
  const finishes = delegationSiblings.finishes;
1044
+ // What dispatch will reserve: a coding child's base too, its overrun is never reserved.
744
1045
  const { exhaustedBy } = await effectiveBudgetForRun(db, childAgent, new Date(), runId);
745
1046
  const remainingMs = deadline - Date.now();
746
1047
  if (!exhaustedBy || remainingMs <= 0)
@@ -798,7 +1099,9 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
798
1099
  trigger: "subagent",
799
1100
  codingTask: args.task,
800
1101
  continuesCodingRunId: args.continuePriorRun,
1102
+ mergeOrder: args.mergeOrder,
801
1103
  parentRunId: runId,
1104
+ parentCallId: callId,
802
1105
  // The child's result flows back into this run, which the
803
1106
  // triggerer sees, so the child is visible to them too.
804
1107
  triggeredById: existingRun.triggeredById,
@@ -818,7 +1121,7 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
818
1121
  // a pull request another deployment opened, say) is the model's to
819
1122
  // report, not a reason to fail the whole run.
820
1123
  if (!(err instanceof ContinuationRefusedError))
821
- throw err;
1124
+ return await afterDelegationKeyConflict(err);
822
1125
  return JSON.stringify({
823
1126
  error: "continuation_refused",
824
1127
  message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
@@ -829,65 +1132,91 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
829
1132
  return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
830
1133
  }
831
1134
  const childRunId = dispatched.run.id;
832
- const startedAt = Date.now();
833
1135
  runnerLog.info({ runId, childRunId, boundName, kind: "coding" }, "delegation started");
834
- const codingRun = await db.codingRun.findUnique({
835
- where: { runId: childRunId },
836
- select: { timeoutSec: true },
837
- });
838
- const { queueTimeoutSec } = loadCodingConcurrencyConfig();
839
- const waited = await waitForCodingChild({
840
- db,
1136
+ return await awaitDelegatedChild({
841
1137
  executor: providers.executor,
842
1138
  childRunId,
843
- parentRunId: runId,
844
- boundMs: (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000,
845
- });
846
- finishSibling();
847
- runnerLog.info({
848
- runId,
849
- childRunId,
1139
+ createdAtMs: dispatched.run.startedAt.getTime(),
1140
+ kind: "coding",
850
1141
  boundName,
851
- outcome: waited.kind === "terminal" ? waited.run.status : waited.kind,
852
- durationMs: Date.now() - startedAt,
853
- }, "delegation finished");
854
- if (waited.kind === "timed_out") {
1142
+ mergeOrderNote: undefined,
1143
+ onSettled: finishSibling,
1144
+ });
1145
+ }
1146
+ const taskOverride = delegationTaskOverride(args);
1147
+ // mergeOrder is a coding-only concept (it orders CodingRuns within a merge); a native
1148
+ // child has no CodingRun to store it on, so it's never passed to dispatchRun/executeRun
1149
+ // below -- only surfaced back to the model as a note, so a lead that set it on the wrong
1150
+ // kind of sub-agent notices rather than silently losing it.
1151
+ const mergeOrderNote = args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined;
1152
+ if (childAgent.nativeExecutionMode === "sandbox") {
1153
+ // A sandbox-mode child must run where its snapshot says, so it goes through
1154
+ // dispatchRun and the Executor (RoutingExecutor sends it to the sandbox executor),
1155
+ // exactly like a coding child — never inline in this process.
1156
+ if (!providers.executor) {
855
1157
  return JSON.stringify({
856
- error: "subagent_wait_timed_out",
857
- runId: childRunId,
858
- message: "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result.",
1158
+ error: "sandbox_dispatch_unavailable",
1159
+ message: "This execution context has no Executor wired in, so a sandbox-mode sub-agent cannot be dispatched.",
859
1160
  });
860
1161
  }
861
- if (waited.kind === "parent_cancelled") {
862
- return JSON.stringify({
863
- error: "parent_cancelled",
864
- runId: childRunId,
865
- message: "This run was cancelled while waiting for the coding sub-agent; the sub-agent was stopped.",
1162
+ let dispatched;
1163
+ try {
1164
+ dispatched = await dispatchRun({
1165
+ db,
1166
+ executor: providers.executor,
1167
+ selfDefects: { db, issueTrackers },
1168
+ agentId: edge.childAgentId,
1169
+ trigger: "subagent",
1170
+ taskOverride,
1171
+ grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
1172
+ parentRunId: runId,
1173
+ parentCallId: callId,
1174
+ triggeredById: existingRun.triggeredById,
1175
+ awaitExecution: true,
1176
+ onPersisted: (persistedRun) => {
1177
+ if (persistedRun.status !== "refused" && persistedRun.status !== "failed") {
1178
+ finishSibling = delegationSiblings.start();
1179
+ }
1180
+ release();
1181
+ },
866
1182
  });
867
1183
  }
868
- const stored = await db.codingRun
869
- .findUnique({ where: { runId: childRunId }, select: { result: true } })
870
- .catch((err) => {
871
- runnerLog.warn({ err, childRunId }, "could not read the coding sub-run's result");
872
- return null;
1184
+ catch (err) {
1185
+ return await afterDelegationKeyConflict(err);
1186
+ }
1187
+ if (!dispatched) {
1188
+ return JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." });
1189
+ }
1190
+ const childRunId = dispatched.run.id;
1191
+ runnerLog.info({ runId, childRunId, boundName, kind: "native-sandbox" }, "delegation started");
1192
+ return await awaitDelegatedChild({
1193
+ executor: providers.executor,
1194
+ childRunId,
1195
+ createdAtMs: dispatched.run.startedAt.getTime(),
1196
+ kind: "native-sandbox",
1197
+ boundName,
1198
+ mergeOrderNote,
1199
+ onSettled: finishSibling,
873
1200
  });
874
- return codingChildResult(waited.run, stored?.result);
875
1201
  }
876
- const taskOverride = args.datastoreRef
877
- ? `${args.task}\n\n[Referenced datastore: name="${args.datastoreRef.name}", key="${args.datastoreRef.key}" — use your datastore tools to read it.]`
878
- : args.task.trim() !== ""
879
- ? args.task
880
- : undefined;
881
- const childRun = await db.run.create({
882
- data: {
883
- agentId: edge.childAgentId,
884
- trigger: "subagent",
885
- parentRunId: runId,
886
- grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
887
- taskOverride,
888
- triggeredById: existingRun.triggeredById,
889
- },
890
- });
1202
+ let childRun;
1203
+ try {
1204
+ childRun = await db.run.create({
1205
+ data: {
1206
+ agentId: edge.childAgentId,
1207
+ trigger: "subagent",
1208
+ parentRunId: runId,
1209
+ parentCallId: callId,
1210
+ grantedParentMemoryKeys: args.grantParentMemoryKeys ?? [],
1211
+ taskOverride,
1212
+ triggeredById: existingRun.triggeredById,
1213
+ nativeExecutionMode: childAgent.nativeExecutionMode,
1214
+ },
1215
+ });
1216
+ }
1217
+ catch (err) {
1218
+ return await afterDelegationKeyConflict(err);
1219
+ }
891
1220
  finishSibling = delegationSiblings.start();
892
1221
  // The child row exists: admit the next delegation while this child runs.
893
1222
  release();
@@ -904,87 +1233,674 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
904
1233
  outcome: childResult.status,
905
1234
  durationMs: Date.now() - startedAt,
906
1235
  }, "delegation finished");
907
- return JSON.stringify({
908
- status: childResult.status,
909
- finalText: childResult.finalText,
910
- costUsd: Number(childResult.costUsd),
911
- tokensIn: childResult.tokensIn,
912
- tokensOut: childResult.tokensOut,
913
- });
1236
+ return nativeChildResult(childResult, mergeOrderNote);
914
1237
  }
915
1238
  finally {
916
1239
  finishSibling();
917
1240
  release();
918
1241
  }
1242
+ };
1243
+ }
1244
+ return undefined;
1245
+ };
1246
+ /**
1247
+ * The only route to a built-in tool, in both execution modes (the in-process loop and the native
1248
+ * gateway's builtin.call). A tool that posts model-written text to a host, and a delegate_to_*
1249
+ * call (its task text becomes the child run's stored task), has every attached secret's value
1250
+ * masked in its arguments first; when the secrets cannot be read the call is refused and
1251
+ * neither the host nor a child run is reached. (The native gateway serves delegate_to_* through
1252
+ * durableDelegate, which masks the same way.)
1253
+ */
1254
+ const builtinHandler = (name) => {
1255
+ const handler = unguardedBuiltinHandler(name);
1256
+ if (!handler || !(POSTING_TOOL_NAMES.has(name) || name.startsWith(DELEGATE_TOOL_PREFIX)))
1257
+ return handler;
1258
+ return async (argsJson, toolCallId) => {
1259
+ const masked = await maskModelWrittenArgs({ db, secrets: providers.secrets, agentId: loaded.agentId, runId }, name, argsJson);
1260
+ if (masked === undefined) {
1261
+ return JSON.stringify({ error: "secret_check_unavailable", message: SECRET_CHECK_UNAVAILABLE_MESSAGE });
919
1262
  }
920
- const tool = toolsByName.get(name);
921
- if (!tool) {
922
- return JSON.stringify({
923
- error: "unknown_tool",
924
- message: `No tool named "${name}" is attached to this agent.`,
925
- });
926
- }
927
- let parsedArgs;
928
- try {
929
- // Some providers stream no JSON delta at all for a zero-parameter
930
- // tool call, yielding an empty argsJson rather than "{}".
931
- parsedArgs = JSON.parse(argsJson || "{}");
932
- }
933
- catch (err) {
934
- return JSON.stringify({
935
- error: "invalid_arguments_json",
936
- message: err instanceof Error ? err.message : String(err),
937
- });
938
- }
939
- const validation = await validateParams(tool.paramsZod, parsedArgs);
940
- if (!validation.ok) {
941
- return JSON.stringify({ error: "validation_failed", message: validation.errorMessage });
942
- }
943
- const result = await runInSandbox({
944
- code: tool.code,
945
- params: validation.value,
946
- agentId: loaded.agentId,
947
- datastore: scopeDatastore(providers.datastore, tool.allowedDatastorePrefixes),
948
- sharedDatastore: scopeSharedDatastoreAccessor(sharedDatastoreAccessor, tool.allowedSharedDatastorePrefixes),
949
- secrets: scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets),
950
- allowedFetchHosts: tool.allowedHosts,
951
- toolName: name,
1263
+ return handler(masked, toolCallId);
1264
+ };
1265
+ };
1266
+ /**
1267
+ * The privileged host for one call of the user tool `name`, scoped to that attachment's
1268
+ * consented capabilities. Undefined for a name that is not an attached user tool. In-process it
1269
+ * serves the sandbox directly; the native sandbox gateway serves a worker's bridge calls with it.
1270
+ */
1271
+ const scopedHost = (name, tool, signal, redactSecretValues) => createPrivilegedHost({
1272
+ redactSecretValues,
1273
+ agentId: loaded.agentId,
1274
+ datastore: scopeDatastore(providers.datastore, tool.allowedDatastorePrefixes),
1275
+ sharedDatastore: scopeSharedDatastoreAccessor(sharedDatastoreAccessor, tool.allowedSharedDatastorePrefixes),
1276
+ secrets: scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets),
1277
+ allowedFetchHosts: tool.allowedHosts,
1278
+ fetchImpl: providers.sandboxFetch,
1279
+ logTag: name,
1280
+ signal,
1281
+ });
1282
+ const privilegedHostFor = (name, signal, redactSecretValues) => {
1283
+ const tool = toolsByName.get(name);
1284
+ return tool ? scopedHost(name, tool, signal, redactSecretValues) : undefined;
1285
+ };
1286
+ /**
1287
+ * Every secret value the user tool `name` may use (readable or brokered), for console redaction
1288
+ * by a stateless gateway. Rejects when any read fails (readRedactionValues).
1289
+ */
1290
+ const readableSecretValues = async (name) => {
1291
+ const tool = toolsByName.get(name);
1292
+ if (!tool)
1293
+ return [];
1294
+ return readRedactionValues(scopeSecretsAccessor(secretsAccessor, tool.allowedSecrets), tool.allowedSecrets);
1295
+ };
1296
+ const runUserTool = async (name, argsJson) => {
1297
+ const tool = toolsByName.get(name);
1298
+ if (!tool) {
1299
+ return JSON.stringify({
1300
+ error: "unknown_tool",
1301
+ message: `No tool named "${name}" is attached to this agent.`,
1302
+ });
1303
+ }
1304
+ return runUserToolCall(tool, argsJson, (signal) => scopedHost(name, tool, signal));
1305
+ };
1306
+ const runSandboxTool = async (name, argsJson, callId) => {
1307
+ const builtin = builtinHandler(name);
1308
+ return builtin ? builtin(argsJson, callId) : runUserTool(name, argsJson);
1309
+ };
1310
+ return { toolsByName, builtinHandler, privilegedHostFor, readableSecretValues, runUserTool, runSandboxTool };
1311
+ }
1312
+ /**
1313
+ * Live progress for observers (MCP get_run, the viewer). Absolute totals, so a DBOS replay
1314
+ * re-writing them is harmless. Written into the run's own cost columns on purpose: the run
1315
+ * tree's shared budget (computeRunTreeSpend) then counts a running parent's spend so far, as it
1316
+ * already does for coding runs, whose proxy ledger writes their totals live. Best-effort: a
1317
+ * failed write only delays what observers see, and finishRun writes the final totals anyway.
1318
+ * A cancelled run takes them too: a stop lands mid-step, and that step's spend is real, while the
1319
+ * cancelled run's terminal write (failNativeRun) records no totals of its own.
1320
+ */
1321
+ export async function recordNativeRunProgress(db, runId, progress) {
1322
+ try {
1323
+ await db.run.updateMany({
1324
+ where: { id: runId, status: { in: ["running", "cancelled"] } },
1325
+ data: {
1326
+ turns: progress.turns,
1327
+ tokensIn: progress.usage.tokensIn,
1328
+ tokensOut: progress.usage.tokensOut,
1329
+ costUsd: progress.usage.costUsd,
1330
+ heartbeatAt: new Date(),
1331
+ },
1332
+ });
1333
+ }
1334
+ catch (err) {
1335
+ runnerLog.warn({ err, runId }, "failed to record run progress");
1336
+ }
1337
+ }
1338
+ /**
1339
+ * `texts` with every value of every secret attached to the run's agent masked, for a terminal
1340
+ * write: the stored row is what get_run, the viewer, host and issue status comments and run
1341
+ * notifications all read. Fails closed: when the secrets cannot be read, every non-null text is
1342
+ * replaced by WITHHELD_TEXT_NOTICE (the log line carries no text and no error message). A pure
1343
+ * function of the stored secrets, so a durable replay writes the same row.
1344
+ */
1345
+ async function maskRunTexts(ctx, texts) {
1346
+ const entries = Object.entries(texts);
1347
+ if (!entries.some(([, text]) => typeof text === "string" && text.length > 0))
1348
+ return texts;
1349
+ let mask;
1350
+ try {
1351
+ const row = await ctx.db.run.findUnique({ where: { id: ctx.runId }, select: { agentId: true } });
1352
+ if (!row)
1353
+ throw new Error("run not found");
1354
+ const named = await loadAgentSecretValues(ctx.db, ctx.providers.secrets, row.agentId);
1355
+ mask = (text) => maskNamedSecrets(text, named);
1356
+ }
1357
+ catch (err) {
1358
+ runnerLog.warn({
1359
+ runId: ctx.runId,
1360
+ event: "run_text_secret_check_failed",
1361
+ errorName: err instanceof Error ? err.name : typeof err,
1362
+ }, "could not read the agent's secrets to check the run's final text and error; storing the withheld notice");
1363
+ mask = () => WITHHELD_TEXT_NOTICE;
1364
+ }
1365
+ return Object.fromEntries(entries.map(([key, text]) => [key, typeof text === "string" && text.length > 0 ? mask(text) : text]));
1366
+ }
1367
+ /** Everything after the terminal write, shared by the normal and backstop paths. */
1368
+ async function settleFinishedNativeRun(ctx, finished, claimed) {
1369
+ const { runId, db, providers, reviewHosts, repoAccess, issueTrackers } = ctx;
1370
+ await closeOpenHostCheck(db, finished, reviewHosts);
1371
+ await completeHostStatus(db, finished, reviewHosts);
1372
+ await completeIssueStatus(db, finished, issueTrackers);
1373
+ // After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
1374
+ if (claimed)
1375
+ await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
1376
+ // A tree root is the lead of a request: once it is terminal every coding run it delegated is
1377
+ // too, so the reviews of their pull requests that waited for it start now, after the related
1378
+ // sections above are current (#259). Not gated on `claimed`: release is claimed per row, and a
1379
+ // lead another finalizer ended (a cancellation) should not leave them to the sweep. A run that
1380
+ // delegated nothing has no rows: one indexed lookup. Never throws.
1381
+ if (finished.parentRunId === null && providers.executor && reviewHosts && repoAccess) {
1382
+ await startDeferredForRequest({ db, executor: providers.executor, hosts: reviewHosts, repoAccess, issueTrackers }, runId).catch((err) => runnerLog.warn({ err, runId }, "could not start the reviews waiting for this run"));
1383
+ }
1384
+ if (claimed && providers.executor && reviewHosts && repoAccess) {
1385
+ await startReviewFixAfterReview(runId, {
1386
+ db,
1387
+ executor: providers.executor,
1388
+ hosts: reviewHosts,
1389
+ repoAccess,
1390
+ issueTrackers,
1391
+ });
1392
+ }
1393
+ // Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
1394
+ // filed twice. Bounded and never throws.
1395
+ if (claimed)
1396
+ await fileSelfDefect(db, issueTrackers, finished);
1397
+ // Only the finalizer that made the row terminal notifies. Never throws.
1398
+ if (claimed)
1399
+ await emitRunFinishedEvents(db, finished);
1400
+ // A lead's end posts the `wardby merge order` check on each open pull request of its set,
1401
+ // once every delegated coding run has opened its PR (#261); last, after the release above (so
1402
+ // no review waits on it) and after the follow-ups and notifications (so none is delayed by its
1403
+ // host reads). Only for a tree root that delegated. Not gated on `claimed`: the sync is
1404
+ // idempotent (an unchanged check writes nothing). Never throws.
1405
+ await syncMergeOrderChecksAfterRun({ db, hosts: reviewHosts }, finished);
1406
+ // A reviewer run's end may be the last thing a pull request waited for: mark it ready for review
1407
+ // when its builder link opted in (#287). Last, so nothing above waits on its host reads; idempotent
1408
+ // (claimed once per pull request); no host reads unless a link opted in. Never throws.
1409
+ await markReadyAfterRun({ db, hosts: reviewHosts, log: runnerLog }, runId);
1410
+ return finished;
1411
+ }
1412
+ /** The terminal write for an engine result, then its usage, host/issue status, and follow-ups. */
1413
+ export async function finishNativeRun(ctx, model, result) {
1414
+ const texts = await maskRunTexts(ctx, { error: result.error ?? null, finalText: result.finalText || null });
1415
+ const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
1416
+ status: result.status,
1417
+ tokensIn: result.usage.tokensIn,
1418
+ tokensOut: result.usage.tokensOut,
1419
+ costUsd: result.usage.costUsd,
1420
+ error: texts.error,
1421
+ finalText: texts.finalText,
1422
+ turns: result.turns,
1423
+ finishedAt: new Date(),
1424
+ });
1425
+ // Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
1426
+ await recordNativeModelUsage(ctx.db, ctx.runId, model, result.usage);
1427
+ return settleFinishedNativeRun(ctx, finished, claimed);
1428
+ }
1429
+ /** The backstop terminal write for a run that threw. A cancellation is not a failure: it carries the operator's own reason. */
1430
+ export async function failNativeRun(ctx, err) {
1431
+ const { error } = await maskRunTexts(ctx, { error: err instanceof Error ? err.message : String(err) });
1432
+ const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
1433
+ status: err instanceof RunCancelledError || err instanceof SandboxRunCancelledError ? "cancelled" : "failed",
1434
+ error,
1435
+ finishedAt: new Date(),
1436
+ });
1437
+ return settleFinishedNativeRun(ctx, finished, claimed);
1438
+ }
1439
+ /** The terminal write for a run that cannot continue (it ends `lost` with `reason`), then the same settle path. */
1440
+ export async function loseNativeRun(ctx, reason) {
1441
+ const { error } = await maskRunTexts(ctx, { error: reason });
1442
+ const { run: finished, claimed } = await finishRunClaimed(ctx.db, ctx.runId, {
1443
+ status: "lost",
1444
+ error,
1445
+ finishedAt: new Date(),
1446
+ });
1447
+ if (claimed)
1448
+ await stopChildrenOfLostRun(ctx.db, ctx.providers.executor, ctx.runId);
1449
+ return settleFinishedNativeRun(ctx, finished, claimed);
1450
+ }
1451
+ /** Where a run stands for the native gateway: still drivable, cancelled (its task), or otherwise ended. */
1452
+ export async function runDrivability(db, runId) {
1453
+ const run = await db.run.findUnique({ where: { id: runId }, select: { status: true } });
1454
+ if (!run || !DRIVABLE.includes(run.status)) {
1455
+ return run?.status === "cancelled" ? "cancelled" : "ended";
1456
+ }
1457
+ const cancelledTask = await db.task.findFirst({ where: { runId, status: "cancelled" }, select: { id: true } });
1458
+ return cancelledTask ? "cancelled" : "drivable";
1459
+ }
1460
+ /**
1461
+ * What a sandbox worker is given: the pinned agent and tool definitions, which names are built-ins
1462
+ * (served by the gateway), and each user tool's code and schema only — never its capabilities,
1463
+ * secret names, or values, which stay with the gateway's privileged host.
1464
+ */
1465
+ export function sandboxWorkerInput(runId, loaded, tools) {
1466
+ if (!loaded.pricing) {
1467
+ throw new Error("native_sandbox_requires_catalog: a sandbox run needs its model's catalog entry, and none was recorded.");
1468
+ }
1469
+ return {
1470
+ v: NATIVE_WORKER_PROTOCOL_VERSION,
1471
+ runId,
1472
+ agent: loaded.agent,
1473
+ tools: loaded.tools,
1474
+ builtinTools: loaded.tools.map((tool) => tool.name).filter((name) => tools.builtinHandler(name) !== undefined),
1475
+ userTools: Object.fromEntries([...tools.toolsByName].map(([name, tool]) => [name, { code: tool.code, paramsZod: tool.paramsZod }])),
1476
+ runsConcurrently: loaded.parallelDelegations
1477
+ ? loaded.tools.map((tool) => tool.name).filter((name) => name.startsWith(DELEGATE_TOOL_PREFIX))
1478
+ : [],
1479
+ pricing: { ...loaded.pricing.entry, efforts: [...loaded.pricing.entry.efforts] },
1480
+ };
1481
+ }
1482
+ /**
1483
+ * A tool call's model-written arguments with every secret value attached to the agent masked, or
1484
+ * undefined (logged by error name only) when the secrets cannot be read and the call must be refused.
1485
+ */
1486
+ async function maskModelWrittenArgs(ctx, tool, argsJson) {
1487
+ try {
1488
+ const named = await loadAgentSecretValues(ctx.db, ctx.secrets, ctx.agentId);
1489
+ return maskNamedSecrets(argsJson, named);
1490
+ }
1491
+ catch (err) {
1492
+ runnerLog.warn({
1493
+ runId: ctx.runId,
1494
+ tool,
1495
+ event: "post_secret_check_failed",
1496
+ errorName: err instanceof Error ? err.name : typeof err,
1497
+ }, "could not read the agent's secrets to check a posted text; refusing the call");
1498
+ return undefined;
1499
+ }
1500
+ }
1501
+ /** Long-poll window: well under common proxy and load-balancer idle timeouts. */
1502
+ export const DELEGATION_POLL_WINDOW_MS = 20_000;
1503
+ /**
1504
+ * A delegate_to_* call from a sandboxed run, served by the native gateway. The in-process branch
1505
+ * holds its admission, budget wait, and child wait in one process's memory for the child's whole
1506
+ * life; this one keeps them on the call's ledger row, so the worker can call again with the same
1507
+ * callId — on any control-plane replica, before or after a restart — and continue where it was:
1508
+ *
1509
+ * - every child, native or coding, is a managed run dispatched through dispatchRun and the
1510
+ * Executor (never inline: an inline child dies with the replica, and the reconciler does not
1511
+ * reap unmanaged runs);
1512
+ * - admission re-checks the per-run delegation limit under a run-scoped advisory lock inside the
1513
+ * child's own transaction, so parallel delegations on different replicas cannot exceed it;
1514
+ * - a delegation the budget would refuse while sibling children still run is recorded as waiting
1515
+ * and re-evaluated on each call (parallelDelegations), up to the same bound as in process;
1516
+ * - each call waits on the child row up to `pollWindowMs`, then answers `pending`.
1517
+ *
1518
+ * Every refusal and result uses the same helpers, and so the same messages, as in process.
1519
+ */
1520
+ export async function durableDelegate(ctx, callId, name, argsJson) {
1521
+ const { runId, existingRun, loaded, providers, db, ledger, sessionId } = ctx;
1522
+ const now = ctx.now ?? Date.now;
1523
+ const sleep = ctx.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
1524
+ const finish = async (result) => {
1525
+ await ledger.recordResult(sessionId, callId, result);
1526
+ return { result };
1527
+ };
1528
+ const claimed = await ledger.claim(sessionId, callId, "builtin.call");
1529
+ if (claimed.outcome === "done")
1530
+ return { result: String(claimed.result) };
1531
+ const state = await ledger.delegation(sessionId, callId);
1532
+ const startedAt = state?.createdAt.getTime() ?? now();
1533
+ const boundName = name.slice(DELEGATE_TOOL_PREFIX.length);
1534
+ const edge = loaded.subAgentEdges.find((e) => e.boundName === boundName);
1535
+ if (!edge) {
1536
+ return finish(JSON.stringify({ error: "no_such_subagent", message: `No sub-agent is bound to name "${boundName}".` }));
1537
+ }
1538
+ const executor = providers.executor;
1539
+ if (!executor) {
1540
+ return finish(JSON.stringify({
1541
+ error: "delegation_unavailable",
1542
+ message: "This execution context has no Executor wired in, so the sub-agent cannot be dispatched.",
1543
+ }));
1544
+ }
1545
+ const parsedArgs = parseDelegateArgs(argsJson);
1546
+ if ("refusal" in parsedArgs)
1547
+ return finish(parsedArgs.refusal);
1548
+ const { args } = parsedArgs;
1549
+ const authorized = await authorizeDelegation(db, loaded.agentId, edge, boundName, args);
1550
+ if ("refusal" in authorized)
1551
+ return finish(authorized.refusal);
1552
+ const { childAgent } = authorized;
1553
+ const coding = childAgent.kind === "coding";
1554
+ const { queueTimeoutSec } = loadCodingConcurrencyConfig();
1555
+ let childRunId = state?.childRunId ?? null;
1556
+ if (!childRunId && state?.childAgentId === edge.childAgentId) {
1557
+ // A replica that died between dispatching the child and recording it left the child row:
1558
+ // adopt it rather than refusing this very delegation as a duplicate.
1559
+ childRunId = (await findDelegationChild(db, runId, edge.childAgentId, callId))?.id ?? null;
1560
+ // Recorded, so later calls wait on it by id.
1561
+ if (childRunId) {
1562
+ await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId, childRunId });
1563
+ }
1564
+ }
1565
+ if (!childRunId) {
1566
+ // About to create a child: its task is model-written and stored, so mask the parent's secret
1567
+ // values first. Only here, never on a poll of an existing child (a transient secret-read
1568
+ // failure must not orphan a running child). Recorded as the call's result, which also completes a waiting row; the model's retry comes with a new callId.
1569
+ const maskedArgsJson = await maskModelWrittenArgs({ db, secrets: providers.secrets, agentId: loaded.agentId, runId }, name, argsJson);
1570
+ if (maskedArgsJson === undefined) {
1571
+ return finish(JSON.stringify({ error: "secret_check_unavailable", message: SECRET_CHECK_UNAVAILABLE_MESSAGE }));
1572
+ }
1573
+ const maskedArgs = parseDelegateArgs(maskedArgsJson);
1574
+ if ("refusal" in maskedArgs)
1575
+ return finish(maskedArgs.refusal);
1576
+ const dispatchArgs = maskedArgs.args;
1577
+ const limit = loaded.maxDelegationsPerRun ?? 1;
1578
+ const refusalNow = async (reader) => {
1579
+ const prior = await reader.run.findMany({ where: { parentRunId: runId }, select: { agentId: true } });
1580
+ return delegationLimitRefusal({
1581
+ priorChildAgentIds: prior.map((p) => p.agentId),
1582
+ waitingChildAgentIds: await ledger.waitingChildAgentIds(sessionId, callId),
1583
+ childAgentId: edge.childAgentId,
1584
+ boundName,
1585
+ limit,
952
1586
  });
953
- if (!result.ok) {
954
- return JSON.stringify({ error: result.errorKind, message: result.errorMessage });
955
- }
956
- return JSON.stringify(result.value);
957
1587
  };
958
- // Live progress for observers (MCP get_run, the viewer). Absolute totals, so a DBOS replay
959
- // re-writing them is harmless. Written into the run's own cost columns on purpose: the run
960
- // tree's shared budget (computeRunTreeSpend) then counts a running parent's spend so far, as it
961
- // already does for coding runs, whose proxy ledger writes their totals live. Best-effort: a
962
- // failed write only delays what observers see, and finishRun writes the final totals anyway.
963
- const onProgress = async (progress) => {
964
- try {
965
- await db.run.updateMany({
966
- where: { id: runId, status: "running" },
967
- data: {
968
- turns: progress.turns,
969
- tokensIn: progress.usage.tokensIn,
970
- tokensOut: progress.usage.tokensOut,
971
- costUsd: progress.usage.costUsd,
972
- heartbeatAt: new Date(),
973
- },
974
- });
1588
+ const limitRefusal = await refusalNow(db);
1589
+ if (limitRefusal)
1590
+ return finish(limitRefusal);
1591
+ // parallelDelegations: a child the budget would refuse while a sibling still runs waits for a
1592
+ // sibling to finish and free its hold, up to the child's own wait bound. With no sibling
1593
+ // running it goes ahead and is refused at dispatch or at its first turn, as in process.
1594
+ if (loaded.parallelDelegations) {
1595
+ const siblings = await db.run.count({ where: { parentRunId: runId, status: { in: [...DRIVABLE] } } });
1596
+ if (siblings > 0) {
1597
+ const runTimeoutSec = coding ? (childAgent.codingProfile?.timeoutSec ?? 0) : 0;
1598
+ const boundMs = (queueTimeoutSec + runTimeoutSec + CODING_CHILD_WAIT_GRACE_SEC) * 1000;
1599
+ const { exhaustedBy } = await effectiveBudgetForRun(db, childAgent, new Date(), runId);
1600
+ if (exhaustedBy && now() - startedAt < boundMs) {
1601
+ await ledger.setDelegation(sessionId, callId, { status: "waiting_budget", childAgentId: edge.childAgentId });
1602
+ return { pending: true };
1603
+ }
975
1604
  }
976
- catch (err) {
977
- runnerLog.warn({ err, runId }, "failed to record run progress");
1605
+ }
1606
+ // Recorded before dispatch, so a replica dying in between leaves a row the next call adopts.
1607
+ await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId });
1608
+ let dispatched;
1609
+ let refusedUnderLock = null;
1610
+ try {
1611
+ dispatched = await dispatchRun({
1612
+ db,
1613
+ executor,
1614
+ selfDefects: { db, issueTrackers: ctx.issueTrackers },
1615
+ agentId: edge.childAgentId,
1616
+ trigger: "subagent",
1617
+ ...(coding
1618
+ ? {
1619
+ codingTask: dispatchArgs.task,
1620
+ continuesCodingRunId: dispatchArgs.continuePriorRun,
1621
+ mergeOrder: dispatchArgs.mergeOrder,
1622
+ }
1623
+ : {
1624
+ taskOverride: delegationTaskOverride(dispatchArgs),
1625
+ grantedParentMemoryKeys: dispatchArgs.grantParentMemoryKeys ?? [],
1626
+ }),
1627
+ parentRunId: runId,
1628
+ parentCallId: callId,
1629
+ // The child's result flows back into this run, which the triggerer sees, so the child is visible to them too.
1630
+ triggeredById: existingRun.triggeredById,
1631
+ beforePersist: async (tx) => {
1632
+ // One admission per parent at a time, across every replica.
1633
+ await tx.$queryRaw `SELECT 1 AS locked FROM (SELECT pg_advisory_xact_lock(hashtext(${runId}))) AS l`;
1634
+ refusedUnderLock = await refusalNow(tx);
1635
+ return refusedUnderLock === null;
1636
+ },
1637
+ });
1638
+ }
1639
+ catch (err) {
1640
+ if (err instanceof ContinuationRefusedError) {
1641
+ return finish(JSON.stringify({
1642
+ error: "continuation_refused",
1643
+ message: `${err.message} No sub-agent run was started. Do not open a new pull request in its place: ` +
1644
+ "tell the requester that this wardby deployment cannot continue that pull request's branch.",
1645
+ }));
978
1646
  }
979
- };
1647
+ // Recorded as the call's result, so the model can report it and a retried callId never
1648
+ // dispatches twice. Only the leading code is exposed: messages can carry internals.
1649
+ const code = declaredCodeOf(err);
1650
+ const values = await ctx.secretValues?.().catch(() => undefined);
1651
+ logUnexpectedError(runnerLog, err, values, "delegation dispatch failed", { runId, boundName });
1652
+ return finish(JSON.stringify({
1653
+ error: "dispatch_failed",
1654
+ message: `The sub-agent run could not be created${code ? ` (${code})` : ""}.`,
1655
+ }));
1656
+ }
1657
+ if (!dispatched) {
1658
+ return finish(refusedUnderLock ??
1659
+ JSON.stringify({ error: "dispatch_failed", message: "The sub-agent run could not be created." }));
1660
+ }
1661
+ childRunId = dispatched.run.id;
1662
+ await ledger.setDelegation(sessionId, callId, { status: "pending", childAgentId: edge.childAgentId, childRunId });
1663
+ runnerLog.info({ runId, childRunId, boundName, kind: childAgent.kind, durable: true }, "delegation started");
1664
+ }
1665
+ const codingRun = coding
1666
+ ? await db.codingRun.findUnique({ where: { runId: childRunId }, select: { timeoutSec: true } })
1667
+ : null;
1668
+ const boundMs = coding
1669
+ ? (queueTimeoutSec + (codingRun?.timeoutSec ?? 0) + CODING_CHILD_WAIT_GRACE_SEC) * 1000
1670
+ : SANDBOX_CHILD_WAIT_SEC * 1000;
1671
+ const windowEnd = now() + (ctx.pollWindowMs ?? DELEGATION_POLL_WINDOW_MS);
1672
+ for (;;) {
1673
+ const child = await db.run.findUniqueOrThrow({ where: { id: childRunId } });
1674
+ if (!DRIVABLE.includes(child.status)) {
1675
+ runnerLog.info({ runId, childRunId, boundName, outcome: child.status, durable: true }, "delegation finished");
1676
+ if (!coding) {
1677
+ return finish(nativeChildResult(child, args.mergeOrder !== undefined ? MERGE_ORDER_IGNORED_NOTE : undefined));
1678
+ }
1679
+ const stored = await db.codingRun
1680
+ .findUnique({ where: { runId: childRunId }, select: { result: true } })
1681
+ .catch(() => null);
1682
+ return finish(codingChildResult(child, stored?.result, await loadBudgetEvent(db, childRunId)));
1683
+ }
1684
+ if (now() - startedAt >= boundMs) {
1685
+ await executor.stop(childRunId, "sub-agent wait timed out").catch((err) => {
1686
+ runnerLog.warn({ err, childRunId, runId }, "failed to stop a timed-out sub-agent run");
1687
+ });
1688
+ return finish(JSON.stringify({
1689
+ error: "subagent_wait_timed_out",
1690
+ runId: childRunId,
1691
+ message: coding
1692
+ ? "The coding sub-agent did not finish within its queue timeout plus run timeout; it was stopped and produced no result."
1693
+ : "The sub-agent did not finish in time; it was stopped and produced no result.",
1694
+ }));
1695
+ }
1696
+ const remaining = windowEnd - now();
1697
+ if (remaining <= 0)
1698
+ return { pending: true };
1699
+ await sleep(Math.min(ctx.pollIntervalMs ?? 1_000, remaining));
1700
+ }
1701
+ }
1702
+ /** The integrations a native run uses, as executeRun derives them from its providers. */
1703
+ export function nativeRunIntegrations(providers, db) {
1704
+ const reviewHosts = configuredReviewHosts(providers.reviewHosts);
1705
+ const repoAccess = reviewHosts
1706
+ ? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
1707
+ : undefined;
1708
+ return { reviewHosts, repoAccess, issueTrackers: configuredIssueTrackers(providers.issueTrackers) };
1709
+ }
1710
+ /** How long a sandbox run's gateway session (and so its capability) stays valid. */
1711
+ export const SANDBOX_RUN_MAX_SEC = SANDBOX_CHILD_WAIT_SEC;
1712
+ /**
1713
+ * Starts a sandbox-mode run's trusted half: loads and pins the run, marks it running, records its
1714
+ * gateway session (capability hash, deadline, budget, snapshot), and records `native-sandbox` as
1715
+ * its execution backend — all before any worker exists. The caller launches the worker with the
1716
+ * returned input, which carries the gateway URL and the one-time capability.
1717
+ */
1718
+ export async function createSandboxSession(options) {
1719
+ const { runId, providers, db, ledger } = options;
1720
+ const now = options.now ?? new Date();
1721
+ const existingRun = await db.run.findUnique({ where: { id: runId } });
1722
+ if (!existingRun)
1723
+ throw new Error(`Unknown run "${runId}".`);
1724
+ if (!DRIVABLE.includes(existingRun.status))
1725
+ return { kind: "ended", run: existingRun };
1726
+ if (existingRun.nativeExecutionMode !== "sandbox")
1727
+ throw new Error(`Run "${runId}" is not a sandbox-mode run.`);
1728
+ const { reviewHosts, repoAccess, issueTrackers } = nativeRunIntegrations(providers, db);
1729
+ const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
1730
+ const loadedOrUnavailable = await loadNativeRun({
1731
+ runId,
1732
+ existingRun,
1733
+ providers,
1734
+ db,
1735
+ step: runStepInline,
1736
+ reviewHosts,
1737
+ issueTrackers,
1738
+ });
1739
+ if ("unavailable" in loadedOrUnavailable) {
1740
+ return { kind: "ended", run: await failNativeRun(finishContext, new Error(loadedOrUnavailable.unavailable)) };
1741
+ }
1742
+ const loaded = loadedOrUnavailable;
1743
+ if (loaded.kind === "coding") {
1744
+ return { kind: "ended", run: await failNativeRun(finishContext, new Error(CODING_EXECUTOR_NOT_CONFIGURED)) };
1745
+ }
1746
+ const tools = createNativeRunTools({
1747
+ runId,
1748
+ existingRun,
1749
+ loaded,
1750
+ providers,
1751
+ db,
1752
+ reviewHosts,
1753
+ repoAccess,
1754
+ issueTrackers,
1755
+ });
1756
+ let input;
1757
+ try {
1758
+ input = sandboxWorkerInput(runId, loaded, tools);
1759
+ }
1760
+ catch (err) {
1761
+ return { kind: "ended", run: await failNativeRun(finishContext, err) };
1762
+ }
1763
+ await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
1764
+ const capability = randomBytes(32).toString("base64url");
1765
+ const snapshot = { input, loaded };
1766
+ const session = await ledger.createSession({
1767
+ runId,
1768
+ capabilityHash: sandboxCapabilityHash(capability),
1769
+ deadlineAt: new Date(now.getTime() + SANDBOX_RUN_MAX_SEC * 1000),
1770
+ budgetUsd: loaded.agent.budgetUsd,
1771
+ snapshot: snapshot,
1772
+ networkReadyAt: options.networkReady === false ? null : now,
1773
+ });
1774
+ // The handle exists before any worker does, so the reconciler routes a stale run to the sandbox executor.
1775
+ await db.run.updateMany({
1776
+ where: { id: runId, executionBackend: null },
1777
+ data: { executionBackend: NATIVE_SANDBOX_BACKEND },
1778
+ });
1779
+ return {
1780
+ kind: "started",
1781
+ sessionId: session.id,
1782
+ capability,
1783
+ input: { ...input, gateway: { url: options.gatewayUrl, capability } },
1784
+ secretValues: () => userToolSecretValues(tools, Object.keys(input.userTools)),
1785
+ };
1786
+ }
1787
+ /**
1788
+ * Every secret value the sandbox run's user tools `names` may read (readable or brokered): what its
1789
+ * worker can be handed, so what the gateway checks its cause codes against and a launcher masks
1790
+ * from the worker's output tail. Rejects when any read fails (readRedactionValues).
1791
+ */
1792
+ export async function userToolSecretValues(tools, names) {
1793
+ return (await Promise.all(names.map((name) => tools.readableSecretValues(name)))).flat();
1794
+ }
1795
+ /** Run.executionBackend for a sandbox-mode run whose worker is (or was) launched. */
1796
+ export const NATIVE_SANDBOX_BACKEND = "native-sandbox";
1797
+ /** The stored form of a sandbox capability: never the token itself. */
1798
+ export function sandboxCapabilityHash(capability) {
1799
+ return createHash("sha256").update(capability).digest("hex");
1800
+ }
1801
+ /** Drives an existing Run (created by `createRun` or the scheduler) to a terminal state. */
1802
+ export async function executeRun(runId, providers, db = defaultDb, onText, step = runStepInline) {
1803
+ // Counted so a shutdown waits for this run instead of abandoning it (core/in-flight-runs.ts).
1804
+ return trackRun(runId, () => executeTrackedRun(runId, providers, db, onText, step));
1805
+ }
1806
+ async function executeTrackedRun(runId, providers, db, onText, step) {
1807
+ const existingRun = await db.run.findUnique({ where: { id: runId } });
1808
+ if (!existingRun) {
1809
+ throw new Error(`Unknown run "${runId}".`);
1810
+ }
1811
+ // Pre-flight guard. A run that already reached a terminal state must never
1812
+ // be re-driven: a durable workflow re-dispatched after the reconciler
1813
+ // reaped its row (rollback to EXECUTOR=in-process, then roll forward) would
1814
+ // otherwise re-spend the whole run against a `lost` row, and a duplicate
1815
+ // attempt of a finished run would spend a second time for a result no write
1816
+ // can land. Cheaper and clearer than letting it run and discarding the
1817
+ // result at the conditional write.
1818
+ if (!DRIVABLE.includes(existingRun.status)) {
1819
+ runnerLog.info({ runId, status: existingRun.status }, "skipping execution of an already-terminal run");
1820
+ return existingRun;
1821
+ }
1822
+ const reviewHosts = configuredReviewHosts(providers.reviewHosts);
1823
+ const repoAccess = reviewHosts
1824
+ ? (providers.repoAccess ?? createRepoAccessGate({ db, hosts: reviewHosts }))
1825
+ : undefined;
1826
+ const issueTrackers = configuredIssueTrackers(providers.issueTrackers);
1827
+ // Inline execution checks for a stop before every step (stopAwareStep); a durable executor passes
1828
+ // its own step runner, which does its own check (DbosExecutor wraps it in stopAwareStep too).
1829
+ if (step === runStepInline)
1830
+ step = stopAwareStep(db, runId);
1831
+ // Fails a run that ends before its engine starts, closing what its trigger opened (the review
1832
+ // check, the host and issue status comments) exactly as the normal and catch paths below do.
1833
+ // Deliberately no self-defect: every caller is a configuration state, not this run's defect (an
1834
+ // admin disabled or removed the model, or this deployment has no coding or native sandbox
1835
+ // executor), and filing would open one defect per affected agent rather than describe a failure
1836
+ // of that agent.
1837
+ const finishEarly = async (reason) => {
1838
+ const { error } = await maskRunTexts({ runId, db, providers }, { error: reason });
1839
+ const finished = await finishRun(db, runId, { status: "failed", error, finishedAt: new Date() });
1840
+ await closeOpenHostCheck(db, finished, reviewHosts);
1841
+ await completeHostStatus(db, finished, reviewHosts);
1842
+ await completeIssueStatus(db, finished, issueTrackers);
1843
+ await emitRunFinishedEvents(db, finished);
1844
+ return finished;
1845
+ };
1846
+ // Decided by the run's own snapshot, before any work or spend (no pricing pin, no engine): a
1847
+ // sandbox-mode run never executes in the control plane.
1848
+ const sandboxed = existingRun.nativeExecutionMode === "sandbox";
1849
+ if (sandboxed && !providers.nativeSandbox)
1850
+ return finishEarly(NATIVE_SANDBOX_NOT_CONFIGURED);
1851
+ const loadedOrUnavailable = await loadNativeRun({
1852
+ runId,
1853
+ existingRun,
1854
+ providers,
1855
+ db,
1856
+ step,
1857
+ reviewHosts,
1858
+ issueTrackers,
1859
+ });
1860
+ if ("unavailable" in loadedOrUnavailable)
1861
+ return finishEarly(loadedOrUnavailable.unavailable);
1862
+ const loaded = loadedOrUnavailable;
1863
+ if (loaded.kind === "coding")
1864
+ return finishEarly(CODING_EXECUTOR_NOT_CONFIGURED);
1865
+ // Conditional on DRIVABLE rather than on `pending`: an adopted attempt
1866
+ // legitimately finds the row already `running`, but a terminal row must
1867
+ // never be flipped back to `running`.
1868
+ await db.run.updateMany({ where: { id: runId, status: { in: [...DRIVABLE] } }, data: { status: "running" } });
1869
+ const finishContext = { runId, db, providers, reviewHosts, repoAccess, issueTrackers };
1870
+ try {
1871
+ const tools = createNativeRunTools({
1872
+ runId,
1873
+ existingRun,
1874
+ loaded,
1875
+ providers,
1876
+ db,
1877
+ reviewHosts,
1878
+ repoAccess,
1879
+ issueTrackers,
1880
+ });
1881
+ const { runSandboxTool } = tools;
1882
+ const onProgress = (progress) => recordNativeRunProgress(db, runId, progress);
1883
+ const llm = loaded.pricing && providers.llm instanceof RoutingLlmProvider
1884
+ ? providers.llm.forRun(loaded.pricing.entry)
1885
+ : providers.llm;
1886
+ if (sandboxed && providers.nativeSandbox) {
1887
+ // The turn loop runs in a worker; this process serves its calls from the same tools and LLM.
1888
+ const engineResult = await runSandboxedEngine({
1889
+ runId,
1890
+ input: sandboxWorkerInput(runId, loaded, tools),
1891
+ ctx: { providers: { llm }, onText, onProgress },
1892
+ builtinHandler: tools.builtinHandler,
1893
+ privilegedHostFor: tools.privilegedHostFor,
1894
+ readableSecretValues: tools.readableSecretValues,
1895
+ drivability: () => runDrivability(db, runId),
1896
+ launcher: providers.nativeSandbox,
1897
+ });
1898
+ return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
1899
+ }
980
1900
  const engineResult = await providers.engine.run({
981
1901
  agent: loaded.agent,
982
1902
  tools: loaded.tools,
983
- providers: {
984
- llm: loaded.pricing && providers.llm instanceof RoutingLlmProvider
985
- ? providers.llm.forRun(loaded.pricing.entry)
986
- : providers.llm,
987
- },
1903
+ providers: { llm },
988
1904
  runSandboxTool,
989
1905
  // A replay recorded before this flag existed has none: its delegations stay sequential.
990
1906
  ...(loaded.parallelDelegations
@@ -994,70 +1910,26 @@ async function executeTrackedRun(runId, providers, db, onText, step) {
994
1910
  onProgress,
995
1911
  step,
996
1912
  });
997
- const { run: finished, claimed } = await finishRunClaimed(db, runId, {
998
- status: engineResult.status,
999
- tokensIn: engineResult.usage.tokensIn,
1000
- tokensOut: engineResult.usage.tokensOut,
1001
- costUsd: engineResult.usage.costUsd,
1002
- error: engineResult.error ?? null,
1003
- finalText: engineResult.finalText || null,
1004
- turns: engineResult.turns,
1005
- finishedAt: new Date(),
1006
- });
1007
- // Per-model usage is written on the same path as Run.costUsd: any future mid-run cost write must write usage too.
1008
- await recordNativeModelUsage(db, runId, loaded.agent.model, engineResult.usage);
1009
- await closeOpenHostCheck(db, finished, reviewHosts);
1010
- await completeHostStatus(db, finished, reviewHosts);
1011
- await completeIssueStatus(db, finished, issueTrackers);
1012
- // After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
1013
- if (claimed)
1014
- await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
1015
- if (claimed && providers.executor && reviewHosts && repoAccess) {
1016
- await startReviewFixAfterReview(runId, {
1017
- db,
1018
- executor: providers.executor,
1019
- hosts: reviewHosts,
1020
- repoAccess,
1021
- issueTrackers,
1022
- });
1023
- }
1024
- // Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
1025
- // filed twice. Bounded and never throws.
1026
- if (claimed)
1027
- await fileSelfDefect(db, issueTrackers, finished);
1028
- return finished;
1913
+ return await finishNativeRun(finishContext, loaded.agent.model, engineResult);
1029
1914
  }
1030
1915
  catch (err) {
1031
1916
  // Defensive backstop: the engine is expected to catch its own errors
1032
1917
  // and return a "failed" EngineResult, but an unexpected throw here
1033
1918
  // (a real bug, or tool-loading failing outside the per-tool try above)
1034
- // must still never leave the run dangling in "running". A cancellation
1035
- // is not a failure: it carries the operator's own reason.
1036
- const { run: finished, claimed } = await finishRunClaimed(db, runId, {
1037
- status: err instanceof RunCancelledError ? "cancelled" : "failed",
1038
- error: err instanceof Error ? err.message : String(err),
1039
- finishedAt: new Date(),
1040
- });
1041
- await closeOpenHostCheck(db, finished, reviewHosts);
1042
- await completeHostStatus(db, finished, reviewHosts);
1043
- await completeIssueStatus(db, finished, issueTrackers);
1044
- // After the issue status, so IssuePullRequest rows for this run exist. Bounded and never throws.
1045
- if (claimed)
1046
- await updateRelatedPullRequests(db, finished, reviewHosts, issueTrackers);
1047
- if (claimed && providers.executor && reviewHosts && repoAccess) {
1048
- await startReviewFixAfterReview(runId, {
1049
- db,
1050
- executor: providers.executor,
1051
- hosts: reviewHosts,
1052
- repoAccess,
1053
- issueTrackers,
1054
- });
1919
+ // must still never leave the run dangling in "running".
1920
+ if (err instanceof RunOwnershipLostError) {
1921
+ // Another execution of this run recorded a step first and owns the run now. Write nothing
1922
+ // and fire no settle side effects: the winner makes the terminal write. Rethrown (not
1923
+ // returned) so the durable executor hands the conflict back to its SDK, which parks this
1924
+ // attempt instead of recording a workflow outcome under the winner.
1925
+ logRunOwnershipLost(runId, err);
1926
+ throw err;
1055
1927
  }
1056
- // Only the call that made the row terminal files, so a run another finalizer (the reconciler) ended is not
1057
- // filed twice. Bounded and never throws.
1058
- if (claimed)
1059
- await fileSelfDefect(db, issueTrackers, finished);
1060
- return finished;
1928
+ // A replay that no longer matches its record (a run recorded by a release whose steps differ)
1929
+ // cannot continue: it ends lost, not failed, through the same settle path.
1930
+ if (err instanceof StepReplayMismatchError)
1931
+ return loseNativeRun(finishContext, STEPS_CHANGED_REASON);
1932
+ return failNativeRun(finishContext, err);
1061
1933
  }
1062
1934
  }
1063
1935
  /**