@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.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 (328) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
  3. package/dist/__test-utils__/approval-contract/types.js +24 -0
  4. package/dist/__test-utils__/approval-contract/types.js.map +1 -0
  5. package/dist/activities/call-agent-status.d.ts +19 -1
  6. package/dist/activities/call-agent-status.js +64 -5
  7. package/dist/activities/call-agent-status.js.map +1 -1
  8. package/dist/activities/call-llm.js +19 -53
  9. package/dist/activities/call-llm.js.map +1 -1
  10. package/dist/activities/classify-tool-approvals.d.ts +24 -0
  11. package/dist/activities/classify-tool-approvals.js +69 -17
  12. package/dist/activities/classify-tool-approvals.js.map +1 -1
  13. package/dist/activities/discover-mcp-server.d.ts +7 -0
  14. package/dist/activities/discover-mcp-server.js +11 -1
  15. package/dist/activities/discover-mcp-server.js.map +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
  17. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
  18. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
  19. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
  20. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
  21. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
  22. package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
  23. package/dist/activities/execute-cursor/approval-policy.js +8 -120
  24. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  25. package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
  26. package/dist/activities/execute-cursor/approval-state.js +233 -17
  27. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  28. package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
  29. package/dist/activities/execute-cursor/capture-flow.js +234 -0
  30. package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
  31. package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
  32. package/dist/activities/execute-cursor/cas-observations.js +184 -0
  33. package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
  34. package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
  35. package/dist/activities/execute-cursor/command-provenance.js +128 -0
  36. package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
  37. package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
  38. package/dist/activities/execute-cursor/exact-apply.js +204 -0
  39. package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
  40. package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
  41. package/dist/activities/execute-cursor/hook-script.js +310 -47
  42. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  43. package/dist/activities/execute-cursor/index.d.ts +8 -1
  44. package/dist/activities/execute-cursor/index.js +500 -64
  45. package/dist/activities/execute-cursor/index.js.map +1 -1
  46. package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
  47. package/dist/activities/execute-cursor/message-translator.js +836 -90
  48. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  49. package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
  50. package/dist/activities/execute-cursor/prompt-builder.js +74 -15
  51. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  52. package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
  53. package/dist/activities/execute-cursor/workspace-setup.js +212 -33
  54. package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
  55. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
  56. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
  57. package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
  58. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
  59. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
  60. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
  61. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
  62. package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
  63. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
  64. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
  65. package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
  66. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
  68. package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
  69. package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
  70. package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
  71. package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
  72. package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
  73. package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
  74. package/dist/activities/execute-deep-agent/hitl.js +5 -1
  75. package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/index.d.ts +2 -1
  77. package/dist/activities/execute-deep-agent/index.js +370 -56
  78. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
  80. package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
  81. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
  83. package/dist/activities/execute-deep-agent/setup.js +149 -92
  84. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
  86. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
  87. package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
  88. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
  89. package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
  90. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
  91. package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
  92. package/dist/activities/execute-deep-agent/status-builder.js +6 -2
  93. package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
  94. package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
  95. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  96. package/dist/activities/execute-deep-agent/streaming.js +3 -15
  97. package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
  98. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
  99. package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
  100. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  101. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
  102. package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
  103. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  104. package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
  105. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
  106. package/dist/claimcheck/payload-codec.js +9 -5
  107. package/dist/claimcheck/payload-codec.js.map +1 -1
  108. package/dist/client/stigmer-client.d.ts +2 -0
  109. package/dist/client/stigmer-client.js +2 -0
  110. package/dist/client/stigmer-client.js.map +1 -1
  111. package/dist/middleware/approval-gate.d.ts +85 -4
  112. package/dist/middleware/approval-gate.js +165 -38
  113. package/dist/middleware/approval-gate.js.map +1 -1
  114. package/dist/middleware/types.d.ts +2 -5
  115. package/dist/shared/activity-input.d.ts +43 -0
  116. package/dist/shared/activity-input.js +17 -0
  117. package/dist/shared/activity-input.js.map +1 -0
  118. package/dist/shared/approval-canonicalize.d.ts +19 -0
  119. package/dist/shared/approval-canonicalize.js +119 -0
  120. package/dist/shared/approval-canonicalize.js.map +1 -0
  121. package/dist/shared/approval-fingerprint.d.ts +106 -0
  122. package/dist/shared/approval-fingerprint.js +113 -0
  123. package/dist/shared/approval-fingerprint.js.map +1 -0
  124. package/dist/shared/approval-policy.d.ts +182 -12
  125. package/dist/shared/approval-policy.js +213 -27
  126. package/dist/shared/approval-policy.js.map +1 -1
  127. package/dist/shared/args-preview.d.ts +52 -0
  128. package/dist/shared/args-preview.js +93 -0
  129. package/dist/shared/args-preview.js.map +1 -0
  130. package/dist/shared/artifact-storage.d.ts +19 -1
  131. package/dist/shared/artifact-storage.js +48 -11
  132. package/dist/shared/artifact-storage.js.map +1 -1
  133. package/dist/shared/file-change.d.ts +44 -0
  134. package/dist/shared/file-change.js +57 -0
  135. package/dist/shared/file-change.js.map +1 -0
  136. package/dist/shared/file-tools.d.ts +107 -0
  137. package/dist/shared/file-tools.js +168 -0
  138. package/dist/shared/file-tools.js.map +1 -0
  139. package/dist/shared/filereview/capture.d.ts +202 -0
  140. package/dist/shared/filereview/capture.js +498 -0
  141. package/dist/shared/filereview/capture.js.map +1 -0
  142. package/dist/shared/filereview/cas-substrate.d.ts +190 -0
  143. package/dist/shared/filereview/cas-substrate.js +284 -0
  144. package/dist/shared/filereview/cas-substrate.js.map +1 -0
  145. package/dist/shared/filereview/digest.d.ts +40 -0
  146. package/dist/shared/filereview/digest.js +66 -0
  147. package/dist/shared/filereview/digest.js.map +1 -0
  148. package/dist/shared/filereview/events.d.ts +170 -0
  149. package/dist/shared/filereview/events.js +298 -0
  150. package/dist/shared/filereview/events.js.map +1 -0
  151. package/dist/shared/filereview/git-substrate.d.ts +175 -0
  152. package/dist/shared/filereview/git-substrate.js +439 -0
  153. package/dist/shared/filereview/git-substrate.js.map +1 -0
  154. package/dist/shared/filereview/index.d.ts +11 -0
  155. package/dist/shared/filereview/index.js +12 -0
  156. package/dist/shared/filereview/index.js.map +1 -0
  157. package/dist/shared/filereview/secret-paths.d.ts +63 -0
  158. package/dist/shared/filereview/secret-paths.js +105 -0
  159. package/dist/shared/filereview/secret-paths.js.map +1 -0
  160. package/dist/shared/fingerprint-secret.d.ts +26 -0
  161. package/dist/shared/fingerprint-secret.js +47 -0
  162. package/dist/shared/fingerprint-secret.js.map +1 -0
  163. package/dist/shared/model-client.d.ts +51 -0
  164. package/dist/shared/model-client.js +77 -0
  165. package/dist/shared/model-client.js.map +1 -0
  166. package/dist/shared/plan-artifact.js +0 -2
  167. package/dist/shared/plan-artifact.js.map +1 -1
  168. package/dist/shared/status-offload.d.ts +83 -9
  169. package/dist/shared/status-offload.js +399 -79
  170. package/dist/shared/status-offload.js.map +1 -1
  171. package/dist/shared/status.js +14 -1
  172. package/dist/shared/status.js.map +1 -1
  173. package/dist/shared/tool-kind.d.ts +19 -0
  174. package/dist/shared/tool-kind.js +13 -0
  175. package/dist/shared/tool-kind.js.map +1 -1
  176. package/dist/shared/tool-row.d.ts +88 -0
  177. package/dist/shared/tool-row.js +127 -0
  178. package/dist/shared/tool-row.js.map +1 -0
  179. package/dist/shared/workspace/platform-dir.d.ts +25 -0
  180. package/dist/shared/workspace/platform-dir.js +38 -2
  181. package/dist/shared/workspace/platform-dir.js.map +1 -1
  182. package/dist/workflows/call-agent-orchestrator.js +56 -7
  183. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  184. package/dist/workflows/connect-mcp-server.d.ts +50 -0
  185. package/dist/workflows/connect-mcp-server.js +136 -15
  186. package/dist/workflows/connect-mcp-server.js.map +1 -1
  187. package/dist/workflows/types.d.ts +8 -0
  188. package/package.json +2 -2
  189. package/src/__test-utils__/approval-contract/contract.ts +224 -0
  190. package/src/__test-utils__/approval-contract/types.ts +179 -0
  191. package/src/__test-utils__/fake-artifact-storage.ts +72 -0
  192. package/src/__tests__/approval-gateway-contract.test.ts +29 -0
  193. package/src/__tests__/claimcheck-codec.test.ts +16 -53
  194. package/src/__tests__/golden-e2e.test.ts +2 -0
  195. package/src/__tests__/runner-token-coordinator.test.ts +3 -3
  196. package/src/activities/__tests__/call-agent-status.test.ts +135 -0
  197. package/src/activities/__tests__/call-llm.test.ts +1 -1
  198. package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
  199. package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
  200. package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
  201. package/src/activities/call-agent-status.ts +74 -4
  202. package/src/activities/call-llm.ts +18 -63
  203. package/src/activities/classify-tool-approvals.ts +101 -19
  204. package/src/activities/discover-mcp-server.ts +29 -1
  205. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
  206. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
  207. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
  208. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
  209. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
  210. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
  211. package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
  212. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
  213. package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
  214. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
  215. package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
  216. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
  217. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
  218. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
  219. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
  220. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
  221. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
  222. package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
  223. package/src/activities/execute-cursor/approval-policy.ts +28 -159
  224. package/src/activities/execute-cursor/approval-state.ts +366 -18
  225. package/src/activities/execute-cursor/capture-flow.ts +323 -0
  226. package/src/activities/execute-cursor/cas-observations.ts +204 -0
  227. package/src/activities/execute-cursor/command-provenance.ts +168 -0
  228. package/src/activities/execute-cursor/exact-apply.ts +253 -0
  229. package/src/activities/execute-cursor/hook-script.ts +317 -51
  230. package/src/activities/execute-cursor/index.ts +575 -67
  231. package/src/activities/execute-cursor/message-translator.ts +963 -89
  232. package/src/activities/execute-cursor/prompt-builder.ts +80 -14
  233. package/src/activities/execute-cursor/workspace-setup.ts +257 -42
  234. package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
  235. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
  236. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
  237. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
  238. package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
  239. package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
  240. package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
  241. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
  242. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
  243. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
  244. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
  245. package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
  246. package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
  247. package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
  248. package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
  249. package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
  250. package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
  251. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
  252. package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
  253. package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
  254. package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
  255. package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
  256. package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
  257. package/src/activities/execute-deep-agent/hitl.ts +15 -1
  258. package/src/activities/execute-deep-agent/index.ts +434 -64
  259. package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
  260. package/src/activities/execute-deep-agent/setup.ts +223 -125
  261. package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
  262. package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
  263. package/src/activities/execute-deep-agent/status-builder.ts +19 -7
  264. package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
  265. package/src/activities/execute-deep-agent/streaming.ts +3 -13
  266. package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
  267. package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
  268. package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
  269. package/src/claimcheck/payload-codec.ts +8 -8
  270. package/src/client/stigmer-client.ts +9 -1
  271. package/src/middleware/__tests__/approval-gate.test.ts +488 -4
  272. package/src/middleware/approval-gate.ts +247 -38
  273. package/src/middleware/types.ts +5 -5
  274. package/src/shared/__tests__/activity-input.test.ts +78 -0
  275. package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
  276. package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
  277. package/src/shared/__tests__/approval-policy.test.ts +274 -40
  278. package/src/shared/__tests__/args-preview.test.ts +78 -0
  279. package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
  280. package/src/shared/__tests__/artifact-storage.test.ts +123 -11
  281. package/src/shared/__tests__/file-change.test.ts +85 -0
  282. package/src/shared/__tests__/file-tools.test.ts +90 -0
  283. package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
  284. package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
  285. package/src/shared/__tests__/model-client.test.ts +162 -0
  286. package/src/shared/__tests__/plan-artifact.test.ts +11 -26
  287. package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
  288. package/src/shared/__tests__/status-offload.test.ts +573 -16
  289. package/src/shared/__tests__/status.test.ts +4 -5
  290. package/src/shared/__tests__/tool-kind.test.ts +24 -1
  291. package/src/shared/__tests__/tool-row.test.ts +221 -0
  292. package/src/shared/activity-input.ts +57 -0
  293. package/src/shared/approval-canonicalize.ts +159 -0
  294. package/src/shared/approval-fingerprint.ts +148 -0
  295. package/src/shared/approval-policy.ts +303 -27
  296. package/src/shared/args-preview.ts +98 -0
  297. package/src/shared/artifact-storage.ts +62 -11
  298. package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
  299. package/src/shared/file-change.ts +64 -0
  300. package/src/shared/file-tools.ts +169 -0
  301. package/src/shared/filereview/__tests__/capture.test.ts +856 -0
  302. package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
  303. package/src/shared/filereview/__tests__/digest.test.ts +100 -0
  304. package/src/shared/filereview/__tests__/events.test.ts +245 -0
  305. package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
  306. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
  307. package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
  308. package/src/shared/filereview/capture.ts +727 -0
  309. package/src/shared/filereview/cas-substrate.ts +401 -0
  310. package/src/shared/filereview/digest.ts +83 -0
  311. package/src/shared/filereview/events.ts +449 -0
  312. package/src/shared/filereview/git-substrate.ts +555 -0
  313. package/src/shared/filereview/index.ts +60 -0
  314. package/src/shared/filereview/secret-paths.ts +121 -0
  315. package/src/shared/fingerprint-secret.ts +53 -0
  316. package/src/shared/model-client.ts +122 -0
  317. package/src/shared/plan-artifact.ts +0 -2
  318. package/src/shared/status-offload.ts +433 -77
  319. package/src/shared/status.ts +13 -0
  320. package/src/shared/tool-kind.ts +33 -0
  321. package/src/shared/tool-row.ts +135 -0
  322. package/src/shared/workspace/platform-dir.ts +41 -2
  323. package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
  324. package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
  325. package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
  326. package/src/workflows/call-agent-orchestrator.ts +53 -6
  327. package/src/workflows/connect-mcp-server.ts +179 -24
  328. package/src/workflows/types.ts +8 -0
