@wardby/cli 0.6.0 → 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 (576) hide show
  1. package/deploy/local/docker-compose.quickstart-coding.yml +13 -0
  2. package/dist/claude-coding-worker/driver.d.ts +21 -3
  3. package/dist/claude-coding-worker/driver.js +125 -17
  4. package/dist/claude-coding-worker/main.js +34 -6
  5. package/dist/claude-coding-worker/sdk.js +12 -1
  6. package/dist/claude-coding-worker/tool-socket.d.ts +27 -0
  7. package/dist/claude-coding-worker/tool-socket.js +111 -0
  8. package/dist/claude-coding-worker/transcript.d.ts +18 -0
  9. package/dist/claude-coding-worker/transcript.js +77 -0
  10. package/dist/cli-help.d.ts +1 -1
  11. package/dist/cli-help.js +1 -0
  12. package/dist/cli.js +24 -10
  13. package/dist/coding/continuation-wording.d.ts +35 -0
  14. package/dist/coding/continuation-wording.js +73 -0
  15. package/dist/coding/linked-issue-note.d.ts +23 -0
  16. package/dist/coding/linked-issue-note.js +47 -0
  17. package/dist/coding/merge-wording.d.ts +28 -0
  18. package/dist/coding/merge-wording.js +78 -0
  19. package/dist/coding/pr-description-note.d.ts +9 -0
  20. package/dist/coding/pr-description-note.js +32 -0
  21. package/dist/coding/profile.d.ts +46 -126
  22. package/dist/coding/profile.js +45 -4
  23. package/dist/coding/protected-path-wording.d.ts +8 -0
  24. package/dist/coding/protected-path-wording.js +1 -1
  25. package/dist/coding/protocol.d.ts +264 -346
  26. package/dist/coding/protocol.js +235 -12
  27. package/dist/coding/redact.d.ts +3 -0
  28. package/dist/coding/redact.js +12 -0
  29. package/dist/coding/registry/allowlist.d.ts +12 -0
  30. package/dist/coding/registry/allowlist.js +14 -1
  31. package/dist/coding/registry/audit-mode.d.ts +153 -0
  32. package/dist/coding/registry/audit-mode.js +304 -0
  33. package/dist/coding/registry/lockfile-shared.d.ts +36 -0
  34. package/dist/coding/registry/lockfile-shared.js +128 -0
  35. package/dist/coding/registry/lockfiles.js +1 -1
  36. package/dist/coding/registry/npm-lockfile-formats.d.ts +11 -0
  37. package/dist/coding/registry/npm-lockfile-formats.js +41 -0
  38. package/dist/coding/registry/npm-lockfile.d.ts +8 -4
  39. package/dist/coding/registry/npm-lockfile.js +4 -6
  40. package/dist/coding/registry/npm.js +78 -8
  41. package/dist/coding/registry/pnpm-lockfile.d.ts +17 -0
  42. package/dist/coding/registry/pnpm-lockfile.js +189 -0
  43. package/dist/coding/registry/pypi-plan.d.ts +50 -0
  44. package/dist/coding/registry/pypi-plan.js +406 -0
  45. package/dist/coding/registry/pypi-requirements.d.ts +58 -0
  46. package/dist/coding/registry/pypi-requirements.js +155 -0
  47. package/dist/coding/registry/pypi.d.ts +3 -27
  48. package/dist/coding/registry/pypi.js +40 -154
  49. package/dist/coding/registry/refusal-hints.d.ts +20 -0
  50. package/dist/coding/registry/refusal-hints.js +61 -0
  51. package/dist/coding/registry/refusal-wording.d.ts +11 -0
  52. package/dist/coding/registry/refusal-wording.js +56 -0
  53. package/dist/coding/registry/report.d.ts +70 -8
  54. package/dist/coding/registry/report.js +112 -5
  55. package/dist/coding/registry/types.d.ts +58 -8
  56. package/dist/coding/registry/yarn-lockfile.d.ts +30 -0
  57. package/dist/coding/registry/yarn-lockfile.js +367 -0
  58. package/dist/coding/services/catalog.d.ts +37 -351
  59. package/dist/coding/transcript/claude-code.d.ts +13 -0
  60. package/dist/coding/transcript/claude-code.js +273 -0
  61. package/dist/coding/transcript/download-protocol.d.ts +30 -0
  62. package/dist/coding/transcript/download-protocol.js +18 -0
  63. package/dist/coding/transcript/format.d.ts +20 -0
  64. package/dist/coding/transcript/format.js +32 -0
  65. package/dist/coding/transcript/recorder.d.ts +51 -0
  66. package/dist/coding/transcript/recorder.js +225 -0
  67. package/dist/coding/transcript/resume/checkout.d.ts +31 -0
  68. package/dist/coding/transcript/resume/checkout.js +203 -0
  69. package/dist/coding/transcript/resume/command.d.ts +17 -0
  70. package/dist/coding/transcript/resume/command.js +176 -0
  71. package/dist/coding/transcript/resume/download.d.ts +20 -0
  72. package/dist/coding/transcript/resume/download.js +153 -0
  73. package/dist/coding/transcript/resume/launch.d.ts +38 -0
  74. package/dist/coding/transcript/resume/launch.js +128 -0
  75. package/dist/coding/transcript/resume/session-file.d.ts +13 -0
  76. package/dist/coding/transcript/resume/session-file.js +79 -0
  77. package/dist/coding/transcript/resume/stand-in.d.ts +11 -0
  78. package/dist/coding/transcript/resume/stand-in.js +49 -0
  79. package/dist/coding-worker/artifact.d.ts +8 -3
  80. package/dist/coding-worker/artifact.js +3 -1
  81. package/dist/coding-worker/debug-trace.d.ts +6 -5
  82. package/dist/coding-worker/debug-trace.js +13 -18
  83. package/dist/coding-worker/dependency-cache.d.mts +229 -0
  84. package/dist/coding-worker/dependency-cache.mjs +2123 -0
  85. package/dist/coding-worker/driver.d.ts +4 -3
  86. package/dist/coding-worker/driver.js +116 -38
  87. package/dist/coding-worker/errors.d.ts +1 -0
  88. package/dist/coding-worker/errors.js +32 -0
  89. package/dist/coding-worker/task-note.d.ts +11 -0
  90. package/dist/coding-worker/task-note.js +13 -0
  91. package/dist/coding-worker/types.d.ts +11 -0
  92. package/dist/config/dependency-cache.d.ts +23 -0
  93. package/dist/config/dependency-cache.js +32 -0
  94. package/dist/config/providers.d.ts +12 -3
  95. package/dist/config/providers.js +55 -5
  96. package/dist/config/test-simulators.d.ts +82 -0
  97. package/dist/config/test-simulators.js +305 -0
  98. package/dist/core/async-context.d.ts +2 -0
  99. package/dist/core/async-context.js +12 -0
  100. package/dist/core/budget-events.d.ts +108 -0
  101. package/dist/core/budget-events.js +346 -0
  102. package/dist/core/budget-groups.d.ts +50 -1
  103. package/dist/core/budget-groups.js +236 -27
  104. package/dist/core/budget-stopped-head.d.ts +46 -0
  105. package/dist/core/budget-stopped-head.js +81 -0
  106. package/dist/core/budget-topup.d.ts +51 -0
  107. package/dist/core/budget-topup.js +156 -0
  108. package/dist/core/budget-wording.d.ts +16 -2
  109. package/dist/core/budget-wording.js +40 -2
  110. package/dist/core/coding-queue.d.ts +2 -1
  111. package/dist/core/coding-queue.js +7 -0
  112. package/dist/core/cron.d.ts +19 -1
  113. package/dist/core/cron.js +17 -0
  114. package/dist/core/datastores.js +1 -1
  115. package/dist/core/deferred-coding.d.ts +14 -0
  116. package/dist/core/deferred-coding.js +62 -0
  117. package/dist/core/delegation-child.d.ts +25 -0
  118. package/dist/core/delegation-child.js +37 -0
  119. package/dist/core/dispatch.d.ts +42 -0
  120. package/dist/core/dispatch.js +131 -56
  121. package/dist/core/engine-native.js +1 -1
  122. package/dist/core/error-code.d.ts +38 -0
  123. package/dist/core/error-code.js +168 -0
  124. package/dist/core/grants.d.ts +19 -6
  125. package/dist/core/grants.js +55 -6
  126. package/dist/core/host-events.d.ts +18 -2
  127. package/dist/core/host-events.js +284 -24
  128. package/dist/core/host-status.d.ts +85 -3
  129. package/dist/core/host-status.js +298 -20
  130. package/dist/core/issue-bridge.d.ts +50 -6
  131. package/dist/core/issue-bridge.js +124 -33
  132. package/dist/core/issue-events.d.ts +1 -1
  133. package/dist/core/issue-events.js +4 -10
  134. package/dist/core/issue-status.d.ts +1 -1
  135. package/dist/core/issue-status.js +54 -26
  136. package/dist/core/logger.d.ts +5 -0
  137. package/dist/core/logger.js +14 -1
  138. package/dist/core/lost-run-children.d.ts +6 -0
  139. package/dist/core/lost-run-children.js +32 -0
  140. package/dist/core/mark-ready.d.ts +34 -0
  141. package/dist/core/mark-ready.js +152 -0
  142. package/dist/core/markdown-code.d.ts +8 -0
  143. package/dist/core/markdown-code.js +107 -0
  144. package/dist/core/memory-tools.js +7 -0
  145. package/dist/core/merge-order-check.d.ts +2 -1
  146. package/dist/core/merge-order-check.js +8 -1
  147. package/dist/core/notification-templates.d.ts +1 -7
  148. package/dist/core/notification-templates.js +36 -2
  149. package/dist/core/pull-request-state-sync.js +2 -2
  150. package/dist/core/reconciler.d.ts +4 -3
  151. package/dist/core/reconciler.js +8 -1
  152. package/dist/core/related-pull-requests.d.ts +11 -3
  153. package/dist/core/related-pull-requests.js +27 -14
  154. package/dist/core/reply-parts.d.ts +21 -0
  155. package/dist/core/reply-parts.js +73 -0
  156. package/dist/core/review-fix.d.ts +1 -1
  157. package/dist/core/review-fix.js +2 -1
  158. package/dist/core/review-host-checks.d.ts +1 -1
  159. package/dist/core/review-host-tools.d.ts +3 -0
  160. package/dist/core/review-host-tools.js +99 -13
  161. package/dist/core/review-reads.d.ts +55 -0
  162. package/dist/core/review-reads.js +233 -0
  163. package/dist/core/run-status.d.ts +12 -0
  164. package/dist/core/run-status.js +12 -0
  165. package/dist/core/runner.d.ts +80 -34
  166. package/dist/core/runner.js +439 -144
  167. package/dist/core/schedule-start.d.ts +12 -0
  168. package/dist/core/schedule-start.js +14 -0
  169. package/dist/core/secret-broker-config.d.ts +10 -74
  170. package/dist/core/secret-guard.d.ts +46 -0
  171. package/dist/core/secret-guard.js +104 -0
  172. package/dist/core/secret-guarded-stores.d.ts +12 -0
  173. package/dist/core/secret-guarded-stores.js +41 -0
  174. package/dist/core/secret-spans.d.ts +50 -0
  175. package/dist/core/secret-spans.js +94 -0
  176. package/dist/core/stop-run.d.ts +46 -0
  177. package/dist/core/stop-run.js +61 -0
  178. package/dist/core/workflow-events.d.ts +14 -2
  179. package/dist/core/workflow-events.js +4 -0
  180. package/dist/core/workflow-run-events.d.ts +5 -3
  181. package/dist/core/workflow-run-events.js +43 -8
  182. package/dist/core/zod-json-schema.d.ts +4 -0
  183. package/dist/core/zod-json-schema.js +39 -0
  184. package/dist/core/zod3-window.d.ts +35 -0
  185. package/dist/core/zod3-window.js +124 -0
  186. package/dist/generated/prisma/browser.d.ts +34 -3
  187. package/dist/generated/prisma/client.d.ts +34 -3
  188. package/dist/generated/prisma/commonInputTypes.d.ts +220 -60
  189. package/dist/generated/prisma/enums.d.ts +10 -0
  190. package/dist/generated/prisma/enums.js +8 -0
  191. package/dist/generated/prisma/internal/class.d.ts +44 -0
  192. package/dist/generated/prisma/internal/class.js +4 -4
  193. package/dist/generated/prisma/internal/prismaNamespace.d.ts +428 -9
  194. package/dist/generated/prisma/internal/prismaNamespace.js +92 -1
  195. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +95 -0
  196. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +92 -1
  197. package/dist/generated/prisma/models/Agent.d.ts +106 -8
  198. package/dist/generated/prisma/models/AgentRepository.d.ts +42 -1
  199. package/dist/generated/prisma/models/CodingAgentProfile.d.ts +229 -1
  200. package/dist/generated/prisma/models/CodingProxyRequest.d.ts +207 -3
  201. package/dist/generated/prisma/models/CodingProxySession.d.ts +114 -1
  202. package/dist/generated/prisma/models/CodingRun.d.ts +1637 -1
  203. package/dist/generated/prisma/models/DependencySnapshot.d.ts +1668 -0
  204. package/dist/generated/prisma/models/DependencySnapshot.js +1 -0
  205. package/dist/generated/prisma/models/IssuePullRequest.d.ts +6 -2
  206. package/dist/generated/prisma/models/LocalReview.d.ts +40 -1
  207. package/dist/generated/prisma/models/NativeGatewayCall.d.ts +39 -1
  208. package/dist/generated/prisma/models/NativeGatewaySession.d.ts +35 -1
  209. package/dist/generated/prisma/models/RegistryAllowance.d.ts +41 -1
  210. package/dist/generated/prisma/models/RegistryApprovedVersion.d.ts +75 -1
  211. package/dist/generated/prisma/models/RegistryAuditRecord.d.ts +1364 -0
  212. package/dist/generated/prisma/models/RegistryAuditRecord.js +1 -0
  213. package/dist/generated/prisma/models/Run.d.ts +738 -2
  214. package/dist/generated/prisma/models/RunBudgetEvent.d.ts +1211 -0
  215. package/dist/generated/prisma/models/RunBudgetEvent.js +1 -0
  216. package/dist/generated/prisma/models/RunTranscript.d.ts +1339 -0
  217. package/dist/generated/prisma/models/RunTranscript.js +1 -0
  218. package/dist/generated/prisma/models.d.ts +4 -0
  219. package/dist/help-index.json +1320 -69
  220. package/dist/import/create.js +32 -3
  221. package/dist/import/neutral-schema.d.ts +50 -281
  222. package/dist/import/neutral-schema.js +5 -4
  223. package/dist/knowledge/check.d.ts +1 -1
  224. package/dist/knowledge/cli.d.ts +11 -1
  225. package/dist/knowledge/cli.js +37 -4
  226. package/dist/knowledge/concept.d.ts +3 -19
  227. package/dist/knowledge/location.d.ts +31 -0
  228. package/dist/knowledge/location.js +78 -0
  229. package/dist/mcp/auth/access.d.ts +10 -2
  230. package/dist/mcp/auth/access.js +15 -1
  231. package/dist/mcp/auth/ownership.d.ts +7 -7
  232. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  233. package/dist/mcp/auth/self-hosted/session.d.ts +2 -2
  234. package/dist/mcp/host-events/github-ingress.d.ts +2 -0
  235. package/dist/mcp/host-events/github-ingress.js +2 -1
  236. package/dist/mcp/index.d.ts +6 -0
  237. package/dist/mcp/index.js +58 -10
  238. package/dist/mcp/server.d.ts +13 -0
  239. package/dist/mcp/server.js +33 -14
  240. package/dist/mcp/tools/agents.d.ts +4 -0
  241. package/dist/mcp/tools/agents.js +198 -16
  242. package/dist/mcp/tools/datastore.js +17 -8
  243. package/dist/mcp/tools/grants.js +9 -3
  244. package/dist/mcp/tools/memory.js +11 -1
  245. package/dist/mcp/tools/notification-channels.js +2 -2
  246. package/dist/mcp/tools/packages.d.ts +2 -0
  247. package/dist/mcp/tools/packages.js +195 -0
  248. package/dist/mcp/tools/repositories.js +18 -5
  249. package/dist/mcp/tools/run-access.d.ts +16 -0
  250. package/dist/mcp/tools/run-access.js +25 -0
  251. package/dist/mcp/tools/run-transcript-download.d.ts +30 -0
  252. package/dist/mcp/tools/run-transcript-download.js +129 -0
  253. package/dist/mcp/tools/run-transcript.d.ts +19 -0
  254. package/dist/mcp/tools/run-transcript.js +70 -0
  255. package/dist/mcp/tools/runs.js +34 -12
  256. package/dist/mcp/tools/scheduling.js +3 -1
  257. package/dist/mcp/tools/secret-elicitation-form.js +5 -0
  258. package/dist/mcp/tools/secret-elicitation-server.d.ts +6 -0
  259. package/dist/mcp/tools/secret-elicitation-server.js +10 -61
  260. package/dist/mcp/tools/secrets.js +2 -2
  261. package/dist/mcp/tools/stop-run.d.ts +2 -0
  262. package/dist/mcp/tools/stop-run.js +82 -0
  263. package/dist/mcp/tools/subagents.js +2 -2
  264. package/dist/mcp/tools/tools.d.ts +1 -1
  265. package/dist/mcp/tools/tools.js +25 -9
  266. package/dist/mcp/transport/stdio-link-host.d.ts +20 -0
  267. package/dist/mcp/transport/stdio-link-host.js +67 -0
  268. package/dist/mcp/transport/streamable-http.d.ts +13 -0
  269. package/dist/mcp/transport/streamable-http.js +41 -1
  270. package/dist/native-worker/docker-launcher.d.ts +38 -2
  271. package/dist/native-worker/docker-launcher.js +65 -5
  272. package/dist/native-worker/gateway-server.d.ts +41 -3
  273. package/dist/native-worker/gateway-server.js +124 -27
  274. package/dist/native-worker/gateway.d.ts +27 -1
  275. package/dist/native-worker/gateway.js +70 -4
  276. package/dist/native-worker/http-server.d.ts +3 -1
  277. package/dist/native-worker/http-server.js +11 -7
  278. package/dist/native-worker/http-transport.js +1 -1
  279. package/dist/native-worker/kubernetes-launcher.d.ts +7 -3
  280. package/dist/native-worker/kubernetes-launcher.js +43 -3
  281. package/dist/native-worker/launch.d.ts +18 -1
  282. package/dist/native-worker/launch.js +6 -1
  283. package/dist/native-worker/ledger.d.ts +18 -2
  284. package/dist/native-worker/ledger.js +57 -4
  285. package/dist/native-worker/loopback.js +3 -8
  286. package/dist/native-worker/main.d.ts +2 -1
  287. package/dist/native-worker/main.js +14 -3
  288. package/dist/native-worker/protocol.d.ts +129 -577
  289. package/dist/native-worker/protocol.js +47 -1
  290. package/dist/native-worker/sandbox-executor.d.ts +17 -2
  291. package/dist/native-worker/sandbox-executor.js +78 -4
  292. package/dist/native-worker/stderr-redaction.d.ts +77 -0
  293. package/dist/native-worker/stderr-redaction.js +237 -0
  294. package/dist/native-worker/stdio.d.ts +1 -1
  295. package/dist/native-worker/stdio.js +6 -5
  296. package/dist/native-worker/warm-pool.d.ts +2 -2
  297. package/dist/native-worker/warm-pool.js +4 -4
  298. package/dist/native-worker/worker.js +28 -2
  299. package/dist/observability/metrics.js +6 -1
  300. package/dist/providers/auth/self-hosted.d.ts +1 -1
  301. package/dist/providers/coding-proxy/dependency-cache/eviction.d.ts +29 -0
  302. package/dist/providers/coding-proxy/dependency-cache/eviction.js +87 -0
  303. package/dist/providers/coding-proxy/dependency-cache/key.d.ts +33 -0
  304. package/dist/providers/coding-proxy/dependency-cache/key.js +86 -0
  305. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.d.ts +18 -0
  306. package/dist/providers/coding-proxy/dependency-cache/model-request-seal.js +40 -0
  307. package/dist/providers/coding-proxy/dependency-cache/prisma-store.d.ts +25 -0
  308. package/dist/providers/coding-proxy/dependency-cache/prisma-store.js +131 -0
  309. package/dist/providers/coding-proxy/dependency-cache/service.d.ts +70 -0
  310. package/dist/providers/coding-proxy/dependency-cache/service.js +409 -0
  311. package/dist/providers/coding-proxy/dependency-cache/store.d.ts +85 -0
  312. package/dist/providers/coding-proxy/dependency-cache/store.js +95 -0
  313. package/dist/providers/coding-proxy/hold.d.ts +32 -0
  314. package/dist/providers/coding-proxy/hold.js +86 -0
  315. package/dist/providers/coding-proxy/interrupted.d.ts +70 -0
  316. package/dist/providers/coding-proxy/interrupted.js +104 -0
  317. package/dist/providers/coding-proxy/memory-ledger.d.ts +15 -2
  318. package/dist/providers/coding-proxy/memory-ledger.js +84 -8
  319. package/dist/providers/coding-proxy/metering.d.ts +16 -0
  320. package/dist/providers/coding-proxy/metering.js +27 -4
  321. package/dist/providers/coding-proxy/prisma-ledger.d.ts +15 -2
  322. package/dist/providers/coding-proxy/prisma-ledger.js +179 -67
  323. package/dist/providers/coding-proxy/proxy.d.ts +14 -1
  324. package/dist/providers/coding-proxy/proxy.js +166 -14
  325. package/dist/providers/coding-proxy/registry/audit.d.ts +53 -10
  326. package/dist/providers/coding-proxy/registry/audit.js +94 -17
  327. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.d.ts +36 -0
  328. package/dist/providers/coding-proxy/registry/lockfile-parse-worker.js +67 -0
  329. package/dist/providers/coding-proxy/registry/lockfile-parse.d.ts +16 -0
  330. package/dist/providers/coding-proxy/registry/lockfile-parse.js +110 -0
  331. package/dist/providers/coding-proxy/registry/plan.d.ts +9 -0
  332. package/dist/providers/coding-proxy/registry/plan.js +201 -48
  333. package/dist/providers/coding-proxy/registry/prisma-store.d.ts +11 -5
  334. package/dist/providers/coding-proxy/registry/prisma-store.js +120 -17
  335. package/dist/providers/coding-proxy/registry/service.d.ts +97 -16
  336. package/dist/providers/coding-proxy/registry/service.js +579 -112
  337. package/dist/providers/coding-proxy/registry/store.d.ts +84 -9
  338. package/dist/providers/coding-proxy/registry/store.js +54 -1
  339. package/dist/providers/coding-proxy/runtime.d.ts +4 -0
  340. package/dist/providers/coding-proxy/runtime.js +78 -16
  341. package/dist/providers/coding-proxy/server.d.ts +6 -1
  342. package/dist/providers/coding-proxy/server.js +45 -1
  343. package/dist/providers/coding-proxy/types.d.ts +65 -1
  344. package/dist/providers/datastore/types.d.ts +5 -0
  345. package/dist/providers/engine/types.d.ts +17 -2
  346. package/dist/providers/engine/types.js +17 -0
  347. package/dist/providers/executor/composition.d.ts +10 -3
  348. package/dist/providers/executor/composition.js +47 -7
  349. package/dist/providers/executor/container.d.ts +361 -8
  350. package/dist/providers/executor/container.js +1136 -91
  351. package/dist/providers/executor/dbos-status.d.ts +31 -0
  352. package/dist/providers/executor/dbos-status.js +64 -0
  353. package/dist/providers/executor/dbos.d.ts +50 -1
  354. package/dist/providers/executor/dbos.js +207 -20
  355. package/dist/providers/executor/deferred.d.ts +12 -24
  356. package/dist/providers/executor/deferred.js +10 -29
  357. package/dist/providers/executor/in-process.d.ts +9 -1
  358. package/dist/providers/executor/in-process.js +14 -4
  359. package/dist/providers/executor/linked-issue-reader.d.ts +16 -0
  360. package/dist/providers/executor/linked-issue-reader.js +30 -0
  361. package/dist/providers/executor/routing.d.ts +2 -0
  362. package/dist/providers/executor/routing.js +4 -0
  363. package/dist/providers/executor/types.d.ts +13 -0
  364. package/dist/providers/index.d.ts +2 -1
  365. package/dist/providers/issue-tracker/types.d.ts +17 -0
  366. package/dist/providers/issue-tracker/types.js +39 -1
  367. package/dist/providers/jobs/claude-tool-setup.d.ts +4 -0
  368. package/dist/providers/jobs/claude-tool-setup.js +5 -2
  369. package/dist/providers/jobs/docker-isolation.d.ts +8 -0
  370. package/dist/providers/jobs/docker-isolation.js +17 -0
  371. package/dist/providers/jobs/docker.d.ts +1 -0
  372. package/dist/providers/jobs/docker.js +27 -2
  373. package/dist/providers/jobs/fake-kubernetes-api.d.ts +2 -0
  374. package/dist/providers/jobs/fake-kubernetes-api.js +3 -0
  375. package/dist/providers/jobs/kubernetes-api.d.ts +8 -0
  376. package/dist/providers/jobs/kubernetes-api.js +10 -0
  377. package/dist/providers/jobs/kubernetes-client.js +10 -1
  378. package/dist/providers/jobs/kubernetes-isolation.d.ts +2 -0
  379. package/dist/providers/jobs/kubernetes-isolation.js +17 -1
  380. package/dist/providers/jobs/kubernetes.d.ts +6 -1
  381. package/dist/providers/jobs/kubernetes.js +105 -16
  382. package/dist/providers/jobs/types.d.ts +33 -0
  383. package/dist/providers/jobs/types.js +13 -1
  384. package/dist/providers/llm/anthropic.js +5 -1
  385. package/dist/providers/llm/bedrock.d.ts +0 -12
  386. package/dist/providers/llm/bedrock.js +4 -0
  387. package/dist/providers/llm/claude-messages.d.ts +3 -1
  388. package/dist/providers/llm/claude-messages.js +4 -1
  389. package/dist/providers/llm/claude-provider.d.ts +2 -2
  390. package/dist/providers/llm/claude-provider.js +2 -2
  391. package/dist/providers/llm/openai.d.ts +2 -2
  392. package/dist/providers/llm/openai.js +6 -2
  393. package/dist/providers/llm/routing.d.ts +2 -2
  394. package/dist/providers/llm/routing.js +2 -2
  395. package/dist/providers/llm/types.d.ts +12 -2
  396. package/dist/providers/review-host/github-events.d.ts +9 -1
  397. package/dist/providers/review-host/github-events.js +34 -12
  398. package/dist/providers/review-host/github.d.ts +29 -2
  399. package/dist/providers/review-host/github.js +224 -29
  400. package/dist/providers/review-host/local.d.ts +8 -4
  401. package/dist/providers/review-host/local.js +49 -31
  402. package/dist/providers/review-host/patch-pages.d.ts +41 -0
  403. package/dist/providers/review-host/patch-pages.js +176 -0
  404. package/dist/providers/review-host/review-format.d.ts +19 -1
  405. package/dist/providers/review-host/review-format.js +38 -7
  406. package/dist/providers/review-host/types.d.ts +134 -2
  407. package/dist/providers/review-host/types.js +8 -0
  408. package/dist/providers/storage/gcs.d.ts +63 -0
  409. package/dist/providers/storage/gcs.js +69 -0
  410. package/dist/providers/storage/index.d.ts +15 -0
  411. package/dist/providers/storage/index.js +14 -0
  412. package/dist/providers/storage/local.d.ts +24 -0
  413. package/dist/providers/storage/local.js +92 -0
  414. package/dist/providers/storage/meter.d.ts +14 -0
  415. package/dist/providers/storage/meter.js +34 -0
  416. package/dist/providers/storage/s3.d.ts +41 -0
  417. package/dist/providers/storage/s3.js +107 -0
  418. package/dist/providers/storage/types.d.ts +37 -3
  419. package/dist/providers/storage/types.js +21 -4
  420. package/dist/providers/vcs/git.d.ts +33 -2
  421. package/dist/providers/vcs/git.js +324 -22
  422. package/dist/providers/vcs/github-remote.d.ts +26 -3
  423. package/dist/providers/vcs/github-remote.js +124 -14
  424. package/dist/providers/vcs/github.d.ts +214 -6
  425. package/dist/providers/vcs/github.js +447 -45
  426. package/dist/providers/vcs/index.js +8 -1
  427. package/dist/providers/vcs/pr-description.d.ts +127 -0
  428. package/dist/providers/vcs/pr-description.js +270 -0
  429. package/dist/providers/vcs/remote.d.ts +33 -1
  430. package/dist/providers/vcs/routing.d.ts +4 -2
  431. package/dist/providers/vcs/routing.js +6 -0
  432. package/dist/providers/vcs/types.d.ts +80 -3
  433. package/dist/quickstart/coding-seed.d.ts +14 -3
  434. package/dist/quickstart/coding-seed.js +213 -4
  435. package/dist/quickstart/reviewer-prompt.d.ts +1 -1
  436. package/dist/quickstart/reviewer-prompt.js +6 -6
  437. package/dist/quickstart-images.json +1 -1
  438. package/dist/sandbox/generated/zod.bundle.js +62 -1
  439. package/dist/sandbox/generated/zod3.bundle.js +5 -0
  440. package/dist/sandbox/host-functions.d.ts +1 -0
  441. package/dist/sandbox/host-functions.js +6 -11
  442. package/dist/sandbox/secret-broker.js +3 -4
  443. package/dist/sandbox/tool-capabilities.d.ts +10 -30
  444. package/dist/sandbox/zod-params.d.ts +37 -5
  445. package/dist/sandbox/zod-params.js +181 -26
  446. package/dist/serve.js +8 -1
  447. package/dist/viewer/api-schema.d.ts +349 -2627
  448. package/dist/viewer/api-schema.js +7 -0
  449. package/dist/viewer/build-schemas.js +6 -2
  450. package/dist/viewer/openapi.js +2 -4
  451. package/dist/wardby-bin.js +10 -0
  452. package/docs/agent-recipes.md +58 -20
  453. package/docs/code-review-agents.md +551 -41
  454. package/docs/coding-agent-setup.md +489 -7
  455. package/docs/coding-packages.md +1033 -90
  456. package/docs/coding-services.md +27 -12
  457. package/docs/coding-worker-byo-images.md +20 -0
  458. package/docs/coding-worker-isolation.md +105 -30
  459. package/docs/getting-started-gke.md +124 -137
  460. package/docs/getting-started-identity-provider.md +10 -1
  461. package/docs/getting-started.md +9 -0
  462. package/docs/jira-agents.md +17 -8
  463. package/docs/knowledge/blob-not-found-semantics.md +41 -0
  464. package/docs/knowledge/bounded-file-descriptor.md +52 -0
  465. package/docs/knowledge/delegation-admission-slots.md +52 -0
  466. package/docs/knowledge/http-dispatcher-bootstrap.md +61 -0
  467. package/docs/knowledge/import-rename-target.md +52 -0
  468. package/docs/knowledge/index.md +57 -0
  469. package/docs/knowledge/log.md +9 -0
  470. package/docs/knowledge/mcp-cancelled-task-precedence.md +41 -0
  471. package/docs/knowledge/mcp-http-prompt-discovery.md +41 -0
  472. package/docs/knowledge/mcp-schema-module-boundary.md +41 -0
  473. package/docs/knowledge/nullable-tool-uniqueness.md +52 -0
  474. package/docs/knowledge/overview-cli.md +74 -0
  475. package/docs/knowledge/overview-coding-proxy.md +72 -0
  476. package/docs/knowledge/overview-coding.md +72 -0
  477. package/docs/knowledge/overview-config.md +60 -0
  478. package/docs/knowledge/overview-core.md +72 -0
  479. package/docs/knowledge/overview-deploy.md +83 -0
  480. package/docs/knowledge/overview-mcp.md +61 -0
  481. package/docs/knowledge/overview-prisma.md +72 -0
  482. package/docs/knowledge/overview-providers.md +89 -0
  483. package/docs/knowledge/overview-sandbox.md +83 -0
  484. package/docs/knowledge/overview-surfaces.md +83 -0
  485. package/docs/knowledge/private-datastore-shared-boundary.md +54 -0
  486. package/docs/knowledge/proxy-settlement-projections.md +41 -0
  487. package/docs/knowledge/run-pricing-first-writer.md +43 -0
  488. package/docs/knowledge/schedule-claim-transaction.md +41 -0
  489. package/docs/knowledge/secret-entry-replica-contract.md +52 -0
  490. package/docs/knowledge/terminal-run-first-writer.md +41 -0
  491. package/docs/knowledge/transactional-agent-authorization.md +52 -0
  492. package/docs/knowledge/viewer-notify-write-isolation.md +30 -0
  493. package/docs/knowledge/warm-input-publication.md +30 -0
  494. package/docs/knowledge/workspace-staging-failure.md +54 -0
  495. package/docs/knowledge.md +54 -14
  496. package/docs/native-sandbox.md +119 -23
  497. package/docs/observability.md +4 -0
  498. package/docs/security-deployment.md +33 -2
  499. package/docs/slack-notifications.md +35 -19
  500. package/docs/tool-secrets.md +42 -10
  501. package/docs/viewer-api.openapi.yml +1 -1
  502. package/help/admin-viewer.md +24 -2
  503. package/help/architecture-agent.md +11 -6
  504. package/help/build-worker-image.md +3 -1
  505. package/help/builder-agent.md +19 -6
  506. package/help/code-review-agents.md +177 -6
  507. package/help/coding-packages.md +156 -6
  508. package/help/cost-attribution.md +9 -5
  509. package/help/creating-agents.md +14 -1
  510. package/help/dependency-cache.md +143 -0
  511. package/help/deploy-gke.md +15 -8
  512. package/help/errors/budget-exhausted.md +187 -0
  513. package/help/errors/budget-group-exhausted.md +5 -1
  514. package/help/errors/coding-stream-quota-exceeded.md +33 -0
  515. package/help/errors/coding-workspace-disk-exceeds-limit.md +39 -0
  516. package/help/errors/continuation-closed.md +9 -12
  517. package/help/errors/delegation-conflict.md +24 -0
  518. package/help/errors/file-list-truncated.md +45 -0
  519. package/help/errors/full-read-required.md +49 -0
  520. package/help/errors/github-account-not-allowed.md +4 -0
  521. package/help/errors/graph-incomplete.md +43 -0
  522. package/help/errors/graph-limit.md +43 -0
  523. package/help/errors/merge-conflict.md +49 -0
  524. package/help/errors/native-sandbox-gateway-error.md +20 -0
  525. package/help/errors/native-sandbox-launch-abandoned.md +21 -0
  526. package/help/errors/native-sandbox-worker-exited.md +3 -2
  527. package/help/errors/package-not-allowed-pending.md +52 -0
  528. package/help/errors/package-not-allowed.md +51 -0
  529. package/help/errors/package-refusals.md +43 -0
  530. package/help/errors/patch-cursor-malformed.md +31 -0
  531. package/help/errors/run-lost.md +39 -0
  532. package/help/errors/run-transcript-download.md +44 -0
  533. package/help/errors/secret-in-value.md +69 -0
  534. package/help/errors/target-merge.md +40 -0
  535. package/help/fix-merge-conflicts.md +73 -0
  536. package/help/github-app-setup.md +132 -0
  537. package/help/github.md +22 -0
  538. package/help/identity-and-access.md +2 -0
  539. package/help/jira.md +23 -6
  540. package/help/knowledge.md +16 -7
  541. package/help/local-repositories.md +1 -1
  542. package/help/low-power-mode.md +7 -1
  543. package/help/merge-order-check.md +5 -0
  544. package/help/native-capabilities.md +29 -1
  545. package/help/native-sandbox.md +15 -1
  546. package/help/operating-agents.md +5 -0
  547. package/help/package-audit-mode.md +91 -0
  548. package/help/related-pull-requests.md +10 -7
  549. package/help/resume-runs-locally.md +131 -0
  550. package/help/review-fix-rounds.md +33 -1
  551. package/help/sharing-agents.md +59 -0
  552. package/help/slack-notifications.md +19 -5
  553. package/help/status-comments.md +31 -0
  554. package/help/stop-run.md +58 -0
  555. package/help/troubleshooting/budgets.md +28 -0
  556. package/help/troubleshooting/coding-workers.md +78 -7
  557. package/package.json +17 -10
  558. package/prisma/migrations/20261016000000_registry_approved_accepted_advisories/migration.sql +3 -0
  559. package/prisma/migrations/20261016000001_mark_ready_when_passing/migration.sql +3 -0
  560. package/prisma/migrations/20261017000000_debug_trace_max_mb/migration.sql +4 -0
  561. package/prisma/migrations/20261018000000_dependency_cache/migration.sql +53 -0
  562. package/prisma/migrations/20261019000000_claude_max_output_tokens/migration.sql +5 -0
  563. package/prisma/migrations/20261021000000_pr_description_hash/migration.sql +4 -0
  564. package/prisma/migrations/20261022000000_dependency_snapshot_validation_metadata/migration.sql +4 -0
  565. package/prisma/migrations/20261023000000_package_audit_mode/migration.sql +27 -0
  566. package/prisma/migrations/20261024000000_budget_events/migration.sql +26 -0
  567. package/prisma/migrations/20261025000000_cache_aware_holds/migration.sql +9 -0
  568. package/prisma/migrations/20261026000000_budget_overrun/migration.sql +4 -0
  569. package/prisma/migrations/20261027000000_coding_run_request/migration.sql +1 -0
  570. package/prisma/migrations/20261028000000_native_gateway_review_reads/migration.sql +6 -0
  571. package/prisma/migrations/20261029000000_lead_survives_deploy/migration.sql +6 -0
  572. package/prisma/migrations/20261030000000_local_review_covered_sha/migration.sql +2 -0
  573. package/prisma/migrations/20261031000000_coding_run_budget_limit/migration.sql +9 -0
  574. package/prisma/migrations/20261101000000_run_transcripts/migration.sql +22 -0
  575. package/prisma/schema.prisma +311 -97
  576. package/dist/sandbox/generated/zod-to-json-schema.bundle.js +0 -5