@@ -2,25 +2,42 @@ import { describe, it, expect, vi } from "vitest";
2
2
  import { create, toBinary } from "@bufbuild/protobuf";
3
3
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
4
4
  import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
5
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
5
6
  import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
6
7
  import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
7
- import type { ArtifactStorage } from "../artifact-storage.js";
8
+ import { makeInMemoryArtifactStorage } from "../../__test-utils__/fake-artifact-storage.js";
9
+ import {
10
+ DiffCompleteness,
11
+ FileCaptureClass,
12
+ FileChangeKind,
13
+ FileChangeType,
14
+ FileChangeCaptureLevel,
15
+ FileReviewBlockReason,
16
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
8
17
  import {
9
18
  offloadOversizedToolOutputs,
19
+ offloadCandidateChangesToFit,
10
20
  enforceStatusSizeLimit,
11
21
  detectImagePayload,
12
22
  } from "../status-offload.js";
23
+ import {
24
+ appendFileReviewEvents,
25
+ buildCandidateCapturedEvent,
26
+ buildCapturedFileChange,
27
+ type ChangeSetContext,
28
+ } from "../filereview/events.js";
29
+ import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
13
30
 
14
31
  function makeFakeStorage() {
32
+ // Canonical double, with the metadata-tracking `uploads` shim these tests assert
33
+ // on and the serve-URL base they expect. `download` reads back what was uploaded.
34
+ const { storage, blobs } = makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" });
15
35
  const uploads: { key: string; size: number; contentType?: string }[] = [];
16
- const storage: ArtifactStorage = {
17
- upload: vi.fn(async (key: string, content: Buffer, contentType?: string) => {
18
- uploads.push({ key, size: content.length, contentType });
19
- return key;
20
- }),
21
- getDownloadUrl: vi.fn(async (key: string) => `https://artifacts.local/${key}`),
22
- exists: vi.fn(async () => true),
23
- };
36
+ storage.upload.mockImplementation(async (key: string, content: Buffer, contentType?: string) => {
37
+ uploads.push({ key, size: content.length, contentType });
38
+ blobs.set(key, Buffer.from(content));
39
+ return key;
40
+ });
24
41
  return { storage, uploads };
25
42
  }
26
43
 
@@ -72,6 +89,87 @@ describe("detectImagePayload", () => {
72
89
  expect(img?.mimeType).toBe("image/png");
73
90
  expect(img?.base64).toBe(BIG_BASE64_IMAGE);
74
91
  });
92
+
93
+ it("extracts the Cursor SDK MCP block shape ({ image: { data, mimeType } })", () => {
94
+ // The shape @cursor/sdk uses for MCP image content. When the result reaches
95
+ // the offloader as a pre-serialized string it bypasses canonicalizeImageResult,
96
+ // so detection must recognize this shape directly.
97
+ const result = JSON.stringify([
98
+ { text: { text: "Screen captured. App state: ready." } },
99
+ { image: { data: BIG_BASE64_IMAGE, mimeType: "image/png" } },
100
+ ]);
101
+ const img = detectImagePayload(result);
102
+ expect(img?.mimeType).toBe("image/png");
103
+ expect(img?.base64).toBe(BIG_BASE64_IMAGE);
104
+ });
105
+
106
+ it("extracts an image nested under a status/value/content envelope", () => {
107
+ // The full Cursor MCP result envelope, serialized to a string. The image is
108
+ // two levels deep — the recursive walk must still find it.
109
+ const result = JSON.stringify({
110
+ status: "success",
111
+ value: {
112
+ isError: false,
113
+ content: [
114
+ { text: { text: "accessibility tree…" } },
115
+ { image: { data: BIG_BASE64_IMAGE, mimeType: "image/jpeg" } },
116
+ ],
117
+ },
118
+ });
119
+ const img = detectImagePayload(result);
120
+ expect(img?.mimeType).toBe("image/jpeg");
121
+ expect(img?.base64).toBe(BIG_BASE64_IMAGE);
122
+ });
123
+
124
+ it("extracts a data URL embedded in a JSON field (not a content block)", () => {
125
+ const result = JSON.stringify({
126
+ accessibilityTree: "Window > Button(OK)",
127
+ screenshot: `data:image/png;base64,${BIG_BASE64_IMAGE}`,
128
+ });
129
+ const img = detectImagePayload(result);
130
+ expect(img?.mimeType).toBe("image/png");
131
+ expect(img?.base64).toBe(BIG_BASE64_IMAGE);
132
+ });
133
+
134
+ it("extracts a data URL embedded in surrounding prose", () => {
135
+ const img = detectImagePayload(`Here is the screenshot: data:image/gif;base64,${BIG_BASE64_IMAGE}`);
136
+ expect(img?.mimeType).toBe("image/gif");
137
+ expect(img?.base64).toBe(BIG_BASE64_IMAGE);
138
+ });
139
+
140
+ it("does NOT treat a bare base64 string with no image marker as an image", () => {
141
+ // A base64-encoded file or a long log must remain text — never a false image.
142
+ const result = JSON.stringify({ file: BIG_BASE64_IMAGE, encoding: "base64" });
143
+ expect(detectImagePayload(result)).toBeNull();
144
+ });
145
+
146
+ it("does NOT treat an image field that is a file path as an image", () => {
147
+ const result = JSON.stringify({ image: "assets/capture.png" });
148
+ expect(detectImagePayload(result)).toBeNull();
149
+ });
150
+
151
+ it("extracts the REAL cursor-harness get_app_state shape (value.content + Buffer-JSON, no mimeType)", () => {
152
+ // Captured verbatim from a production cursor-harness get_app_state result:
153
+ // the multimodal envelope arrives as a serialized string, blocks live under
154
+ // value.content, the image data is Node Buffer-JSON, and there is NO
155
+ // mimeType. The old top-level-only detector missed this (offloaded as text);
156
+ // this is the exact regression that made screenshots render as raw JSON.
157
+ const PNG_BYTES = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
158
+ const result = JSON.stringify({
159
+ status: "success",
160
+ value: {
161
+ content: [
162
+ { text: { text: "App=com.google.Chrome … 65 button Done." } },
163
+ { image: { data: { type: "Buffer", data: PNG_BYTES } } },
164
+ ],
165
+ isError: false,
166
+ },
167
+ });
168
+ const img = detectImagePayload(result);
169
+ expect(img).not.toBeNull();
170
+ expect(img?.mimeType).toBe("image/png");
171
+ expect(img?.base64).toBe(Buffer.from(PNG_BYTES).toString("base64"));
172
+ });
75
173
  });