@@ -1,7 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { isPackageAuditMode, mergePackageAuditReports, } from "../../coding/registry/audit-mode.js";
2
3
  import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
4
  import { join, resolve, sep } from "node:path";
4
5
  import { withBaseCommit } from "../../coding/base-commit.js";
6
+ import { withPullRequestDescription } from "../../coding/pr-description-note.js";
7
+ import { withLinkedIssue } from "../../coding/linked-issue-note.js";
5
8
  import { buildClaudeContext, MAX_RECORDED_SKIPS } from "../../coding/claude-context.js";
6
9
  import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
7
10
  import { codingProviderForModelProvider } from "../../coding/provider.js";
@@ -10,31 +13,50 @@ import { parseStoredServices, storedServiceNames, workerServices } from "../../c
10
13
  import { MAX_SERVICE_DECLARATION_BYTES, SERVICE_DECLARATION_PATH } from "../../coding/services/declaration.js";
11
14
  import { SERVICE_UNREADY_CATEGORY, SERVICE_UNREADY_ERROR, serviceUnreadyName, serviceUnreadySentence, } from "../../coding/services/wording.js";
12
15
  import { PROTECTED_PATH_CATEGORY, protectedPathFromError, protectedPathSentence, } from "../../coding/protected-path-wording.js";