76
174
 
77
175
  describe("offloadOversizedToolOutputs", () => {
@@ -93,13 +191,83 @@ describe("offloadOversizedToolOutputs", () => {
93
191
  expect(out.outputRef).toBeDefined();
94
192
  expect(out.outputRef?.isImage).toBe(true);
95
193
  expect(out.outputRef?.mimeType).toBe("image/png");
96
- expect(out.outputRef?.downloadUrl).toContain("artifacts/exec-1/toolcalls/tc-1.png");
194
+ expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-1.png");
97
195
  // result is collapsed to a short label, no longer the giant blob.
98
196
  expect(out.result.length).toBeLessThan(200);
99
197
  expect(uploads).toHaveLength(1);
100
198
  expect(uploads[0].contentType).toBe("image/png");
101
199
  });
102
200
 
201
+ it("offloads a Cursor-SDK-shape multimodal result as a renderable image (not text)", async () => {
202
+ // Reproduces the production symptom class: a get_app_state-style result
203
+ // (status line text + screenshot) delivered as a pre-serialized string in
204
+ // the @cursor/sdk MCP block shape. Before the recursive detector this fell
205
+ // through to a text offload (is_image=false → "view full output" instead of
206
+ // the picture); it must now offload as an image.
207
+ const { storage, uploads } = makeFakeStorage();
208
+ const result = JSON.stringify({
209
+ status: "success",
210
+ value: {
211
+ isError: false,
212
+ content: [
213
+ { text: { text: "Screen captured. App state: ready." } },
214
+ { image: { data: BIG_BASE64_IMAGE, mimeType: "image/png" } },
215
+ ],
216
+ },
217
+ });
218
+ const tc = create(ToolCallSchema, { id: "tc-app", name: "get_app_state", result });
219
+ const status = statusWithToolCall(tc);
220
+
221
+ await offloadOversizedToolOutputs(status, {
222
+ artifactStorage: storage,
223
+ executionId: "exec-1",
224
+ maxInlineBytes: 256,
225
+ });
226
+
227
+ const out = status.messages[0].toolCalls[0];
228
+ expect(out.outputRef?.isImage).toBe(true);
229
+ expect(out.outputRef?.mimeType).toBe("image/png");
230
+ expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-app.png");
231
+ expect(out.result).not.toContain(BIG_BASE64_IMAGE);
232
+ expect(uploads).toHaveLength(1);
233
+ expect(uploads[0].contentType).toBe("image/png");
234
+ });
235
+
236
+ it("offloads the REAL get_app_state shape (Buffer-JSON, no mimeType) as a renderable image", async () => {
237
+ // End-to-end guard for the production regression: a serialized multimodal
238
+ // envelope with the image as Buffer-JSON under value.content and no mimeType
239
+ // must offload as an image ref (is_image=true), not text.
240
+ const { storage, uploads } = makeFakeStorage();
241
+ const pngBytes = [137, 80, 78, 71, 13, 10, 26, 10, ...Array(4096).fill(65)];
242
+ const result = JSON.stringify({
243
+ status: "success",
244
+ value: {
245
+ content: [
246
+ { text: { text: "accessibility tree…" } },
247
+ { image: { data: { type: "Buffer", data: pngBytes } } },
248
+ ],
249
+ isError: false,
250
+ },
251
+ });
252
+ const tc = create(ToolCallSchema, { id: "tc-gas", name: "get_app_state", result });
253
+ const status = statusWithToolCall(tc);
254
+
255
+ await offloadOversizedToolOutputs(status, {
256
+ artifactStorage: storage,
257
+ executionId: "exec-1",
258
+ maxInlineBytes: 256,
259
+ });
260
+
261
+ const out = status.messages[0].toolCalls[0];
262
+ expect(out.outputRef?.isImage).toBe(true);
263
+ expect(out.outputRef?.mimeType).toBe("image/png");
264
+ expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-gas.png");
265
+ expect(uploads).toHaveLength(1);
266
+ expect(uploads[0].contentType).toBe("image/png");
267
+ // The decoded image bytes are what gets uploaded, not the Buffer-JSON array.
268
+ expect(out.result).not.toContain('"Buffer"');
269
+ });
270
+
103
271
  it("offloads oversized text with a preview head and text/plain upload", async () => {
104
272
  const { storage, uploads } = makeFakeStorage();
105
273
  const result = "LOG ".repeat(2000); // ~8 KB
@@ -141,7 +309,7 @@ describe("offloadOversizedToolOutputs", () => {
141
309
 
142
310
  const out = status.messages[0].toolCalls[0];
143
311
  expect(out.outputRef?.isImage).toBe(true);
144
- expect(out.outputRef?.downloadUrl).toContain("artifacts/exec-1/toolcalls/tc-img.png");
312
+ expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-img.png");
145
313
  expect(out.result).not.toContain(smallImage);
146
314
  expect(uploads).toHaveLength(1);
147
315
  expect(uploads[0].contentType).toBe("image/png");
@@ -201,11 +369,8 @@ describe("offloadOversizedToolOutputs", () => {
201
369
  });
202
370
 
203
371
  it("falls back to inline truncation when the upload fails (never throws)", async () => {
204
- const storage: ArtifactStorage = {
205
- upload: vi.fn(async () => { throw new Error("storage down"); }),
206
- getDownloadUrl: vi.fn(async (k: string) => k),
207
- exists: vi.fn(async () => false),
208
- };
372
+ const { storage } = makeInMemoryArtifactStorage();
373
+ storage.upload.mockImplementation(async () => { throw new Error("storage down"); });
209
374
  const result = "LOG ".repeat(2000);
210
375
  const tc = create(ToolCallSchema, { id: "tc-6", name: "Shell", result });
211
376
  const status = statusWithToolCall(tc);
@@ -226,6 +391,97 @@ describe("offloadOversizedToolOutputs", () => {
226
391
  });
227
392
  });
228
393
 
394
+ // ---------------------------------------------------------------------------
395
+ // Sub-agent coverage — the size-bounding walks must include
396
+ // sub_agent_executions[].messages, not just the parent transcript. A delegated
397
+ // read/grep/screenshot result is as unbounded as a top-level one; missing this
398
+ // location was a hole in the persist boundary's bounded-payload guarantee.
399
+ // ---------------------------------------------------------------------------
400
+
401
+ function statusWithSubAgentToolCall(tc: ToolCall): AgentExecutionStatus {
402
+ return create(AgentExecutionStatusSchema, {
403
+ subAgentExecutions: [
404
+ create(SubAgentExecutionSchema, {
405
+ id: "sa-1",
406
+ name: "researcher",
407
+ messages: [create(AgentMessageSchema, { toolCalls: [tc] })],
408
+ }),
409
+ ],
410
+ });
411
+ }
412
+
413
+ describe("sub-agent message size bounding", () => {
414
+ it("offloads an oversized sub-agent tool result exactly like a parent one", async () => {
415
+ const { storage, uploads } = makeFakeStorage();
416
+ const result = "LOG ".repeat(2000); // ~8 KB
417
+ const tc = create(ToolCallSchema, { id: "tc-sub", name: "Shell", result });
418
+ const status = statusWithSubAgentToolCall(tc);
419
+
420
+ await offloadOversizedToolOutputs(status, {
421
+ artifactStorage: storage,
422
+ executionId: "exec-1",
423
+ maxInlineBytes: 256,
424
+ });
425
+
426
+ const out = status.subAgentExecutions[0].messages[0].toolCalls[0];
427
+ expect(out.outputRef?.mimeType).toBe("text/plain");
428
+ expect(out.result).toContain("view full output");
429
+ expect(uploads).toHaveLength(1);
430
+ });
431
+
432
+ it("offloads a sub-agent image result into a renderable ref", async () => {
433
+ const { storage, uploads } = makeFakeStorage();
434
+ const result = JSON.stringify([
435
+ { type: "image", data: BIG_BASE64_IMAGE, mimeType: "image/png" },
436
+ ]);
437
+ const tc = create(ToolCallSchema, { id: "tc-sub-img", name: "screenshot", result });
438
+ const status = statusWithSubAgentToolCall(tc);
439
+
440
+ await offloadOversizedToolOutputs(status, {
441
+ artifactStorage: storage,
442
+ executionId: "exec-1",
443
+ maxInlineBytes: 256,
444
+ });
445
+
446
+ const out = status.subAgentExecutions[0].messages[0].toolCalls[0];
447
+ expect(out.outputRef?.isImage).toBe(true);
448
+ expect(out.result).not.toContain(BIG_BASE64_IMAGE);
449
+ expect(uploads).toHaveLength(1);
450
+ });
451
+
452
+ it("enforceStatusSizeLimit elides oversized sub-agent tool results", () => {
453
+ const big = "Z".repeat(50_000);
454
+ const tc = create(ToolCallSchema, { id: "tc-sub-big", name: "Shell", result: big });
455
+ const status = statusWithSubAgentToolCall(tc);
456
+ expect(encodedSize(status)).toBeGreaterThan(40_000);
457
+
458
+ const elided = enforceStatusSizeLimit(status, 4_000);
459
+
460
+ expect(elided).toBe(true);
461
+ expect(encodedSize(status)).toBeLessThanOrEqual(4_000);
462
+ expect(status.subAgentExecutions[0].messages[0].toolCalls[0].result)
463
+ .not.toBe(big);
464
+ });
465
+
466
+ it("enforceStatusSizeLimit last-resort elides oversized sub-agent message content", () => {
467
+ const status = create(AgentExecutionStatusSchema, {
468
+ subAgentExecutions: [
469
+ create(SubAgentExecutionSchema, {
470
+ id: "sa-2",
471
+ messages: [create(AgentMessageSchema, { content: "W".repeat(50_000) })],
472
+ }),
473
+ ],
474
+ });
475
+ expect(encodedSize(status)).toBeGreaterThan(40_000);
476
+
477
+ const elided = enforceStatusSizeLimit(status, 8_000);
478
+
479
+ expect(elided).toBe(true);
480
+ expect(encodedSize(status)).toBeLessThanOrEqual(8_000);
481
+ expect(status.subAgentExecutions[0].messages[0].content).toContain("elided");
482
+ });
483
+ });
484
+
229
485
  describe("enforceStatusSizeLimit", () => {
230
486
  it("returns false and changes nothing when under the limit", () => {
231
487
  const tc = create(ToolCallSchema, { id: "tc", name: "Read", result: "tiny" });
@@ -253,4 +509,305 @@ describe("enforceStatusSizeLimit", () => {
253
509
  expect(elided).toBe(true);
254
510
  expect(encodedSize(status)).toBeLessThanOrEqual(4_000);
255
511
  });
512
+
513
+ it("marks a size-elided file-review change SIZE_ELIDED and PARTIAL_BLOCKED (doc 15)", () => {
514
+ const big = "Z".repeat(50_000);
515
+ const ctx: ChangeSetContext = {
516
+ changeSetId: "exec-1:0",
517
+ turnId: "turn-0",
518
+ harnessId: "deep-agent",
519
+ timestamp: "2026-07-01T00:00:00Z",
520
+ };
521
+ const change = buildCapturedFileChange({
522
+ id: "fc-big",
523
+ pathBefore: "src/big.ts",
524
+ pathAfter: "src/big.ts",
525
+ kind: FileChangeKind.MODIFY,
526
+ captureClass: FileCaptureClass.GIT_TRACKED,
527
+ before: big,
528
+ after: big,
529
+ });
530
+ // Reviewable at capture time: no reason, complete.
531
+ expect(change.blockedReason).toBe(FileReviewBlockReason.UNSPECIFIED);
532
+ expect(change.diffComplete).toBe(true);
533
+
534
+ const status = create(AgentExecutionStatusSchema, {});
535
+ appendFileReviewEvents(status, "exec-1", [
536
+ buildCandidateCapturedEvent(ctx, undefined, [change]),
537
+ ]);
538
+ expect(encodedSize(status)).toBeGreaterThan(90_000);
539
+
540
+ const elided = enforceStatusSizeLimit(status, 4_000);
541
+ expect(elided).toBe(true);
542
+
543
+ const ev = status.fileReviewEventStream?.events.find(
544
+ (e) => e.payload.case === "candidateCaptured",
545
+ );
546
+ expect(ev?.payload.case).toBe("candidateCaptured");
547
+ if (ev?.payload.case === "candidateCaptured") {
548
+ const dropped = ev.payload.value.changes[0];
549
+ // Bodies dropped, marked incomplete, and the honest cause recorded.
550
+ expect(dropped.before).toBeUndefined();
551
+ expect(dropped.after).toBeUndefined();
552
+ expect(dropped.diffComplete).toBe(false);
553
+ expect(dropped.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
554
+ expect(ev.payload.value.diffCompleteness).toBe(DiffCompleteness.PARTIAL_BLOCKED);
555
+ }
556
+ });
557
+
558
+ it("downgrades a BINARY_SUMMARY_ONLY set to PARTIAL_BLOCKED when size-elision drops a text body", () => {
559
+ // A set whose only blocker is a binary file (BINARY_SUMMARY_ONLY) plus a
560
+ // large-but-reviewable text file. When the text body is elided it becomes a
561
+ // non-binary incompleteness, so the shared re-derivation must downgrade the
562
+ // whole set to PARTIAL_BLOCKED (it is no longer binary-only).
563
+ const big = "Z".repeat(50_000);
564
+ const ctx: ChangeSetContext = {
565
+ changeSetId: "exec-3:0",
566
+ turnId: "turn-0",
567
+ harnessId: "deep-agent",
568
+ timestamp: "2026-07-01T00:00:00Z",
569
+ };
570
+ const binary = buildCapturedFileChange({
571
+ id: "fc-bin",
572
+ pathBefore: "",
573
+ pathAfter: "assets/logo.png",
574
+ kind: FileChangeKind.ADD,
575
+ captureClass: FileCaptureClass.GIT_TRACKED,
576
+ after: { kind: "binary", sha256: "sha-bin" },
577
+ diffComplete: false,
578
+ });
579
+ const bigText = buildCapturedFileChange({
580
+ id: "fc-text",
581
+ pathBefore: "src/big.ts",
582
+ pathAfter: "src/big.ts",
583
+ kind: FileChangeKind.MODIFY,
584
+ captureClass: FileCaptureClass.GIT_TRACKED,
585
+ before: big,
586
+ after: big,
587
+ });
588
+
589
+ const status = create(AgentExecutionStatusSchema, {});
590
+ appendFileReviewEvents(status, "exec-3", [
591
+ buildCandidateCapturedEvent(ctx, undefined, [binary, bigText]),
592
+ ]);
593
+ // Before elision: binary is the only blocker.
594
+ const before = status.fileReviewEventStream?.events.find(
595
+ (e) => e.payload.case === "candidateCaptured",
596
+ );
597
+ if (before?.payload.case === "candidateCaptured") {
598
+ expect(before.payload.value.diffCompleteness).toBe(DiffCompleteness.BINARY_SUMMARY_ONLY);
599
+ }
600
+
601
+ expect(enforceStatusSizeLimit(status, 4_000)).toBe(true);
602
+
603
+ const after = status.fileReviewEventStream?.events.find(
604
+ (e) => e.payload.case === "candidateCaptured",
605
+ );
606
+ if (after?.payload.case === "candidateCaptured") {
607
+ const cs = after.payload.value;
608
+ const text = cs.changes.find((c) => c.id === "fc-text");
609
+ const bin = cs.changes.find((c) => c.id === "fc-bin");
610
+ // The text body is dropped (now a non-binary incompleteness); the binary
611
+ // side is body-less already, so it is untouched and keeps is_binary.
612
+ expect(text?.diffComplete).toBe(false);
613
+ expect(text?.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
614
+ expect(bin?.after?.isBinary).toBe(true);
615
+ expect(cs.diffCompleteness).toBe(DiffCompleteness.PARTIAL_BLOCKED);
616
+ }
617
+ });
618
+
619
+ it("does not overwrite a more specific reason (SECRET_WITHHELD survives size elision)", () => {
620
+ // A secret entry is content-less, so it can't be size-elided; but guard the
621
+ // precedence explicitly: if a reason is already set, the size backstop keeps it.
622
+ const big = "Z".repeat(50_000);
623
+ const ctx: ChangeSetContext = {
624
+ changeSetId: "exec-2:0",
625
+ turnId: "turn-0",
626
+ harnessId: "deep-agent",
627
+ timestamp: "2026-07-01T00:00:00Z",
628
+ };
629
+ const secretButLarge = buildCapturedFileChange({
630
+ id: "fc-x",
631
+ pathBefore: "big.env",
632
+ pathAfter: "big.env",
633
+ kind: FileChangeKind.MODIFY,
634
+ captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
635
+ before: big,
636
+ after: big,
637
+ diffComplete: false,
638
+ blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
639
+ });
640
+ const status = create(AgentExecutionStatusSchema, {});
641
+ appendFileReviewEvents(status, "exec-2", [
642
+ buildCandidateCapturedEvent(ctx, undefined, [secretButLarge]),
643
+ ]);
644
+ enforceStatusSizeLimit(status, 4_000);
645
+ const ev = status.fileReviewEventStream?.events.find(
646
+ (e) => e.payload.case === "candidateCaptured",
647
+ );
648
+ if (ev?.payload.case === "candidateCaptured") {
649
+ expect(ev.payload.value.changes[0].blockedReason).toBe(
650
+ FileReviewBlockReason.SECRET_WITHHELD,
651
+ );
652
+ }
653
+ });
256
654
  });
655
+
656
+ describe("offloadCandidateChangesToFit", () => {
657
+ function statusWithCandidate(
658
+ changes: CapturedFileChange[],
659
+ executionId: string,
660
+ ): AgentExecutionStatus {
661
+ const ctx: ChangeSetContext = {
662
+ changeSetId: `${executionId}:0`,
663
+ turnId: "turn-0",
664
+ harnessId: "deep-agent",
665
+ timestamp: "2026-07-01T00:00:00Z",
666
+ };
667
+ const status = create(AgentExecutionStatusSchema, {});
668
+ appendFileReviewEvents(status, executionId, [
669
+ buildCandidateCapturedEvent(ctx, undefined, changes),
670
+ ]);
671
+ return status;
672
+ }
673
+
674
+ function candidateOf(status: AgentExecutionStatus) {
675
+ const ev = status.fileReviewEventStream?.events.find(
676
+ (e) => e.payload.case === "candidateCaptured",
677
+ );
678
+ if (ev?.payload.case !== "candidateCaptured") throw new Error("no candidate event");
679
+ return ev.payload.value;
680
+ }
681
+
682
+ function textChange(id: string, path: string, before: string, after: string): CapturedFileChange {
683
+ return buildCapturedFileChange({
684
+ id,
685
+ pathBefore: before === "" ? "" : path,
686
+ pathAfter: after === "" ? "" : path,
687
+ kind: FileChangeKind.MODIFY,
688
+ captureClass: FileCaptureClass.GIT_TRACKED,
689
+ ...(before !== "" ? { before } : {}),
690
+ ...(after !== "" ? { after } : {}),
691
+ });
692
+ }
693
+
694
+ it("offloads oversized captured bodies to refs so they stay reviewable (no SIZE_ELIDED)", async () => {
695
+ const { storage, uploads } = makeFakeStorage();
696
+ // Two mid-sized files, each under the 128 KiB per-file cap but summing well
697
+ // past the injected soft cap — the exact case the per-item pass misses. The
698
+ // cap sits above the ~4 KB-per-side preview floor a ref keeps, so all four
699
+ // oversized sides can be offloaded and the status still lands under it.
700
+ const a = textChange("exec-fit:0:a.ts", "a.ts", "A".repeat(30_000), "A".repeat(50_000));
701
+ const b = textChange("exec-fit:0:b.ts", "b.ts", "B".repeat(40_000), "B".repeat(60_000));
702
+ const status = statusWithCandidate([a, b], "exec-fit");
703
+ expect(candidateOf(status).diffCompleteness).toBe(DiffCompleteness.COMPLETE);
704
+ expect(encodedSize(status)).toBeGreaterThan(150_000);
705
+
706
+ const offloaded = await offloadCandidateChangesToFit(
707
+ status,
708
+ { artifactStorage: storage, executionId: "exec-fit" },
709
+ 20_000,
710
+ );
711
+ expect(offloaded).toBe(true);
712
+ expect(encodedSize(status)).toBeLessThanOrEqual(20_000);
713
+
714
+ const cs = candidateOf(status);
715
+ for (const c of cs.changes) {
716
+ // Every remaining side is a retrievable ref (never dropped, never left large-inline).
717
+ if (c.before) expect(c.before.body.case).toBe("ref");
718
+ if (c.after) expect(c.after.body.case).toBe("ref");
719
+ // Reviewability preserved: complete, no block reason.
720
+ expect(c.diffComplete).toBe(true);
721
+ expect(c.blockedReason).toBe(FileReviewBlockReason.UNSPECIFIED);
722
+ }
723
+ // The set stays COMPLETE (approvable), never PARTIAL_BLOCKED.
724
+ expect(cs.diffCompleteness).toBe(DiffCompleteness.COMPLETE);
725
+ // Keys follow the canonical maybeOffloadCandidateChanges convention.
726
+ expect(uploads.some((u) => u.key === "artifacts/exec-fit/filereview/exec-fit:0:a.ts.after.txt")).toBe(true);
727
+ });
728
+
729
+ it("offloads biggest-first and stops as soon as the status fits", async () => {
730
+ const { storage, uploads } = makeFakeStorage();
731
+ const big = textChange("exec-bf:0:big.ts", "big.ts", "", "B".repeat(60_000));
732
+ const small = textChange("exec-bf:0:small.ts", "small.ts", "", "s".repeat(2_000));
733
+ const status = statusWithCandidate([small, big], "exec-bf");
734
+ // Cap chosen so offloading only the big side gets under it.
735
+ const cap = encodedSize(status) - 40_000;
736
+
737
+ await offloadCandidateChangesToFit(
738
+ status,
739
+ { artifactStorage: storage, executionId: "exec-bf" },
740
+ cap,
741
+ );
742
+
743
+ const cs = candidateOf(status);
744
+ const bigC = cs.changes.find((c) => c.id === "exec-bf:0:big.ts");
745
+ const smallC = cs.changes.find((c) => c.id === "exec-bf:0:small.ts");
746
+ // The big body was offloaded; the small one was never touched (still inline).
747
+ expect(bigC?.after?.body.case).toBe("ref");
748
+ expect(smallC?.after?.body.case).toBe("inline");
749
+ expect(uploads).toHaveLength(1);
750
+ expect(uploads[0].key).toBe("artifacts/exec-bf/filereview/exec-bf:0:big.ts.after.txt");
751
+ });
752
+
753
+ it("is idempotent: an already-offloaded ref side is skipped, never re-uploaded", async () => {
754
+ const { storage, uploads } = makeFakeStorage();
755
+ const c = textChange("exec-idem:0:x.ts", "x.ts", "", "X".repeat(40_000));
756
+ const status = statusWithCandidate([c], "exec-idem");
757
+ const ctx = { artifactStorage: storage, executionId: "exec-idem" };
758
+
759
+ // A tiny cap the ref can never satisfy forces full iteration on both passes.
760
+ const first = await offloadCandidateChangesToFit(status, ctx, 100);
761
+ expect(first).toBe(true);
762
+ expect(uploads).toHaveLength(1);
763
+ expect(candidateOf(status).changes[0].after?.body.case).toBe("ref");
764
+
765
+ // Second pass over the same status: the side is a ref, so it is not collected
766
+ // and nothing is re-uploaded.
767
+ const second = await offloadCandidateChangesToFit(status, ctx, 100);
768
+ expect(second).toBe(false);
769
+ expect(uploads).toHaveLength(1);
770
+ });
771
+
772
+ it("falls through to the SIZE_ELIDED backstop when the upload fails", async () => {
773
+ const { storage } = makeFakeStorage();
774
+ storage.upload.mockRejectedValue(new Error("storage down"));
775
+ vi.spyOn(console, "warn").mockImplementation(() => {});
776
+ const c = textChange("exec-fail:0:y.ts", "y.ts", "", "Y".repeat(50_000));
777
+ const status = statusWithCandidate([c], "exec-fail");
778
+
779
+ const offloaded = await offloadCandidateChangesToFit(
780
+ status,
781
+ { artifactStorage: storage, executionId: "exec-fail" },
782
+ 4_000,
783
+ );
784
+ // Nothing offloaded; the body is left inline for the backstop, not dropped here.
785
+ expect(offloaded).toBe(false);
786
+ expect(candidateOf(status).changes[0].after?.body.case).toBe("inline");
787
+
788
+ // The storage-less backstop then drops it and records the honest cause.
789
+ expect(enforceStatusSizeLimit(status, 4_000)).toBe(true);
790
+ const dropped = candidateOf(status).changes[0];
791
+ expect(dropped.after).toBeUndefined();
792
+ expect(dropped.diffComplete).toBe(false);
793
+ expect(dropped.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
794
+ });
795
+
796
+ it("only offloads file-review bodies, never tool-call content", async () => {
797
+ const { storage, uploads } = makeFakeStorage();
798
+ const tc = create(ToolCallSchema, { id: "tc-1", name: "Read", result: "R".repeat(50_000) });
799
+ const status = statusWithToolCall(tc);
800
+ expect(encodedSize(status)).toBeGreaterThan(40_000);
801
+
802
+ const offloaded = await offloadCandidateChangesToFit(
803
+ status,
804
+ { artifactStorage: storage, executionId: "exec-tc" },
805
+ 4_000,
806
+ );
807
+ expect(offloaded).toBe(false);
808
+ expect(uploads).toHaveLength(0);
809
+ // The tool-call result is untouched (that is offloadOversizedToolOutputs' job).
810
+ expect(status.messages[0].toolCalls[0].result).toHaveLength(50_000);
811
+ });
812
+ });
813
+
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, vi } from "vitest";
2
+ import { makeInMemoryArtifactStorage } from "../../__test-utils__/fake-artifact-storage.js";
2
3
  import { ConnectError, Code } from "@connectrpc/connect";
3
4
  import { create } from "@bufbuild/protobuf";
4
5
  import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
@@ -144,13 +145,11 @@ describe("persistStatus", () => {
144
145
 
145
146
  it("offloads oversized tool outputs before persisting when given an offload context", async () => {
146
147
  const uploaded: string[] = [];
148
+ const { storage: offloadStorage } = makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" });
149
+ offloadStorage.upload.mockImplementation(async (key: string) => { uploaded.push(key); return key; });
147
150
  const offload = {
148
151
  executionId: "exec-off",
149
- artifactStorage: {
150
- upload: vi.fn(async (key: string) => { uploaded.push(key); return key; }),
151
- getDownloadUrl: vi.fn(async (key: string) => `https://artifacts.local/${key}`),
152
- exists: vi.fn(async () => true),
153
- },
152
+ artifactStorage: offloadStorage,
154
153
  maxInlineBytes: 256,
155
154
  };
156
155
  const mockClient = {