13
- import { CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR } from "../../coding/continuation-wording.js";
14
- import { budgetSentence } from "../../core/budget-wording.js";
16
+ import { budgetStoppedSummary, CONTINUATION_CLOSED_CATEGORY, CONTINUATION_CLOSED_ERROR, WORKER_BUDGET_EXHAUSTED_SUMMARY, } from "../../coding/continuation-wording.js";
17
+ import { CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY, TARGET_MERGE_CATEGORY, TARGET_MERGE_FAILED_ERROR, conflictMarkersPathFromError, conflictMarkersSentence, mergeSentence, withTargetMerge, } from "../../coding/merge-wording.js";
18
+ import { budgetSentence, overrunSentence } from "../../core/budget-wording.js";
15
19
  import { fileSelfDefect } from "../../core/self-defects.js";
16
20
  import { isTransactionUnavailable } from "../../core/dispatch.js";
21
+ import { topUpCodingBudget } from "../../core/budget-topup.js";
17
22
  import { CONTENDED_TX_MAX_WAIT_MS } from "../../core/timing.js";
18
23
  import { classifyProviderFailure, providerClassOfCategory, providerSentence, } from "../../core/provider-wording.js";
19
24
  import { logger } from "../../core/logger.js";
20
25
  import { requiredLevel } from "../../core/repo-access.js";
21
26
  import { ensurePrivateDirectory } from "../../core/private-directory.js";
22
27
  import { codingRunObserver } from "../../coding/observability.js";
23
- import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
28
+ import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, MAX_CODING_INPUT_BYTES, isLocalRepository, parseCodingWorkerOutputJson, redactAndTruncate, restoredDependencyFolders, sanitizeDependencyCacheReport, } from "../../coding/protocol.js";
24
29
  import { parseStoredEntry } from "../llm/catalog-types.js";
25
30
  import { resolveCodingEntry } from "../../core/run-pricing.js";
26
31
  import { CODING_PROXY_ALIAS, CODING_PROXY_PORT, isImmutableDockerImage } from "../jobs/docker-isolation.js";
27
32
  import { normalizeRegistryLockfiles } from "../../coding/registry/lockfiles.js";
28
- import { ISSUE_KEY, ISSUE_TRACKER_NAMES } from "../issue-tracker/types.js";
33
+ import { warningRefusals } from "../../coding/registry/report.js";
34
+ import { hostBudgetSentence, loadBudgetEvent, ownerBudgetSentence, proxyRefusalDetails, recordBudgetEvent, } from "../../core/budget-events.js";
35
+ import { JobCapacityError, } from "../jobs/types.js";
36
+ import { ISSUE_KEY, issueReference } from "../issue-tracker/types.js";
29
37
  import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
30
38
  import { collectRelatedPullRequests } from "../../core/related-pull-requests.js";
39
+ import { isIssueMentionTask } from "../../core/host-status.js";
40
+ import { pullRequestBody, } from "../vcs/github.js";
41
+ import { closingIssueNumber, descriptionSha256 } from "../vcs/pr-description.js";
31
42
  const containerLog = logger.child({ module: "container-executor" });
43
+ /** A transcript read and store must not hold up the job's removal for long. */
44
+ const TRANSCRIPT_RECORD_TIMEOUT_MS = 60_000;
32
45
  /** A logged context skip path is cut to this many characters: a refused import is reported as written. */
33
46
  const MAX_LOGGED_SKIP_PATH_CHARS = 256;
34
47
  /** The input path in a spec built only to ask the launcher about capacity; never written or read. */
35
48
  const CAPACITY_PROBE_INPUT = "/dev/null";
36
49
  const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
37
50
  const PROVISIONING_BACKEND = "provisioning";
51
+ /** The longest a no-change fix round's description refresh may hold up the run's terminal write. */
52
+ export const DESCRIPTION_REFRESH_TIMEOUT_MS = 60_000;
53
+ /**
54
+ * After the cluster refuses a run's launch for capacity, how long this executor leaves the run
55
+ * waiting in the queue before trying it again (drainCodingQueue starts it every tick meanwhile).
56
+ */
57
+ export const CAPACITY_RETRY_BACKOFF_MS = 60_000;
58
+ /** How many runs above a root coding run are searched for the mention that started it. */
59
+ const MAX_LINKED_ISSUE_RUN_DEPTH = 8;
38
60
  /** Serializes concurrency-slot claims across replicas. Distinct from the OAuth client lock (7412901). */
39
61
  const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412902)";
40
62
  /**
@@ -48,6 +70,14 @@ const CODING_SLOT_LOCK_SQL = "SELECT 1 AS locked FROM pg_advisory_xact_lock(7412
48
70
  */
49
71
  class RunNoLongerActiveError extends Error {
50
72
  }
73
+ /** Private sentinel: rolls back requeueAfterCapacityRefusal's transaction with the outcome it carries. */
74
+ class NotRequeuedError extends Error {
75
+ outcome;
76
+ constructor(outcome) {
77
+ super(outcome);
78
+ this.outcome = outcome;
79
+ }
80
+ }
51
81
  function proxyProtocol(provider) {
52
82
  if (provider === "codex")
53
83
  return "openai-responses";
@@ -68,6 +98,51 @@ export async function normalizeCollectedLockfiles(provider, workspacePath) {
68
98
  proxyBaseUrl: `http://${CODING_PROXY_ALIAS}:${CODING_PROXY_PORT}`,
69
99
  });
70
100
  }
101
+ /** The worker's dependency cache report, as CodingRunRecord columns: nothing on a run without one. */
102
+ function dependencyCacheRecord(output) {
103
+ return output.dependencyCache ? { dependencyCacheResult: output.dependencyCache } : {};
104
+ }
105
+ /** What a push records on the run: its base commit, and a pushed branch's name (a local repository). */
106
+ function pushedRecord(finalized) {
107
+ return {
108
+ baseSha: finalized.baseCommit,
109
+ ...(finalized.outcome === "branch_pushed" ? { resultBranch: finalized.headRef } : {}),
110
+ };
111
+ }
112
+ /** The PR body's dependency cache line: the folders restored from a snapshot (hit or nearest). */
113
+ function restoredDependenciesFor(output) {
114
+ const folders = restoredDependencyFolders(output.dependencyCache);
115
+ return folders.length > 0 ? { restoredDependencies: folders } : {};
116
+ }
117
+ /** The stored form of a description outcome: written, replaced, refreshed:<reason>, unchanged, edited_by_hand, skipped:<reason>. */
118
+ export function descriptionOutcomeLabel(outcome) {
119
+ if (outcome.kind === "refreshed" || outcome.kind === "skipped")
120
+ return `${outcome.kind}:${outcome.reason}`;
121
+ return outcome.kind;
122
+ }
123
+ /** A local repository has no pull request: its runs get no description note and record no description outcome. */
124
+ function hasPullRequestDescription(run) {
125
+ return !isLocalRepository(run.repository);
126
+ }
127
+ /**
128
+ * A budget-stopped run's stored result names what its kept changes became
129
+ * (see keepStoppedChanges): the `changesPushed` detail for its status comment,
130
+ * or nothing when it pushed nothing.
131
+ */
132
+ function storedChangesPushed(result) {
133
+ const parsed = CodingRunResultSchema.safeParse(result);
134
+ if (!parsed.success)
135
+ return {};
136
+ const stored = parsed.data;
137
+ if (stored.outcome === "branch_pushed")
138
+ return { changesPushed: {} };
139
+ if (stored.outcome === "pull_request_opened" || stored.outcome === "pull_request_updated") {
140
+ return { changesPushed: { pullRequestNumber: stored.pullRequestNumber } };
141
+ }
142
+ return {};
143
+ }
144
+ /** The longest reading the linked issue may hold up a continuation's launch. */
145
+ export const LINKED_ISSUE_READ_TIMEOUT_MS = 15_000;
71
146
  export class PrismaContainerExecutionStore {
72
147
  db;
73
148
  options;
@@ -96,11 +171,13 @@ export class PrismaContainerExecutionStore {
96
171
  return null;
97
172
  return {
98
173
  runId: row.id,
174
+ agentId: row.agentId,
99
175
  status: row.status,
100
176
  agentKind: row.agent.kind,
101
177
  agentName: row.agent.name,
102
178
  ownerId: row.agent.ownerId,
103
179
  task: row.codingRun.task,
180
+ ...(row.codingRun.request ? { request: row.codingRun.request } : {}),
104
181
  repository: row.codingRun.repository,
105
182
  baseRef: row.codingRun.baseRef,
106
183
  headRef: row.codingRun.headRef,
@@ -115,6 +192,8 @@ export class PrismaContainerExecutionStore {
115
192
  mergeOrder: row.codingRun.mergeOrder,
116
193
  budgetUsd: Number(row.codingRun.budgetReservedUsd),
117
194
  agentBudgetUsd: Number(row.agent.budgetUsd),
195
+ ...(row.codingRun.budgetBaseUsd == null ? {} : { budgetBaseUsd: Number(row.codingRun.budgetBaseUsd) }),
196
+ ...(row.codingRun.budgetLimitUsd == null ? {} : { budgetLimitUsd: Number(row.codingRun.budgetLimitUsd) }),
118
197
  budgetGroupName: row.agent.budgetGroup?.name ?? null,
119
198
  tokensIn: row.tokensIn,
120
199
  tokensOut: row.tokensOut,
@@ -129,9 +208,13 @@ export class PrismaContainerExecutionStore {
129
208
  toolImage: row.codingRun.toolImage,
130
209
  workspaceDiskMb: row.codingRun.workspaceDiskMb,
131
210
  debugTrace: row.codingRun.debugTrace,
211
+ debugTraceMaxMb: row.codingRun.debugTraceMaxMb,
132
212
  maxTurns: row.codingRun.maxTurns,
213
+ maxOutputTokens: row.codingRun.maxOutputTokens,
133
214
  repoSkills: row.codingRun.repoSkills,
134
215
  claudeBareMode: row.codingRun.claudeBareMode,
216
+ dependencyCache: row.codingRun.dependencyCache,
217
+ sessionResume: row.codingRun.sessionResume,
135
218
  services: row.codingRun.services,
136
219
  failureCategory: row.codingRun.failureCategory,
137
220
  profileRepository: row.agent.codingProfile?.repository ?? null,
@@ -150,6 +233,108 @@ export class PrismaContainerExecutionStore {
150
233
  ...(pr.mergeOrder !== undefined ? { mergeOrder: pr.mergeOrder } : {}),
151
234
  }));
152
235
  }
236
+ async pullRequestMarkedReady(rootRunId) {
237
+ const root = await this.db.codingRun.findUnique({ where: { runId: rootRunId }, select: { markedReadyAt: true } });
238
+ return Boolean(root?.markedReadyAt);
239
+ }
240
+ async pullRequestDescriptionBaseline(rootRunId) {
241
+ const root = await this.db.codingRun.findUnique({
242
+ where: { runId: rootRunId },
243
+ select: {
244
+ prDescriptionSha256: true,
245
+ prDescriptionRunId: true,
246
+ result: true,
247
+ repository: true,
248
+ baseRef: true,
249
+ headRef: true,
250
+ dependencyCacheResult: true,
251
+ issueProvider: true,
252
+ issueKey: true,
253
+ },
254
+ });
255
+ if (!root)
256
+ throw new Error("coding_run_not_found");
257
+ // Legacy pull requests (no recorded author run) carry the root run's own text.
258
+ const agentTextRunId = root.prDescriptionRunId ?? rootRunId;
259
+ const source = agentTextRunId === rootRunId
260
+ ? root.result
261
+ : (await this.db.codingRun.findUnique({ where: { runId: agentTextRunId }, select: { result: true } }))?.result;
262
+ const previousAgentText = storedSummary(source);
263
+ const rootSummary = storedSummary(root.result);
264
+ const legacySource = root.prDescriptionSha256 === null
265
+ ? {
266
+ repository: root.repository,
267
+ baseRef: root.baseRef,
268
+ headRef: root.headRef,
269
+ ...(rootSummary !== undefined ? { summary: rootSummary } : {}),
270
+ ...(storedField(root.result, "tests") !== undefined ? { tests: storedField(root.result, "tests") } : {}),
271
+ ...(root.dependencyCacheResult != null ? { dependencyCache: root.dependencyCacheResult } : {}),
272
+ issueProvider: root.issueProvider,
273
+ issueKey: root.issueKey,
274
+ }
275
+ : undefined;
276
+ return {
277
+ recordedSha256: root.prDescriptionSha256,
278
+ ...(previousAgentText !== undefined ? { previousAgentText } : {}),
279
+ agentTextRunId,
280
+ ...(legacySource ? { legacySource } : {}),
281
+ };
282
+ }
283
+ async linkedIssueSource(rootRunId) {
284
+ const root = await this.db.codingRun.findUnique({
285
+ where: { runId: rootRunId },
286
+ select: { issueProvider: true, issueKey: true, run: { select: { parentRunId: true } } },
287
+ });
288
+ if (!root)
289
+ throw new Error("coding_run_not_found");
290
+ // The mention that started the work is on the root's delegating run, or a run above it.
291
+ let hostStatus = null;
292
+ let parentRunId = root.run.parentRunId;
293
+ for (let depth = 0; parentRunId && depth < MAX_LINKED_ISSUE_RUN_DEPTH; depth++) {
294
+ const parent = await this.db.run.findUnique({
295
+ where: { id: parentRunId },
296
+ select: {
297
+ parentRunId: true,
298
+ taskOverride: true,
299
+ hostStatus: { select: { provider: true, repository: true, number: true } },
300
+ },
301
+ });
302
+ if (!parent)
303
+ break;
304
+ if (parent.hostStatus) {
305
+ const { provider, repository, number } = parent.hostStatus;
306
+ hostStatus = {
307
+ provider,
308
+ repository,
309
+ number,
310
+ issueMention: provider === "github" && isIssueMentionTask(parent.taskOverride, repository, number),
311
+ };
312
+ break;
313
+ }
314
+ parentRunId = parent.parentRunId;
315
+ }
316
+ return { issueProvider: root.issueProvider, issueKey: root.issueKey, hostStatus };
317
+ }
318
+ async pullRequestRunIds(rootRunId) {
319
+ const rows = await this.db.codingRun.findMany({
320
+ where: { OR: [{ runId: rootRunId }, { rootCodingRunId: rootRunId }] },
321
+ select: { runId: true, result: true },
322
+ orderBy: { run: { startedAt: "asc" } },
323
+ });
324
+ const pushed = rows.filter((row) => {
325
+ const outcome = storedField(row.result, "outcome");
326
+ return outcome === "pull_request_opened" || outcome === "pull_request_updated";
327
+ });
328
+ // The root first, whatever the clock says.
329
+ return [...pushed.filter((row) => row.runId === rootRunId), ...pushed.filter((row) => row.runId !== rootRunId)].map((row) => row.runId);
330
+ }
331
+ async recordPullRequestDescription(input) {
332
+ const written = await this.db.codingRun.updateMany({
333
+ where: { runId: input.rootRunId, prDescriptionSha256: input.expectedSha256 },
334
+ data: { prDescriptionSha256: input.sha256, prDescriptionRunId: input.agentTextRunId },
335
+ });
336
+ return written.count === 1;
337
+ }
153
338
  async claimProvisioning(runId, claimId) {
154
339
  try {
155
340
  return await this.db.$transaction(async (tx) => {
@@ -239,6 +424,52 @@ export class PrismaContainerExecutionStore {
239
424
  data: { queuedAt: new Date() },
240
425
  });
241
426
  }
427
+ async requeueAfterCapacityRefusal(runId, owned) {
428
+ const holds = [
429
+ ...(owned.claimId ? [{ jobBackend: PROVISIONING_BACKEND, jobHandle: owned.claimId }] : []),
430
+ ...(owned.planned ? [{ jobBackend: owned.planned.backend, jobHandle: owned.planned.id }] : []),
431
+ ];
432
+ if (holds.length === 0)
433
+ return "not_owned";
434
+ try {
435
+ return await this.db.$transaction(async (tx) => {
436
+ const run = await tx.run.findUnique({ where: { id: runId }, select: { startedAt: true } });
437
+ if (!run)
438
+ return "not_owned";
439
+ // queuedAt is the dispatch time, not now: claimProvisioning clears it on every claim, and a
440
+ // fresh value per refusal would keep a run the cluster refuses forever from timing out.
441
+ const released = await tx.codingRun.updateMany({
442
+ where: { runId, OR: holds, run: { status: { in: ["pending", "running"] } } },
443
+ data: { jobBackend: null, jobHandle: null, queuedAt: run.startedAt },
444
+ });
445
+ if (released.count !== 1)
446
+ return "not_owned";
447
+ // The pod never started, so its session served no request. A run has one session, and the
448
+ // next attempt creates its own: drop this one, but never one with spend recorded against it.
449
+ const session = await tx.codingProxySession.findUnique({
450
+ where: { runId },
451
+ select: { id: true, _count: { select: { requests: true } } },
452
+ });
453
+ if (session && session._count.requests > 0)
454
+ throw new NotRequeuedError("spend_recorded");
455
+ if (session)
456
+ await tx.codingProxySession.delete({ where: { id: session.id } });
457
+ const waiting = await tx.run.updateMany({
458
+ where: { id: runId, status: { in: ["pending", "running"] } },
459
+ data: { status: "pending", heartbeatAt: null },
460
+ });
461
+ // Ended (stopped, say) since the release above: roll the release back too.
462
+ if (waiting.count !== 1)
463
+ throw new NotRequeuedError("not_owned");
464
+ return "requeued";
465
+ });
466
+ }
467
+ catch (err) {
468
+ if (err instanceof NotRequeuedError)
469
+ return err.outcome;
470
+ throw err;
471
+ }
472
+ }
242
473
  async persistHandle(runId, claimId, handle) {
243
474
  await this.db.$transaction(async (tx) => {
244
475
  const coding = await tx.codingRun.findUnique({ where: { runId }, include: { run: true } });
@@ -290,6 +521,11 @@ export class PrismaContainerExecutionStore {
290
521
  resultSchema: CODING_PROTOCOL_VERSION,
291
522
  ...(record.resultBranch ? { resultBranch: record.resultBranch } : {}),
292
523
  ...(record.baseSha ? { baseSha: record.baseSha } : {}),
524
+ ...(record.dependencyCacheResult ? { dependencyCacheResult: record.dependencyCacheResult } : {}),
525
+ ...(record.prDescriptionOutcome ? { prDescriptionOutcome: record.prDescriptionOutcome } : {}),
526
+ ...(record.prDescriptionSha256 && record.prDescriptionRunId
527
+ ? { prDescriptionSha256: record.prDescriptionSha256, prDescriptionRunId: record.prDescriptionRunId }
528
+ : {}),
293
529
  },
294
530
  });
295
531
  await tx.run.update({
@@ -308,8 +544,33 @@ export class PrismaContainerExecutionStore {
308
544
  await this.selfDefect({ ...finished, status, error: null, finishedAt });
309
545
  }
310
546
  }
547
+ async recordBudgetRefusal(runId, sessionId) {
548
+ const session = await this.db.codingProxySession.findUnique({
549
+ where: { id: sessionId },
550
+ select: { budgetRefusal: true },
551
+ });
552
+ // The proxy (a less-trusted role) wrote this: keep only well-formed amounts, model and turn.
553
+ const refusal = proxyRefusalDetails(session?.budgetRefusal);
554
+ if (!refusal)
555
+ return;
556
+ const existing = await this.db.runBudgetEvent.findFirst({
557
+ where: { runId, details: { path: ["source"], equals: "proxy" } },
558
+ select: { id: true },
559
+ });
560
+ if (existing)
561
+ return;
562
+ const coding = await this.db.codingRun.findUnique({ where: { runId }, select: { budgetLimitedBy: true } });
563
+ await recordBudgetEvent(this.db, runId, (coding?.budgetLimitedBy ?? "run"), refusal);
564
+ }
565
+ budgetEvent(runId) {
566
+ return loadBudgetEvent(this.db, runId);
567
+ }
568
+ topUpBudget(runId) {
569
+ return topUpCodingBudget(this.db, runId);
570
+ }
311
571
  async terminate(runId, status, error, audit) {
312
572
  const finishedAt = new Date();
573
+ let changed = false;
313
574
  const agentId = await this.db.$transaction(async (tx) => {
314
575
  const updated = await tx.run.updateMany({
315
576
  where: { id: runId, status: { in: ["pending", "running"] } },
@@ -317,6 +578,7 @@ export class PrismaContainerExecutionStore {
317
578
  });
318
579
  if (!updated.count)
319
580
  return null;
581
+ changed = true;
320
582
  if (audit) {
321
583
  await tx.codingRun.update({
322
584
  where: { runId },
@@ -331,6 +593,7 @@ export class PrismaContainerExecutionStore {
331
593
  // After commit, so the failure category is readable; cancelled/refused are skipped inside fileSelfDefect.
332
594
  if (agentId)
333
595
  await this.selfDefect({ id: runId, agentId, status, error, finishedAt });
596
+ return changed;
334
597
  }
335
598
  }
336
599
  /** Plain capabilities live only for the short provisioning window. */
@@ -351,6 +614,9 @@ export class RunCapabilityVault {
351
614
  this.values.delete(runId);
352
615
  }
353
616
  }
617
+ /** A run's deduplicated served packages and refusals (see summarizeRegistryFetches), and its package audit. */
618
+ /** The poll interval, at most, while a run with an overrun limit is near its grant. */
619
+ export const TOP_UP_NEAR_POLL_MS = 1_000;
354
620
  class PreflightError extends Error {
355
621
  }
356
622
  /** A refusal by repository authorization; its message is the failure category's prefix (repo_access_*). */
@@ -364,6 +630,11 @@ export class ContainerExecutor {
364
630
  now;
365
631
  observer;
366
632
  startedAt = new Map();
633
+ /**
634
+ * When the cluster last refused each queued run's launch for capacity (this.now() ms). Per
635
+ * process: another replica's drain may start the run again inside the backoff.
636
+ */
637
+ capacityRefusedAt = new Map();
367
638
  constructor(options) {
368
639
  this.options = options;
369
640
  this.artifactRoot = resolve(options.artifactRoot);
@@ -415,6 +686,9 @@ export class ContainerExecutor {
415
686
  const current = this.active.get(runId);
416
687
  if (current)
417
688
  return current;
689
+ // Refused for capacity a moment ago: keep its place and clock in the queue, untouched.
690
+ if (this.inCapacityBackoff(runId))
691
+ return;
418
692
  this.startedAt.set(runId, this.now().getTime());
419
693
  this.emit({ stage: "queued", runId });
420
694
  const execution = this.execute(runId).finally(async () => {
@@ -434,10 +708,16 @@ export class ContainerExecutor {
434
708
  return;
435
709
  if (TERMINAL_STATUSES.has(run.status))
436
710
  return this.cleanupTerminal(run);
437
- this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
438
711
  // Persist the cancellation fence first so a concurrent finisher cannot publish.
439
712
  const failure = this.failure("cancelled", { cancelled: true });
440
- await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit);
713
+ // False when the run reached a terminal status after the load above: its finisher owns the
714
+ // cleanup and the continuation comment (recover() cleans up any terminal run the reconciler sees
715
+ // later), so stopping now would overwrite a good outcome. "stopping" is emitted only once this
716
+ // stop owns the run, so it is always followed by its terminal and cleanup events.
717
+ if (!(await this.options.store.terminate(runId, "cancelled", failure.error, failure.audit)))
718
+ return;
719
+ this.emit({ stage: "stopping", runId, jobId: run.jobHandle?.id });
720
+ await this.recordRefusal(runId, run.proxySessionId);
441
721
  this.terminal(run, "cancelled", failure.audit);
442
722
  if (run.proxySessionId)
443
723
  await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
@@ -448,10 +728,11 @@ export class ContainerExecutor {
448
728
  this.options.capabilities.delete(runId);
449
729
  const input = this.preflightForCleanup(run);
450
730
  const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
451
- if (workspace) {
731
+ if (workspace)
452
732
  await this.options.vcs.cleanup(workspace).catch(() => undefined);
453
- await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
454
- }
733
+ const target = workspace ?? this.continuationTargetOf(run);
734
+ if (target)
735
+ await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
455
736
  await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
456
737
  this.emit({ stage: "cleanup", runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
457
738
  }
@@ -479,6 +760,9 @@ export class ContainerExecutor {
479
760
  await this.abandon(run, "coding_job_status_unavailable");
480
761
  return { state: "lost", reason: "coding_job_status_unavailable" };
481
762
  }
763
+ if (status.state === "pending" && status.capacityRefused && this.options.store.requeueAfterCapacityRefusal) {
764
+ return this.finishCapacityRequeue(run, run.jobHandle);
765
+ }
482
766
  if (status.state === "pending" || status.state === "running") {
483
767
  await this.options.store.heartbeat(run.runId);
484
768
  return { state: "active" };
@@ -499,9 +783,12 @@ export class ContainerExecutor {
499
783
  let sessionId = run.proxySessionId;
500
784
  let handle = run.jobHandle;
501
785
  let claimId = null;
786
+ let planned;
502
787
  let spendEnabled = Boolean(sessionId);
503
788
  try {
504
- const prepared = this.preflight(run);
789
+ // A launched run resumed after a restart is checked the way recover() checks it: its cost so
790
+ // far may be past the limit (a reply can cost more than its hold), and it is still delivered.
791
+ const prepared = this.preflight(run, { launched: Boolean(handle) });
505
792
  if (!handle) {
506
793
  if (run.provisioningClaim)
507
794
  return;
@@ -559,7 +846,7 @@ export class ContainerExecutor {
559
846
  const spec = this.jobSpec(run, inputArtifact);
560
847
  // Backends whose handle is derivable (Kubernetes) persist it first, so a crash between the
561
848
  // launch and the write still leaves a handle `abandon()` can stop and remove the run with.
562
- const planned = this.options.jobs.plannedHandle?.(spec);
849
+ planned = this.options.jobs.plannedHandle?.(spec);
563
850
  if (planned)
564
851
  await this.options.store.persistHandle(runId, claimId, planned);
565
852
  handle = await this.whileHeartbeating(runId, () => this.options.jobs.launch(spec));
@@ -567,12 +854,15 @@ export class ContainerExecutor {
567
854
  throw new Error("coding_job_handle_mismatch");
568
855
  }
569
856
  await this.options.store.persistHandle(runId, claimId, handle);
857
+ this.capacityRefusedAt.delete(runId);
570
858
  this.options.capabilities.delete(runId);
571
859
  this.emit({ stage: "launched", runId, jobId: handle.id, budgetReservedUsd: run.budgetUsd });
572
860
  }
573
861
  let delay = this.options.pollMinMs ?? 250;
574
862
  const maximumDelay = this.options.pollMaxMs ?? 5_000;
575
863
  let observedRunning = false;
864
+ // Overrun from available room: asked on each poll until it is granted in full or refused once.
865
+ let asksForTopUps = run.budgetLimitUsd !== undefined && run.budgetLimitUsd > run.budgetUsd;
576
866
  for (;;) {
577
867
  const status = await this.options.jobs.status(handle);
578
868
  if (status.state !== "pending" && status.state !== "running") {
@@ -584,6 +874,17 @@ export class ContainerExecutor {
584
874
  this.emit({ stage: "running", runId, jobId: handle.id });
585
875
  }
586
876
  await this.options.store.heartbeat(runId);
877
+ if (asksForTopUps && this.options.store.topUpBudget) {
878
+ const topUp = await this.options.store.topUpBudget(runId).catch((error) => {
879
+ containerLog.warn({ runId, error: safeError(error) }, "coding run top-up failed; asking again next poll");
880
+ return { outcome: "not_needed", near: true };
881
+ });
882
+ if (topUp.outcome === "unavailable" || topUp.outcome === "closed")
883
+ asksForTopUps = false;
884
+ // Near its grant: poll often, so the next step lands before a request no longer fits.
885
+ else if (topUp.near)
886
+ delay = Math.min(delay, TOP_UP_NEAR_POLL_MS);
887
+ }
587
888
  await this.sleep(delay);
588
889
  delay = Math.min(maximumDelay, Math.max(delay + 1, Math.ceil(delay * 1.5)));
589
890
  run = (await this.options.store.load(runId)) ?? run;
@@ -593,6 +894,34 @@ export class ContainerExecutor {
593
894
  }
594
895
  catch (error) {
595
896
  this.options.capabilities.delete(runId);
897
+ if (error instanceof JobCapacityError && claimId && this.options.store.requeueAfterCapacityRefusal) {
898
+ // The cluster has no room for the pod (Kubernetes: the namespace quota): the run waits in
899
+ // the queue again rather than failing. The launcher already removed what it created, so
900
+ // there is no job to stop; nothing ran, so the session is dropped with the claim and a
901
+ // fresh one is made on the next attempt.
902
+ if (sessionId)
903
+ await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
904
+ const requeued = await this.options.store.requeueAfterCapacityRefusal(runId, {
905
+ claimId,
906
+ ...(planned ? { planned } : {}),
907
+ });
908
+ if (requeued !== "spend_recorded") {
909
+ if (workspace)
910
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
911
+ if (requeued === "requeued") {
912
+ // The next attempt writes its own input; this one names a session that is gone.
913
+ await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
914
+ this.recordCapacityRefusal(runId);
915
+ this.emit({ stage: "queued", runId });
916
+ containerLog.info({ runId }, "coding run waiting for capacity: the cluster refused its pod");
917
+ }
918
+ return;
919
+ }
920
+ // Spend against a pod that never started: fail it like any launch error, and stop and remove
921
+ // the launcher's record through the planned handle so the run is never launched again.
922
+ if (planned)
923
+ handle = planned;
924
+ }
596
925
  if (sessionId)
597
926
  await this.options.sessions.cancelSession(sessionId).catch(() => undefined);
598
927
  if (handle)
@@ -603,7 +932,8 @@ export class ContainerExecutor {
603
932
  : !spendEnabled && error instanceof PreflightError
604
933
  ? "refused"
605
934
  : "failed";
606
- const failure = outOfBudget ? this.budgetFailure(error) : this.failure(error);
935
+ await this.recordRefusal(runId, sessionId);
936
+ const failure = outOfBudget ? await this.budgetFailure(runId, error) : this.failure(error);
607
937
  this.emit({
608
938
  stage: "stopping",
609
939
  runId,
@@ -622,7 +952,7 @@ export class ContainerExecutor {
622
952
  await this.options.vcs.cleanup(workspace).catch(() => undefined);
623
953
  await this.options.vcs.notifyContinuationFinished?.(workspace, outOfBudget ? "budget_exhausted" : "failed", {
624
954
  agentName: run.agentName,
625
- ...(outOfBudget ? { budgetSentence: this.budgetSentence(run) } : {}),
955
+ ...(outOfBudget ? { budgetSentence: await this.budgetSentence(run) } : {}),
626
956
  ...(unreadyService ? { serviceSentence: serviceUnreadySentence([unreadyService]) } : {}),
627
957
  ...(protectedPath ? { protectedPathSentence: protectedPathSentence(protectedPath) } : {}),
628
958
  });
@@ -687,39 +1017,62 @@ export class ContainerExecutor {
687
1017
  if (!decision.ok)
688
1018
  throw new RepoAccessError(`repo_access_${decision.reason}`);
689
1019
  }
690
- /** The originating issue for the PR title/body: control-plane data from the CodingRun row, key re-validated. */
691
- issueFor(run) {
1020
+ /**
1021
+ * The CodingRun's recorded issue as the Related section shows it (a Jira or
1022
+ * GitHub issue): control-plane data from the CodingRun row, key re-validated.
1023
+ */
1024
+ recordedIssue(run) {
692
1025
  const { issueProvider, issueKey } = run;
693
- if (!issueProvider || !issueKey || !ISSUE_KEY.test(issueKey))
1026
+ if (!issueProvider || !issueKey)
1027
+ return undefined;
1028
+ return issueReference(issueProvider, issueKey, (key) => this.options.issueUrl?.(issueProvider, key));
1029
+ }
1030
+ /**
1031
+ * The originating tracker issue for the PR title/body. A GitHub issue is not
1032
+ * one: its line comes from issueSourceFor (`From issue #N`).
1033
+ */
1034
+ issueFor(run) {
1035
+ if (run.issueProvider === "github")
694
1036
  return {};
695
- let url;
1037
+ const issue = this.recordedIssue(run);
1038
+ return issue ? { issue } : {};
1039
+ }
1040
+ /**
1041
+ * The PR title/body's originating issue, whichever system it is in: the
1042
+ * CodingRun's tracker issue (issueFor), else the GitHub issue in the same
1043
+ * repository whose mention started the run tree (`From issue #N`). Read from
1044
+ * the root run, so a continuation's rebuilt body keeps the root's line.
1045
+ * Never throws: a failed lookup gives no issue line.
1046
+ */
1047
+ async issueSourceFor(run) {
1048
+ const tracker = this.issueFor(run);
1049
+ if (tracker.issue || !this.options.store.linkedIssueSource)
1050
+ return tracker;
696
1051
  try {
697
- url = this.options.issueUrl?.(issueProvider, issueKey);
1052
+ const status = (await this.options.store.linkedIssueSource(run.rootCodingRunId ?? run.runId)).hostStatus;
1053
+ if (status?.provider === "github" &&
1054
+ status.issueMention === true &&
1055
+ status.repository.toLowerCase() === run.repository.toLowerCase() &&
1056
+ Number.isSafeInteger(status.number) &&
1057
+ status.number > 0) {
1058
+ return { issueNumber: status.number };
1059
+ }
698
1060
  }
699
- catch {
700
- url = undefined;
1061
+ catch (err) {
1062
+ containerLog.warn({ err, runId: run.runId }, "could not find the issue the run came from");
701
1063
  }
702
- const trackerName = Object.hasOwn(ISSUE_TRACKER_NAMES, issueProvider)
703
- ? ISSUE_TRACKER_NAMES[issueProvider]
704
- : undefined;
705
- return {
706
- issue: {
707
- key: issueKey,
708
- ...(url?.startsWith("https://") ? { url } : {}),
709
- ...(trackerName ? { trackerName } : {}),
710
- },
711
- };
1064
+ return {};
712
1065
  }
713
1066
  /** The new PR's Related pull requests input: earlier PRs of the same request, then this one. Never throws. */
714
1067
  async relatedFor(run) {
715
- // A continuation pushes onto an existing PR, whose body is never rewritten here.
1068
+ // A continuation's Related block is preserved from the current body (the description update keeps it).
716
1069
  if (run.rootCodingRunId || !this.options.store.relatedPullRequests)
717
1070
  return {};
718
1071
  try {
719
1072
  const earlier = await this.options.store.relatedPullRequests(run.runId);
720
1073
  if (earlier.length === 0)
721
1074
  return {};
722
- const { issue } = this.issueFor(run);
1075
+ const issue = this.recordedIssue(run);
723
1076
  return {
724
1077
  related: {
725
1078
  entries: [
@@ -753,14 +1106,20 @@ export class ContainerExecutor {
753
1106
  const entry = resolveCodingEntry(run.provider, run.model);
754
1107
  return { version: entry.priceVersion, entry: parseStoredEntry(entry) };
755
1108
  }
756
- preflight(run) {
1109
+ /**
1110
+ * `launched`: the run's job was already launched (it is running, or done and
1111
+ * only its push is left). The cost check applies only before launch: one
1112
+ * reply can cost more than its hold, so a launched run may already be past
1113
+ * its limit, and its finished work is still delivered.
1114
+ */
1115
+ preflight(run, options = {}) {
757
1116
  try {
758
1117
  if (run.status !== "pending" && run.status !== "running")
759
1118
  throw new Error("coding_run_status_invalid");
760
1119
  if (run.agentKind !== "coding" || !run.ownerId)
761
1120
  throw new Error("coding_run_ownership_invalid");
762
1121
  this.modelTerms(run);
763
- if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
1122
+ if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || (!options.launched && run.costUsd > run.budgetUsd)) {
764
1123
  throw new Error("coding_run_budget_invalid");
765
1124
  }
766
1125
  const profile = CodingProfileSchema.parse({
@@ -809,11 +1168,19 @@ export class ContainerExecutor {
809
1168
  // "failed" and is only flipped right before a success or out-of-budget return.
810
1169
  let outcome = "failed";
811
1170
  let finishedSummary;
1171
+ // Set with a success that spent past the run's base budget; says so on the host.
1172
+ let overrun = {};
812
1173
  // Set when a failed job's session relayed a model-provider failure; names it on the host.
813
1174
  // Only a failed job: a transient provider error the worker retried past did not end the run.
814
1175
  let providerClass;
815
1176
  // Set when finalizeChanges refused the collected diff for touching a protected path; names it on the host.
816
1177
  let protectedPath;
1178
+ // Set when a continuation's merge of its target branch left conflict markers behind; names the file on the host.
1179
+ let conflictMarkersPath;
1180
+ // Set with a success whose push merged the pull request's target branch; says what on the host.
1181
+ let merged = {};
1182
+ // Set when a run stopped at its budget still pushed its changes so far; says so on the host.
1183
+ let changesPushed;
817
1184
  try {
818
1185
  if (jobState !== "succeeded") {
819
1186
  const collected = await this.options.jobs.collect(handle).catch(() => null);
@@ -822,7 +1189,8 @@ export class ContainerExecutor {
822
1189
  // The worker usually fails outright when the proxy refuses a model
823
1190
  // request for budget; the session records that refusal.
824
1191
  if (await this.outOfBudget(sessionId)) {
825
- const failure = this.budgetFailure(`job_${reason}`, { issues: collected?.diagnosticIssues });
1192
+ await this.recordRefusal(run.runId, sessionId);
1193
+ const failure = await this.budgetFailure(run.runId, `job_${reason}`, { issues: collected?.diagnosticIssues });
826
1194
  await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
827
1195
  this.terminal(run, "budget_exhausted", failure.audit);
828
1196
  outcome = "budget_exhausted";
@@ -834,6 +1202,7 @@ export class ContainerExecutor {
834
1202
  ? this.providerFailureOf(run, provider, `job_${reason}`, { issues: collected?.diagnosticIssues })
835
1203
  : this.failure(`job_${reason}`, { issues: collected?.diagnosticIssues });
836
1204
  providerClass = provider?.class;
1205
+ await this.recordRefusal(run.runId, sessionId);
837
1206
  await this.options.store.terminate(run.runId, status, failure.error, failure.audit);
838
1207
  this.terminal(run, status, failure.audit);
839
1208
  return;
@@ -842,94 +1211,124 @@ export class ContainerExecutor {
842
1211
  this.emit({ stage: "collected", runId: run.runId, jobId: handle.id });
843
1212
  if (collected.reason !== "completed" || !collected.resultArtifact)
844
1213
  throw new Error("coding_result_missing");
845
- const output = parseCodingAgentOutputJson(collected.resultArtifact);
1214
+ const output = parseCodingWorkerOutputJson(collected.resultArtifact);
846
1215
  if (output.runId !== run.runId)
847
1216
  throw new Error("coding_result_run_mismatch");
848
- let current = await this.requireCurrent(run.runId);
849
- if (current.costUsd > current.budgetUsd || output.outcome === "budget_exhausted") {
1217
+ const current = this.requireActive(await this.requireCurrent(run.runId));
1218
+ // Finished work is delivered even when it cost more than the run's limit (one reply
1219
+ // can cost more than its hold); the status comment then says so (overrunDetails).
1220
+ if (output.outcome === "budget_exhausted") {
850
1221
  this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
851
- await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
852
- this.terminal(current, "budget_exhausted");
1222
+ const kept = await this.keepStoppedChanges(run, handle, current, output, existingWorkspace);
1223
+ await this.recordRefusal(run.runId, sessionId);
1224
+ if (kept) {
1225
+ await this.options.store.complete(run.runId, "budget_exhausted", kept.result, kept.record);
1226
+ this.emit({ stage: kept.finalized.outcome, runId: run.runId, jobId: handle.id });
1227
+ this.terminal(kept.current, "budget_exhausted");
1228
+ changesPushed =
1229
+ kept.finalized.outcome === "branch_pushed" ? {} : { pullRequestNumber: kept.finalized.pullRequestNumber };
1230
+ if (kept.finalized.outcome === "pull_request_updated" && kept.finalized.targetMerge) {
1231
+ merged = { mergeSentence: mergeSentence(kept.finalized.targetMerge) };
1232
+ }
1233
+ finishedSummary = output.summary;
1234
+ }
1235
+ else {
1236
+ const latest = await this.requireCurrent(run.runId).catch(() => current);
1237
+ await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, latest, "budget_exhausted"), dependencyCacheRecord(output));
1238
+ this.terminal(latest, "budget_exhausted");
1239
+ }
853
1240
  outcome = "budget_exhausted";
854
1241
  return;
855
1242
  }
856
- if (output.outcome === "no_changes") {
857
- await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current));
1243
+ // A workspace that merged its target branch has that merge to push even when the agent changed nothing.
1244
+ if (output.outcome === "no_changes" && !(await this.mergedTargetBranch(current, existingWorkspace))) {
1245
+ // Before complete: the terminal hook (a no-change fix round's re-review) reads the refreshed body.
1246
+ const description = await this.refreshDescriptionInTime(current, output, existingWorkspace);
1247
+ await this.recordRefusal(run.runId, sessionId);
1248
+ await this.options.store.complete(run.runId, "succeeded", this.resultFor(output, current), {
1249
+ ...dependencyCacheRecord(output),
1250
+ ...description,
1251
+ });
858
1252
  this.terminal(current, "succeeded");
859
1253
  outcome = "succeeded";
1254
+ overrun = this.overrunDetails(current);
860
1255
  finishedSummary = output.summary;
861
1256
  return;
862
1257
  }
863
- const preparedInput = this.preflight(current);
864
- const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
865
- if (!workspace)
866
- throw new Error("coding_workspace_lost");
867
- await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
868
- await normalizeCollectedLockfiles(current.provider, workspace.workspacePath);
869
- current = await this.requireCurrent(run.runId);
870
- if (current.costUsd > current.budgetUsd) {
871
- this.emit({ stage: "budget_cutoff", runId: run.runId, jobId: handle.id });
872
- await this.options.store.complete(run.runId, "budget_exhausted", this.resultFor(output, current, "budget_exhausted"));
873
- this.terminal(current, "budget_exhausted");
874
- outcome = "budget_exhausted";
875
- return;
876
- }
877
- // One more (usually cached) check right before anything is pushed: access
878
- // revoked while the run was working stops the push.
879
- await this.authorizeRepository(current, { inFlight: true });
880
- const report = await this.loadRegistryReport(run.runId);
881
- const finalized = await this.options.vcs.finalizeChanges(workspace, {
882
- summary: output.summary,
883
- tests: output.tests,
884
- tag: output.tag,
885
- ...report,
886
- ...this.issueFor(current),
887
- ...(await this.relatedFor(current)),
888
- });
1258
+ const pushed = await this.pushChanges(run, handle, current, output, output.summary, existingWorkspace);
1259
+ const { finalized, workspace } = pushed;
1260
+ // An empty diff pushes nothing, so it refreshes the description the way a
1261
+ // worker no_changes does, before complete (the terminal hook reads the body).
1262
+ const description = finalized.outcome === "no_changes"
1263
+ ? await this.refreshDescriptionInTime(pushed.current, output, workspace)
1264
+ : await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
1265
+ ? finalized.description
1266
+ : undefined);
889
1267
  // A pushed branch (local repository) is a successful run with no pull
890
1268
  // request; its branch is recorded on the run for the operator to merge.
891
- const result = this.resultFor(output, current, finalized.outcome, finalized);
1269
+ const result = this.resultFor(output, pushed.current, finalized.outcome, finalized);
1270
+ await this.recordRefusal(run.runId, sessionId);
892
1271
  await this.options.store.complete(run.runId, "succeeded", result, {
893
- baseSha: finalized.baseCommit,
894
- ...(finalized.outcome === "branch_pushed" ? { resultBranch: finalized.headRef } : {}),
1272
+ ...dependencyCacheRecord(output),
1273
+ ...pushedRecord(finalized),
1274
+ ...description,
895
1275
  });
896
- if (finalized.outcome === "pull_request_opened" ||
897
- finalized.outcome === "pull_request_updated" ||
898
- finalized.outcome === "branch_pushed") {
1276
+ if (finalized.outcome !== "no_changes") {
899
1277
  this.emit({ stage: finalized.outcome, runId: run.runId, jobId: handle.id });
900
1278
  }
901
- this.terminal(current, "succeeded");
1279
+ this.terminal(pushed.current, "succeeded");
902
1280
  outcome = "succeeded";
1281
+ overrun = this.overrunDetails(pushed.current);
1282
+ if (finalized.outcome === "pull_request_updated" && finalized.targetMerge) {
1283
+ merged = { mergeSentence: mergeSentence(finalized.targetMerge) };
1284
+ }
903
1285
  finishedSummary = output.summary;
904
1286
  }
905
1287
  catch (error) {
906
- if (await this.outOfBudget(sessionId)) {
907
- const failure = this.budgetFailure(error);
1288
+ if (error instanceof Error && error.message === "coding_run_no_longer_active") {
1289
+ // Stopped while its result was collected: the stop already wrote the terminal row.
1290
+ containerLog.info({ runId: run.runId }, "coding run was stopped before its result was published");
1291
+ }
1292
+ else if (await this.outOfBudget(sessionId)) {
1293
+ await this.recordRefusal(run.runId, sessionId);
1294
+ const failure = await this.budgetFailure(run.runId, error);
908
1295
  await this.options.store.terminate(run.runId, "budget_exhausted", failure.error, failure.audit);
909
1296
  this.terminal(run, "budget_exhausted", failure.audit);
910
1297
  outcome = "budget_exhausted";
911
1298
  }
912
1299
  else {
1300
+ await this.recordRefusal(run.runId, sessionId);
913
1301
  const failure = this.failure(error);
914
1302
  await this.options.store.terminate(run.runId, "failed", failure.error, failure.audit);
915
1303
  this.terminal(run, "failed", failure.audit);
916
1304
  protectedPath = protectedPathFromError(error) ?? undefined;
1305
+ conflictMarkersPath = conflictMarkersPathFromError(error) ?? undefined;
917
1306
  }
918
1307
  }
919
1308
  finally {
1309
+ if (run.sessionResume)
1310
+ await this.recordTranscript(run, handle);
920
1311
  await this.options.jobs.remove(handle).catch(() => undefined);
921
1312
  const input = this.preflightForCleanup(run);
922
1313
  const workspace = existingWorkspace ?? (input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null);
923
- if (workspace) {
1314
+ if (workspace)
924
1315
  await this.options.vcs.cleanup(workspace).catch(() => undefined);
925
- await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
1316
+ // A run another process launched has no workspace here; its pull request still gets the final status.
1317
+ const target = workspace ?? this.continuationTargetOf(run);
1318
+ if (target) {
1319
+ await this.options.vcs.notifyContinuationFinished?.(target, outcome, {
926
1320
  summary: finishedSummary,
927
1321
  agentName: run.agentName,
928
- ...(outcome === "budget_exhausted" ? { budgetSentence: this.budgetSentence(run) } : {}),
1322
+ ...(outcome === "succeeded" ? { ...merged, ...overrun } : {}),
1323
+ ...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
1324
+ ...(outcome === "budget_exhausted" && changesPushed ? { changesPushed, ...merged } : {}),
929
1325
  ...(outcome === "failed" && providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
930
1326
  ...(outcome === "failed" && protectedPath
931
1327
  ? { protectedPathSentence: protectedPathSentence(protectedPath) }
932
1328
  : {}),
1329
+ ...(outcome === "failed" && conflictMarkersPath
1330
+ ? { conflictMarkersSentence: conflictMarkersSentence(conflictMarkersPath) }
1331
+ : {}),
933
1332
  });
934
1333
  }
935
1334
  await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
@@ -937,6 +1336,412 @@ export class ContainerExecutor {
937
1336
  await this.notifyTerminal(run.runId);
938
1337
  }
939
1338
  }
1339
+ /** Whether the run's workspace merged its pull request's target branch (PreparedWorkspace.targetMerge). */
1340
+ async mergedTargetBranch(run, existingWorkspace) {
1341
+ if (!run.rootCodingRunId)
1342
+ return false;
1343
+ if (existingWorkspace)
1344
+ return Boolean(existingWorkspace.targetMerge);
1345
+ try {
1346
+ return Boolean((await this.options.vcs.recoverWorkspace(this.preflight(run, { launched: true })))?.targetMerge);
1347
+ }
1348
+ catch {
1349
+ return false;
1350
+ }
1351
+ }
1352
+ /**
1353
+ * Pushes a finished job's changes: copies the job's workspace into the run's,
1354
+ * then, right before finalizeChanges pushes, checks again that the owner still
1355
+ * has access and that the run was not stopped meanwhile. Shared by a finished
1356
+ * run and one stopped at its budget; throws whatever stops the push.
1357
+ */
1358
+ async pushChanges(run, handle, collectedFor, output, summary, existingWorkspace, options = {}) {
1359
+ const preparedInput = this.preflight(collectedFor, { launched: true });
1360
+ const workspace = existingWorkspace ?? (await this.options.vcs.recoverWorkspace(preparedInput));
1361
+ if (!workspace)
1362
+ throw new Error("coding_workspace_lost");
1363
+ await this.options.jobs.materializeWorkspace(handle, workspace.workspacePath);
1364
+ await normalizeCollectedLockfiles(collectedFor.provider, workspace.workspacePath);
1365
+ const current = await this.requireCurrent(run.runId);
1366
+ // One more (usually cached) check right before anything is pushed: access
1367
+ // revoked while the run was working stops the push.
1368
+ await this.authorizeRepository(current, { inFlight: true });
1369
+ // And a run stopped while its result was collected (stop_run, a cancelled task) pushes nothing.
1370
+ this.requireActive(await this.requireCurrent(run.runId));
1371
+ const report = await this.pullRequestRegistryReport(current);
1372
+ // keepDescription: a continuation's existing description is left as it is (no update, no proposal).
1373
+ const descriptionState = options.keepDescription ? null : await this.descriptionStateFor(current);
1374
+ const finalized = await this.options.vcs.finalizeChanges(workspace, {
1375
+ summary,
1376
+ tests: output.tests,
1377
+ tag: output.tag,
1378
+ ...report,
1379
+ ...restoredDependenciesFor(output),
1380
+ ...(await this.issueSourceFor(current)),
1381
+ ...(await this.relatedFor(current)),
1382
+ ...(descriptionState && "baseline" in descriptionState
1383
+ ? { description: { baseline: descriptionState.baseline } }
1384
+ : {}),
1385
+ });
1386
+ return { current, workspace, finalized, descriptionState };
1387
+ }
1388
+ /**
1389
+ * A run stopped at its budget whose worker exited cleanly keeps its changes so
1390
+ * far: they are pushed as the usual draft (or branch), with a summary that says
1391
+ * the work is unfinished. A fresh pull request's description starts with that
1392
+ * summary; a continuation leaves its pull request's description alone (the
1393
+ * status comment says the run stopped), and pushes nothing onto a pull request
1394
+ * that is no longer a draft. Null when there was nothing to push or the push
1395
+ * failed or was skipped for any reason: the run then ends out of budget without
1396
+ * them. Once pushed, the pushed outcome is always returned. Throws only when the
1397
+ * run was stopped meanwhile (the stop wrote its terminal row).
1398
+ */
1399
+ async keepStoppedChanges(run, handle, current, output, existingWorkspace) {
1400
+ const summary = budgetStoppedSummary(output.summary, current.request);
1401
+ const continuesPullRequest = Boolean(current.rootCodingRunId) && hasPullRequestDescription(current);
1402
+ let pushed;
1403
+ try {
1404
+ let workspace = existingWorkspace;
1405
+ if (continuesPullRequest) {
1406
+ workspace ??= await this.options.vcs.recoverWorkspace(this.preflight(current, { launched: true }));
1407
+ if (!workspace)
1408
+ throw new Error("coding_workspace_lost");
1409
+ if (!(await this.continuationStillDraft(current, workspace))) {
1410
+ containerLog.info({ runId: run.runId }, "a run stopped at its budget pushes nothing onto a pull request that is no longer a draft");
1411
+ return null;
1412
+ }
1413
+ }
1414
+ pushed = await this.pushChanges(run, handle, current, output, summary, workspace, {
1415
+ keepDescription: continuesPullRequest,
1416
+ });
1417
+ }
1418
+ catch (error) {
1419
+ if (error instanceof Error && error.message === "coding_run_no_longer_active")
1420
+ throw error;
1421
+ containerLog.warn({ runId: run.runId, reason: describeFailure(error) }, "could not push the changes of a run stopped at its budget; it ends without them");
1422
+ return null;
1423
+ }
1424
+ const { finalized } = pushed;
1425
+ if (finalized.outcome === "no_changes")
1426
+ return null;
1427
+ // Pushed: from here on the run records the pushed outcome whatever else fails.
1428
+ const description = await this.recordDescription(pushed.current, pushed.descriptionState, finalized.outcome === "pull_request_opened" || finalized.outcome === "pull_request_updated"
1429
+ ? finalized.description
1430
+ : undefined).catch(() => ({}));
1431
+ let result;
1432
+ try {
1433
+ result = this.resultFor({ ...output, summary }, pushed.current, finalized.outcome, finalized);
1434
+ }
1435
+ catch (error) {
1436
+ containerLog.warn({ runId: run.runId, reason: describeFailure(error) }, "could not record the worker's summary and tests of a budget-stopped run; recording its push alone");
1437
+ result = this.resultFor({
1438
+ ...output,
1439
+ summary: budgetStoppedSummary(WORKER_BUDGET_EXHAUSTED_SUMMARY, current.request),
1440
+ tests: [],
1441
+ tag: undefined,
1442
+ }, pushed.current, finalized.outcome, finalized);
1443
+ }
1444
+ return {
1445
+ current: pushed.current,
1446
+ finalized,
1447
+ result,
1448
+ record: { ...dependencyCacheRecord(output), ...pushedRecord(finalized), ...description },
1449
+ };
1450
+ }
1451
+ /**
1452
+ * The pull request description a continuation run is about to replace, for
1453
+ * its task. Undefined for a fresh run or when it cannot be loaded: the run
1454
+ * goes ahead without the note.
1455
+ */
1456
+ async currentDescriptionText(run) {
1457
+ const store = this.options.store;
1458
+ if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline) {
1459
+ return undefined;
1460
+ }
1461
+ try {
1462
+ return (await store.pullRequestDescriptionBaseline(run.rootCodingRunId)).previousAgentText;
1463
+ }
1464
+ catch (err) {
1465
+ containerLog.debug({ err, runId: run.runId }, "could not load the pull request description for the task");
1466
+ return undefined;
1467
+ }
1468
+ }
1469
+ /**
1470
+ * The issue a continuation's pull request was opened for, read for its task
1471
+ * the way the first run had it: the root run's Jira issue, else the GitHub
1472
+ * issue whose mention started the work (same repository), else the issue the
1473
+ * description wardby last wrote closes (same repository). Each candidate is
1474
+ * read in turn until one is an issue. Undefined for a fresh or local run, or
1475
+ * on any failure: the run goes ahead without it.
1476
+ */
1477
+ async linkedIssueFor(run, description) {
1478
+ const store = this.options.store;
1479
+ const read = this.options.readLinkedIssue;
1480
+ if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !read || !store.linkedIssueSource)
1481
+ return undefined;
1482
+ let timer;
1483
+ const timedOut = new Promise((resolve) => {
1484
+ timer = setTimeout(() => {
1485
+ containerLog.warn({ runId: run.runId }, "reading the linked issue timed out; continuing without it");
1486
+ resolve(undefined);
1487
+ }, LINKED_ISSUE_READ_TIMEOUT_MS);
1488
+ });
1489
+ try {
1490
+ return await Promise.race([this.readLinkedIssue(run, run.rootCodingRunId, description, read), timedOut]);
1491
+ }
1492
+ catch (err) {
1493
+ containerLog.warn({ err, runId: run.runId }, "could not read the linked issue; continuing without it");
1494
+ return undefined;
1495
+ }
1496
+ finally {
1497
+ clearTimeout(timer);
1498
+ }
1499
+ }
1500
+ async readLinkedIssue(run, rootRunId, description, read) {
1501
+ const source = await this.options.store.linkedIssueSource(rootRunId);
1502
+ const candidates = [];
1503
+ if (source.issueProvider === "jira" && source.issueKey && ISSUE_KEY.test(source.issueKey)) {
1504
+ candidates.push({ provider: "jira", key: source.issueKey });
1505
+ }
1506
+ const status = source.hostStatus;
1507
+ if (status?.provider === "github" &&
1508
+ status.repository.toLowerCase() === run.repository.toLowerCase() &&
1509
+ Number.isSafeInteger(status.number) &&
1510
+ status.number > 0) {
1511
+ candidates.push({ provider: "github", repository: run.repository, number: status.number });
1512
+ }
1513
+ const closed = closingIssueNumber(description, run.repository);
1514
+ if (closed !== undefined && !candidates.some((ref) => ref.provider === "github" && ref.number === closed)) {
1515
+ candidates.push({ provider: "github", repository: run.repository, number: closed });
1516
+ }
1517
+ for (const ref of candidates) {
1518
+ let issue;
1519
+ try {
1520
+ issue = await read(ref);
1521
+ }
1522
+ catch (err) {
1523
+ // The next candidate may still be readable.
1524
+ containerLog.warn({ err, runId: run.runId, issue: ref }, "could not read a linked issue candidate");
1525
+ continue;
1526
+ }
1527
+ if (!issue)
1528
+ continue;
1529
+ return ref.provider === "github"
1530
+ ? { kind: "github", number: ref.number, title: issue.title, body: issue.body }
1531
+ : { kind: "jira", key: ref.key, title: issue.title, body: issue.body };
1532
+ }
1533
+ return undefined;
1534
+ }
1535
+ /**
1536
+ * A fix round's description baseline (null: no description flow -- a fresh
1537
+ * run, or a store without it). Never throws: a load failure is recorded as
1538
+ * skipped:error and the push goes ahead without a description update.
1539
+ */
1540
+ /**
1541
+ * Whether a continuation's pull request is still a draft: the host's answer
1542
+ * when it has one, else the root run's record of wardby marking it ready.
1543
+ * A failed record lookup counts as not a draft (nothing unfinished is pushed).
1544
+ */
1545
+ async continuationStillDraft(run, workspace) {
1546
+ const draft = await this.options.vcs.continuationPullRequestDraft?.(workspace).catch(() => undefined);
1547
+ if (draft !== undefined)
1548
+ return draft;
1549
+ const store = this.options.store;
1550
+ if (!run.rootCodingRunId || !store.pullRequestMarkedReady)
1551
+ return true;
1552
+ try {
1553
+ return !(await store.pullRequestMarkedReady(run.rootCodingRunId));
1554
+ }
1555
+ catch (err) {
1556
+ containerLog.warn({ err, runId: run.runId }, "could not read whether the pull request was marked ready");
1557
+ return false;
1558
+ }
1559
+ }
1560
+ async descriptionStateFor(run) {
1561
+ const store = this.options.store;
1562
+ if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestDescriptionBaseline)
1563
+ return null;
1564
+ let stored;
1565
+ try {
1566
+ stored = await store.pullRequestDescriptionBaseline(run.rootCodingRunId);
1567
+ }
1568
+ catch (err) {
1569
+ containerLog.warn({ err, runId: run.runId }, "could not load the pull request description baseline");
1570
+ return { failed: true };
1571
+ }
1572
+ const { legacySource, ...baseline } = stored;
1573
+ if (baseline.recordedSha256 !== null || !legacySource)
1574
+ return { baseline };
1575
+ const legacySha256 = await this.legacyDescriptionSha256(run.rootCodingRunId, legacySource);
1576
+ return { baseline: legacySha256 ? { ...baseline, legacySha256 } : baseline };
1577
+ }
1578
+ /**
1579
+ * A legacy pull request's description as its root run wrote it, rebuilt from
1580
+ * stored records (the final message, tests, packages, dependency cache line
1581
+ * and issue line; the Related section is not hashed), and hashed. Undefined
1582
+ * when the root stored no final message, or on any failure: the description
1583
+ * then counts as edited, the safe direction. A lock file line the root's
1584
+ * refusal warning may have carried is not stored, so such a description
1585
+ * never matches.
1586
+ */
1587
+ async legacyDescriptionSha256(rootRunId, source) {
1588
+ if (!source.summary)
1589
+ return undefined;
1590
+ try {
1591
+ const tests = storedTests(source.tests);
1592
+ const restored = source.dependencyCache === undefined
1593
+ ? []
1594
+ : restoredDependencyFolders(sanitizeDependencyCacheReport(source.dependencyCache));
1595
+ const body = pullRequestBody({
1596
+ repository: source.repository,
1597
+ baseRef: source.baseRef,
1598
+ headRef: source.headRef,
1599
+ runId: rootRunId,
1600
+ summary: source.summary,
1601
+ ...(tests.length > 0 ? { tests } : {}),
1602
+ ...(await this.loadRegistryReport(rootRunId)),
1603
+ ...(restored.length > 0 ? { restoredDependencies: restored } : {}),
1604
+ ...this.issueFor({ issueProvider: source.issueProvider ?? null, issueKey: source.issueKey ?? null }),
1605
+ });
1606
+ return descriptionSha256(body);
1607
+ }
1608
+ catch (err) {
1609
+ containerLog.debug({ err, rootRunId }, "could not rebuild a legacy pull request description");
1610
+ return undefined;
1611
+ }
1612
+ }
1613
+ /**
1614
+ * The Packages section's input: on a continuation, every package installed
1615
+ * by the runs that pushed to the pull request plus this run (merged in run
1616
+ * order); refusals are this run's only, since they describe the current
1617
+ * state. A fresh run, or a failed listing, reports this run's alone.
1618
+ */
1619
+ async pullRequestRegistryReport(run) {
1620
+ const own = await this.loadRegistryReport(run.runId);
1621
+ const store = this.options.store;
1622
+ if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !store.pullRequestRunIds)
1623
+ return own;
1624
+ let earlier;
1625
+ try {
1626
+ earlier = (await store.pullRequestRunIds(run.rootCodingRunId)).filter((id) => id !== run.runId);
1627
+ }
1628
+ catch (err) {
1629
+ containerLog.warn({ err, runId: run.runId }, "could not list the pull request's runs; listing this run's packages");
1630
+ return own;
1631
+ }
1632
+ const reports = await Promise.all(earlier.map((id) => this.loadRegistryReport(id)));
1633
+ const packages = mergePackages([...reports.map((report) => report.packages ?? []), own.packages ?? []]);
1634
+ // Package audit mode is cumulative like packages: the banner stays on the
1635
+ // pull request once any of its runs had the mode.
1636
+ const packageAudit = mergePackageAuditReports([...reports.map((report) => report.packageAudit), own.packageAudit]);
1637
+ return {
1638
+ ...(packages.length > 0 ? { packages } : {}),
1639
+ ...(own.packageRefusals ? { packageRefusals: own.packageRefusals } : {}),
1640
+ ...(packageAudit ? { packageAudit } : {}),
1641
+ };
1642
+ }
1643
+ /**
1644
+ * The run's description columns for store.complete. A fresh run that wrote
1645
+ * its PR records the hash on its own (root) row; a fix round that wrote or
1646
+ * kept wardby's text compare-and-sets the root's hash. Never throws.
1647
+ */
1648
+ async recordDescription(run, state, outcome) {
1649
+ if (!hasPullRequestDescription(run))
1650
+ return {};
1651
+ if (!run.rootCodingRunId) {
1652
+ return outcome?.kind === "written"
1653
+ ? { prDescriptionOutcome: "written", prDescriptionSha256: outcome.sha256, prDescriptionRunId: run.runId }
1654
+ : {};
1655
+ }
1656
+ if (!state)
1657
+ return {};
1658
+ if ("failed" in state)
1659
+ return { prDescriptionOutcome: "skipped:error" };
1660
+ if (!outcome || outcome.kind === "written")
1661
+ return {};
1662
+ const label = descriptionOutcomeLabel(outcome);
1663
+ if (outcome.kind !== "replaced" && outcome.kind !== "refreshed" && outcome.kind !== "unchanged") {
1664
+ return { prDescriptionOutcome: label };
1665
+ }
1666
+ const store = this.options.store;
1667
+ if (!store.recordPullRequestDescription)
1668
+ return { prDescriptionOutcome: label };
1669
+ try {
1670
+ const won = await store.recordPullRequestDescription({
1671
+ rootRunId: run.rootCodingRunId,
1672
+ expectedSha256: state.baseline.recordedSha256,
1673
+ sha256: outcome.sha256,
1674
+ agentTextRunId: outcome.agentTextRunId,
1675
+ });
1676
+ // A lost race leaves the root's hash as the other write left it: the next
1677
+ // round sees a mismatch and falls back to a comment, the safe direction.
1678
+ if (!won) {
1679
+ containerLog.warn({ runId: run.runId, rootRunId: run.rootCodingRunId }, "pull request description hash changed meanwhile; not recorded");
1680
+ }
1681
+ return { prDescriptionOutcome: label };
1682
+ }
1683
+ catch (err) {
1684
+ containerLog.warn({ err, runId: run.runId }, "could not record the pull request description hash");
1685
+ return { prDescriptionOutcome: "skipped:error" };
1686
+ }
1687
+ }
1688
+ /**
1689
+ * refreshDescriptionWithoutPush, capped at DESCRIPTION_REFRESH_TIMEOUT_MS so a
1690
+ * slow host cannot hold up store.complete; a timeout is skipped:error.
1691
+ */
1692
+ async refreshDescriptionInTime(run, output, existingWorkspace) {
1693
+ let timer;
1694
+ const timedOut = new Promise((resolve) => {
1695
+ timer = setTimeout(() => {
1696
+ containerLog.warn({ runId: run.runId, timeoutMs: DESCRIPTION_REFRESH_TIMEOUT_MS }, "pull request description refresh timed out");
1697
+ resolve({ prDescriptionOutcome: "skipped:error" });
1698
+ }, DESCRIPTION_REFRESH_TIMEOUT_MS);
1699
+ });
1700
+ try {
1701
+ return await Promise.race([this.refreshDescriptionWithoutPush(run, output, existingWorkspace), timedOut]);
1702
+ }
1703
+ finally {
1704
+ clearTimeout(timer);
1705
+ }
1706
+ }
1707
+ /**
1708
+ * A fix round that changed nothing still re-derives its PR description (no
1709
+ * push). Never throws: any failure is recorded as skipped:error.
1710
+ */
1711
+ async refreshDescriptionWithoutPush(run, output, existingWorkspace) {
1712
+ const vcs = this.options.vcs;
1713
+ if (!run.rootCodingRunId || !hasPullRequestDescription(run) || !vcs.refreshPullRequestDescription)
1714
+ return {};
1715
+ const state = await this.descriptionStateFor(run);
1716
+ if (!state || "failed" in state)
1717
+ return this.recordDescription(run, state, undefined);
1718
+ try {
1719
+ const input = this.preflightForCleanup(run);
1720
+ // Only the pull request is needed: a run another process launched has no workspace here.
1721
+ const workspace = existingWorkspace ??
1722
+ (input ? await vcs.recoverWorkspace(input).catch(() => null) : null) ??
1723
+ this.continuationTargetOf(run);
1724
+ if (!workspace)
1725
+ throw new Error("coding_workspace_lost");
1726
+ // The same in-flight access check a push gets: a revoked owner's run edits nothing.
1727
+ await this.authorizeRepository(run, { inFlight: true });
1728
+ const details = {
1729
+ summary: output.summary,
1730
+ tests: output.tests,
1731
+ tag: output.tag,
1732
+ ...(await this.pullRequestRegistryReport(run)),
1733
+ ...restoredDependenciesFor(output),
1734
+ ...(await this.issueSourceFor(run)),
1735
+ description: { baseline: state.baseline },
1736
+ };
1737
+ const outcome = await vcs.refreshPullRequestDescription(workspace, details);
1738
+ return await this.recordDescription(run, state, outcome);
1739
+ }
1740
+ catch (err) {
1741
+ containerLog.warn({ err, runId: run.runId }, "could not refresh the pull request description");
1742
+ return { prDescriptionOutcome: "skipped:error" };
1743
+ }
1744
+ }
940
1745
  /** Runs the onCodingRunTerminal hook; nothing it does, throws, or rejects reaches the caller. */
941
1746
  async notifyTerminal(runId) {
942
1747
  const hook = this.options.onCodingRunTerminal;
@@ -956,10 +1761,15 @@ export class ContainerExecutor {
956
1761
  try {
957
1762
  const report = await this.options.registryReport?.(runId);
958
1763
  const packages = Array.isArray(report?.packages) ? report.packages : [];
959
- const packageRefusals = Array.isArray(report?.packageRefusals) ? report.packageRefusals : [];
1764
+ // A listing-only refusal is for get_run and the agent, not the PR (see LISTING_REFUSAL_FILENAME).
1765
+ const packageRefusals = Array.isArray(report?.packageRefusals) ? warningRefusals(report.packageRefusals) : [];
1766
+ const packageAudit = isPackageAuditMode(report?.packageAuditMode)
1767
+ ? { mode: report.packageAuditMode, entries: Array.isArray(report.packageAudit) ? report.packageAudit : [] }
1768
+ : undefined;
960
1769
  return {
961
1770
  ...(packages.length > 0 ? { packages } : {}),
962
1771
  ...(packageRefusals.length > 0 ? { packageRefusals } : {}),
1772
+ ...(packageAudit ? { packageAudit } : {}),
963
1773
  };
964
1774
  }
965
1775
  catch {
@@ -1081,12 +1891,89 @@ export class ContainerExecutor {
1081
1891
  async warmUp() {
1082
1892
  await this.options.jobs.warmUp?.();
1083
1893
  }
1894
+ /**
1895
+ * Throws coding_run_no_longer_active once something else made the run terminal (a stop's
1896
+ * cancellation fence): nothing may then be pushed, opened, or rewritten on the host for it.
1897
+ */
1898
+ requireActive(run) {
1899
+ if (TERMINAL_STATUSES.has(run.status))
1900
+ throw new Error("coding_run_no_longer_active");
1901
+ return run;
1902
+ }
1084
1903
  async requireCurrent(runId) {
1085
1904
  const current = await this.options.store.load(runId);
1086
1905
  if (!current)
1087
1906
  throw new Error("coding_run_not_found");
1088
1907
  return current;
1089
1908
  }
1909
+ /**
1910
+ * Recovery's half of a capacity refusal: the launcher recorded it, but the process died before the
1911
+ * store put the run back in the queue. The planned handle the attempt persisted is the ownership
1912
+ * proof; the run waits in the queue again, never reported ended.
1913
+ */
1914
+ async finishCapacityRequeue(run, planned) {
1915
+ const outcome = await this.options.store.requeueAfterCapacityRefusal(run.runId, { planned });
1916
+ if (outcome === "spend_recorded") {
1917
+ await this.abandon(run, "coding_ambiguous_provisioning");
1918
+ return { state: "lost", reason: "coding_ambiguous_provisioning" };
1919
+ }
1920
+ if (outcome === "not_owned") {
1921
+ const current = await this.options.store.load(run.runId);
1922
+ if (!current || !TERMINAL_STATUSES.has(current.status))
1923
+ return { state: "active" };
1924
+ await this.cleanupTerminal(current);
1925
+ return { state: "terminal" };
1926
+ }
1927
+ // The session went with the requeue; the rest of what the dead attempt left is removed here.
1928
+ this.options.capabilities.delete(run.runId);
1929
+ const input = this.preflightForCleanup(run);
1930
+ const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
1931
+ if (workspace)
1932
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
1933
+ await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
1934
+ this.emit({ stage: "queued", runId: run.runId });
1935
+ containerLog.info({ runId: run.runId }, "coding run waiting for capacity: recovered a refusal its process left");
1936
+ return { state: "active" };
1937
+ }
1938
+ /**
1939
+ * After drainCodingQueue failed a queued run (coding_queue_timeout): releases what an earlier
1940
+ * attempt the cluster refused left behind, as stop() does for a queued run. A refused
1941
+ * continuation already marked its pull request as being worked on; that check is finished
1942
+ * here. Best effort; the artifact is removed only where this process wrote it.
1943
+ */
1944
+ async cleanupQueueTimeout(runId) {
1945
+ this.capacityRefusedAt.delete(runId);
1946
+ this.options.capabilities.delete(runId);
1947
+ const run = await this.options.store.load(runId);
1948
+ if (run) {
1949
+ const input = this.preflightForCleanup(run);
1950
+ const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
1951
+ if (workspace)
1952
+ await this.options.vcs.cleanup(workspace).catch(() => undefined);
1953
+ const target = workspace ?? this.continuationTargetOf(run);
1954
+ if (target)
1955
+ await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
1956
+ }
1957
+ await rm(this.artifactPath(runId), { recursive: true, force: true }).catch(() => undefined);
1958
+ }
1959
+ recordCapacityRefusal(runId) {
1960
+ const now = this.now().getTime();
1961
+ // Drop the ones whose backoff is over, so runs that never come back (timed out in the queue) do not pile up.
1962
+ for (const [id, at] of this.capacityRefusedAt) {
1963
+ if (now - at >= CAPACITY_RETRY_BACKOFF_MS)
1964
+ this.capacityRefusedAt.delete(id);
1965
+ }
1966
+ this.capacityRefusedAt.set(runId, now);
1967
+ }
1968
+ inCapacityBackoff(runId) {
1969
+ const at = this.capacityRefusedAt.get(runId);
1970
+ if (at === undefined)
1971
+ return false;
1972
+ if (this.now().getTime() - at < CAPACITY_RETRY_BACKOFF_MS)
1973
+ return true;
1974
+ this.capacityRefusedAt.delete(runId);
1975
+ return false;
1976
+ }
1090
1977
  /**
1091
1978
  * False when the launcher reports this run's job would not fit the cluster right now; the run
1092
1979
  * is then queued and drainCodingQueue retries it. Launchers and stores without the hooks, and
@@ -1128,6 +2015,10 @@ export class ContainerExecutor {
1128
2015
  // A run keeps the tool image it was dispatched with; rows from before CodingRun.toolImage use the default.
1129
2016
  ...(provider === "claude-code" ? { toolImage: run.toolImage ?? this.options.claudeToolRunnerImage } : {}),
1130
2017
  inputArtifact,
2018
+ // Only when on: every other run's spec stays exactly what it was.
2019
+ ...(run.dependencyCache ? { dependencyCache: true } : {}),
2020
+ // Claude Code only: the launcher gives the SDK's config dir, where the session grows, its own scratch.
2021
+ ...(run.sessionResume && provider === "claude-code" ? { sessionResume: true } : {}),
1131
2022
  timeoutSec: run.timeoutSec,
1132
2023
  limits: { ...this.options.limits, ...(run.workspaceDiskMb ? { diskMb: run.workspaceDiskMb } : {}) },
1133
2024
  labels: {},
@@ -1154,8 +2045,13 @@ export class ContainerExecutor {
1154
2045
  const destination = join(directory, "input.json");
1155
2046
  const temporary = join(directory, `.input-${randomUUID()}.tmp`);
1156
2047
  const services = parseStoredServices(run.services);
1157
- const task = withBaseCommit(run.task, workspace.baseCommit);
1158
- if (task === run.task && /^[0-9a-f]{40}$/.test(workspace.baseCommit)) {
2048
+ const withCommit = withBaseCommit(run.task, workspace.baseCommit);
2049
+ const description = await this.currentDescriptionText(run);
2050
+ // The ticket first, then the description: each is cut to what's left of the task's budget.
2051
+ // The target-branch merge note (conflicts to resolve) comes before the ticket and the description.
2052
+ const withMerge = withTargetMerge(withCommit, workspace.targetMerge, run.headRef);
2053
+ const task = withPullRequestDescription(withLinkedIssue(withMerge, await this.linkedIssueFor(run, description)), description);
2054
+ if (withCommit === run.task && /^[0-9a-f]{40}$/.test(workspace.baseCommit)) {
1159
2055
  containerLog.warn({ event: "coding.base_commit_omitted", runId: run.runId }, "the coding task left no room for the base commit line");
1160
2056
  }
1161
2057
  // A repository context read must never fail a run ("the run never fails because of context"):
@@ -1179,17 +2075,31 @@ export class ContainerExecutor {
1179
2075
  headRef: run.headRef,
1180
2076
  task,
1181
2077
  model: run.model,
1182
- budgetUsd: run.budgetUsd,
2078
+ // The run's limit, not its grant: the proxy enforces the grant, and top-ups raise it while
2079
+ // the job runs. Claude Code's own budget stop (its SDK maxBudgetUsd) is set from this, so it
2080
+ // never stops a run before a top-up can; Codex has no stop of its own.
2081
+ budgetUsd: run.budgetLimitUsd ?? run.budgetUsd,
1183
2082
  deadlineAt: deadlineAt.toISOString(),
1184
2083
  continuationOf: run.rootCodingRunId ? { runId: run.rootCodingRunId } : undefined,
1185
2084
  // Only when true: an untraced run's input stays exactly what older workers expect.
1186
2085
  ...(run.debugTrace ? { debugTrace: true } : {}),
2086
+ // Only on a traced run whose agent sets a cap: every other run's input stays exactly what older workers expect.
2087
+ ...(run.debugTrace && run.debugTraceMaxMb ? { debugTraceMaxMb: run.debugTraceMaxMb } : {}),
1187
2088
  // Only when the agent sets one: every other run's input stays exactly what older workers expect.
1188
2089
  ...(run.maxTurns ? { maxTurns: run.maxTurns } : {}),
2090
+ // Only when the agent sets one: every other run's input stays exactly what older workers expect.
2091
+ ...(run.maxOutputTokens ? { maxOutputTokens: run.maxOutputTokens } : {}),
1189
2092
  // Only when off: every other run's input stays exactly what older workers expect.
1190
2093
  ...(run.repoSkills === false ? { repoSkills: false } : {}),
1191
2094
  // Only when off: every other run's input stays exactly what older workers expect.
1192
2095
  ...(run.claudeBareMode === false ? { claudeBareMode: false } : {}),
2096
+ // Only when on: every other run's input stays exactly what older workers expect. The volume
2097
+ // size (the job spec's limits.diskMb) bounds what the cache may restore.
2098
+ ...(run.dependencyCache
2099
+ ? { dependencyCache: true, diskMb: run.workspaceDiskMb ?? this.options.limits.diskMb }
2100
+ : {}),
2101
+ // Only when on: every other run's input stays exactly what older workers expect.
2102
+ ...(run.sessionResume && run.provider === "claude-code" ? { sessionResume: true } : {}),
1193
2103
  // Only when there are some: every other run's input stays exactly what older workers expect.
1194
2104
  ...(services.length > 0 ? { services: workerServices(services) } : {}),
1195
2105
  // Only when there is something to load: every other run's input stays exactly what older workers expect.
@@ -1231,6 +2141,58 @@ export class ContainerExecutor {
1231
2141
  artifactPath(runId) {
1232
2142
  return resolve(this.artifactRoot, runId);
1233
2143
  }
2144
+ /**
2145
+ * A continuation's pull request from the run record alone, for the host-side
2146
+ * finish signals when the workspace is gone (another control-plane process
2147
+ * launched the run). Null for a fresh run: it has no pull request yet.
2148
+ */
2149
+ continuationTargetOf(run) {
2150
+ if (!run.rootCodingRunId)
2151
+ return null;
2152
+ return {
2153
+ runId: run.runId,
2154
+ repository: run.repository,
2155
+ baseRef: run.baseRef,
2156
+ headRef: run.headRef,
2157
+ continuation: { rootRunId: run.rootCodingRunId },
2158
+ };
2159
+ }
2160
+ /**
2161
+ * Session resume: never throws, never changes the run's outcome. The recorder redacts; only a status
2162
+ * code is logged. The run capability is already gone from the vault, so it is not passed (the recorder
2163
+ * removes its fixed token shape).
2164
+ */
2165
+ async recordTranscript(run, handle) {
2166
+ const recorder = this.options.transcripts;
2167
+ if (!recorder)
2168
+ return;
2169
+ const jobs = this.options.jobs;
2170
+ let timer;
2171
+ try {
2172
+ const recording = Promise.resolve()
2173
+ .then(() => recorder.record({
2174
+ runId: run.runId,
2175
+ agentId: run.agentId,
2176
+ provider: run.provider,
2177
+ read: (maxBytes) => jobs.readTranscript ? jobs.readTranscript(handle, maxBytes) : Promise.resolve(undefined),
2178
+ }))
2179
+ .catch(() => "failed");
2180
+ const timeout = new Promise((resolve) => {
2181
+ timer = setTimeout(() => resolve("timeout"), TRANSCRIPT_RECORD_TIMEOUT_MS);
2182
+ timer.unref();
2183
+ });
2184
+ const result = await Promise.race([recording, timeout]);
2185
+ if (result !== "stored")
2186
+ containerLog.info({ runId: run.runId, transcript: result }, "run transcript not stored");
2187
+ }
2188
+ catch {
2189
+ // never changes the run's outcome
2190
+ }
2191
+ finally {
2192
+ if (timer)
2193
+ clearTimeout(timer);
2194
+ }
2195
+ }
1234
2196
  preflightForCleanup(run) {
1235
2197
  if (!Array.isArray(run.protectedPaths) || !run.protectedPaths.every((path) => typeof path === "string"))
1236
2198
  return null;
@@ -1255,6 +2217,7 @@ export class ContainerExecutor {
1255
2217
  const failure = this.failure(reason);
1256
2218
  await this.options.store.terminate(run.runId, "lost", failure.error, failure.audit);
1257
2219
  this.terminal(run, "lost", failure.audit);
2220
+ await this.recordRefusal(run.runId, run.proxySessionId);
1258
2221
  if (run.proxySessionId)
1259
2222
  await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
1260
2223
  if (run.jobHandle) {
@@ -1264,14 +2227,17 @@ export class ContainerExecutor {
1264
2227
  }
1265
2228
  const input = this.preflightForCleanup(run);
1266
2229
  const workspace = input ? await this.options.vcs.recoverWorkspace(input).catch(() => null) : null;
1267
- if (workspace) {
2230
+ if (workspace)
1268
2231
  await this.options.vcs.cleanup(workspace).catch(() => undefined);
1269
- await this.options.vcs.notifyContinuationFinished?.(workspace, "failed", { agentName: run.agentName });
1270
- }
2232
+ const target = workspace ?? this.continuationTargetOf(run);
2233
+ if (target)
2234
+ await this.options.vcs.notifyContinuationFinished?.(target, "failed", { agentName: run.agentName });
1271
2235
  await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
1272
2236
  this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
1273
2237
  }
1274
2238
  async cleanupTerminal(run) {
2239
+ // A run another path ended (or one recovered after a crash) still gets its refusal; the copy is idempotent.
2240
+ await this.recordRefusal(run.runId, run.proxySessionId);
1275
2241
  if (run.proxySessionId)
1276
2242
  await this.options.sessions.cancelSession(run.proxySessionId).catch(() => undefined);
1277
2243
  if (run.jobHandle) {
@@ -1301,7 +2267,9 @@ export class ContainerExecutor {
1301
2267
  const protectedPathFailed = run.status === "failed" && run.failureCategory === PROTECTED_PATH_CATEGORY;
1302
2268
  await this.options.vcs.notifyContinuationFinished?.(workspace, outcome, {
1303
2269
  agentName: run.agentName,
1304
- ...(outcome === "budget_exhausted" ? { budgetSentence: this.budgetSentence(run) } : {}),
2270
+ ...(outcome === "succeeded" ? this.overrunDetails(run) : {}),
2271
+ ...(outcome === "budget_exhausted" ? { budgetSentence: await this.budgetSentence(run) } : {}),
2272
+ ...(outcome === "budget_exhausted" ? storedChangesPushed(run.result) : {}),
1305
2273
  ...(providerClass ? { providerSentence: providerSentence(providerClass) } : {}),
1306
2274
  ...(unreadyServices ? { serviceSentence: serviceUnreadySentence(unreadyServices) } : {}),
1307
2275
  ...(protectedPathFailed ? { protectedPathSentence: protectedPathSentence(null) } : {}),
@@ -1310,6 +2278,16 @@ export class ContainerExecutor {
1310
2278
  await rm(this.artifactPath(run.runId), { recursive: true, force: true }).catch(() => undefined);
1311
2279
  this.emit({ stage: "cleanup", runId: run.runId, jobId: run.jobHandle?.id, cleanupSucceeded: true });
1312
2280
  }
2281
+ /**
2282
+ * Records the session's budget refusal on the run, at every ending: a run the proxy refused once may
2283
+ * still finish, fail for another reason, or be stopped. A session with no refusal adds nothing.
2284
+ * Best effort; never throws.
2285
+ */
2286
+ async recordRefusal(runId, sessionId) {
2287
+ if (!sessionId)
2288
+ return;
2289
+ await this.options.store.recordBudgetRefusal?.(runId, sessionId).catch(() => undefined);
2290
+ }
1313
2291
  /** Whether the proxy refused this run's session for budget. Never throws. */
1314
2292
  async outOfBudget(sessionId) {
1315
2293
  if (!sessionId || !this.options.sessions.budgetExhausted)
@@ -1337,11 +2315,23 @@ export class ContainerExecutor {
1337
2315
  audit: { failureCategory: `provider_${provider.class}`, diagnosticId: logged.audit.diagnosticId },
1338
2316
  };
1339
2317
  }
2318
+ /** The overrun sentence for a finished run that spent past its base budget, as status details. */
2319
+ overrunDetails(run) {
2320
+ if (run.budgetBaseUsd === undefined)
2321
+ return {};
2322
+ const sentence = overrunSentence(run.costUsd, run.budgetBaseUsd, run.budgetLimitUsd ?? run.budgetUsd);
2323
+ return sentence ? { overrunSentence: sentence } : {};
2324
+ }
1340
2325
  /** The host-safe sentence for a run that ran out of budget. */
1341
- budgetSentence(run) {
2326
+ async budgetSentence(run) {
2327
+ const loaded = await this.options.store.budgetEvent?.(run.runId).catch(() => null);
2328
+ if (loaded)
2329
+ return hostBudgetSentence(loaded.event);
1342
2330
  return budgetSentence({
1343
2331
  runBudgetUsd: run.budgetUsd,
1344
2332
  agentBudgetUsd: run.agentBudgetUsd ?? run.budgetUsd,
2333
+ baseBudgetUsd: run.budgetBaseUsd,
2334
+ limitUsd: run.budgetLimitUsd,
1345
2335
  budgetGroupName: run.budgetGroupName,
1346
2336
  });
1347
2337
  }
@@ -1349,10 +2339,13 @@ export class ContainerExecutor {
1349
2339
  * A failure the proxy's budget refusal caused. The failure itself is still
1350
2340
  * logged under its diagnostic id, so the operator keeps the real reason.
1351
2341
  */
1352
- budgetFailure(error, options = {}) {
2342
+ async budgetFailure(runId, error, options = {}) {
1353
2343
  const logged = this.failure(error, options);
2344
+ const loaded = await this.options.store.budgetEvent?.(runId).catch(() => null);
1354
2345
  return {
1355
- error: "coding_budget_exhausted",
2346
+ error: loaded
2347
+ ? `coding_budget_exhausted: ${ownerBudgetSentence(loaded.event, loaded.agentName)}`
2348
+ : "coding_budget_exhausted",
1356
2349
  audit: { failureCategory: "budget", diagnosticId: logged.audit.diagnosticId },
1357
2350
  };
1358
2351
  }
@@ -1365,6 +2358,7 @@ export class ContainerExecutor {
1365
2358
  }
1366
2359
  }
1367
2360
  terminal(run, outcome, audit) {
2361
+ this.capacityRefusedAt.delete(run.runId);
1368
2362
  this.emit({
1369
2363
  stage: "terminal",
1370
2364
  runId: run.runId,
@@ -1449,6 +2443,10 @@ const CATEGORY_BY_PREFIX = [
1449
2443
  // Checked before the generic "vcs_" fallback: the collected diff touched a
1450
2444
  // path this agent may not edit, not a generic workspace/git failure.
1451
2445
  ["vcs_protected_path:", PROTECTED_PATH_CATEGORY],
2446
+ // A continuation's target-branch merge (git.ts): conflict markers left in a
2447
+ // conflicted file, or a target branch that could not be fetched or merged.
2448
+ [CONFLICT_MARKERS_ERROR_PREFIX, MERGE_CONFLICT_CATEGORY],
2449
+ [TARGET_MERGE_FAILED_ERROR, TARGET_MERGE_CATEGORY],
1452
2450
  // A continuation whose pull request was merged or closed: nothing pushed (coding/continuation-wording.ts).
1453
2451
  [CONTINUATION_CLOSED_ERROR, CONTINUATION_CLOSED_CATEGORY],
1454
2452
  // A provider whose worker images this deployment doesn't configure (codex or claude-code): configuration.
@@ -1507,6 +2505,53 @@ function failureCategory(error) {
1507
2505
  return "cancelled";
1508
2506
  return "executor";
1509
2507
  }
2508
+ /** A stored CodingRun result's summary (the agent-written description text), when it has one. */
2509
+ function storedSummary(result) {
2510
+ const summary = storedField(result, "summary");
2511
+ return typeof summary === "string" ? summary : undefined;
2512
+ }
2513
+ /** One field of a stored JSON object; undefined when it isn't an object. */
2514
+ function storedField(value, key) {
2515
+ if (!value || typeof value !== "object" || Array.isArray(value))
2516
+ return undefined;
2517
+ return value[key];
2518
+ }
2519
+ /** Stored test results the PR body can list (command + outcome); anything else is dropped. */
2520
+ function storedTests(value) {
2521
+ if (!Array.isArray(value))
2522
+ return [];
2523
+ return value.flatMap((test) => {
2524
+ const command = storedField(test, "command");
2525
+ const outcome = storedField(test, "outcome");
2526
+ return typeof command === "string" && (outcome === "passed" || outcome === "failed" || outcome === "skipped")
2527
+ ? [{ command, outcome }]
2528
+ : [];
2529
+ });
2530
+ }
2531
+ /**
2532
+ * Packages from several runs' reports, in order: one entry per ecosystem +
2533
+ * name + version, first appearance first, accepted advisories merged.
2534
+ */
2535
+ function mergePackages(lists) {
2536
+ const merged = new Map();
2537
+ for (const list of lists) {
2538
+ for (const pkg of list) {
2539
+ const key = `${pkg.ecosystem}\0${pkg.name}\0${pkg.version}`;
2540
+ const existing = merged.get(key);
2541
+ if (!existing) {
2542
+ merged.set(key, {
2543
+ ...pkg,
2544
+ ...(pkg.acceptedAdvisories ? { acceptedAdvisories: [...pkg.acceptedAdvisories] } : {}),
2545
+ });
2546
+ continue;
2547
+ }
2548
+ if (pkg.acceptedAdvisories?.length) {
2549
+ existing.acceptedAdvisories = [...new Set([...(existing.acceptedAdvisories ?? []), ...pkg.acceptedAdvisories])];
2550
+ }
2551
+ }
2552
+ }
2553
+ return [...merged.values()];
2554
+ }
1510
2555
  function stableJson(value) {
1511
2556
  if (Array.isArray(value))
1512
2557
  return `[${value.map(stableJson).join(",")}]`;