@stigmer/runner 3.14.0 → 3.15.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 (331) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/execution-record-fixture.d.ts +47 -0
  3. package/dist/__test-utils__/execution-record-fixture.js +68 -0
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -0
  5. package/dist/__test-utils__/harness-contract/types.d.ts +166 -0
  6. package/dist/__test-utils__/harness-contract/types.js +49 -0
  7. package/dist/__test-utils__/harness-contract/types.js.map +1 -0
  8. package/dist/__test-utils__/hermetic-activity.d.ts +268 -0
  9. package/dist/__test-utils__/hermetic-activity.js +385 -0
  10. package/dist/__test-utils__/hermetic-activity.js.map +1 -0
  11. package/dist/__test-utils__/mock-client.d.ts +13 -0
  12. package/dist/__test-utils__/mock-client.js +45 -0
  13. package/dist/__test-utils__/mock-client.js.map +1 -0
  14. package/dist/__test-utils__/mock-workspace.d.ts +15 -0
  15. package/dist/__test-utils__/mock-workspace.js +21 -0
  16. package/dist/__test-utils__/mock-workspace.js.map +1 -0
  17. package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
  18. package/dist/__test-utils__/model-registry-fixture.js +66 -0
  19. package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
  20. package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
  21. package/dist/__test-utils__/turn-input-fixture.js +118 -0
  22. package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
  23. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
  24. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
  25. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
  26. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
  27. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
  28. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  29. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
  30. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  31. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +148 -0
  32. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +257 -0
  33. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
  34. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +183 -0
  35. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +262 -0
  36. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
  37. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +138 -0
  38. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +191 -0
  39. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
  40. package/dist/activities/execute-cursor/adapter.d.ts +55 -0
  41. package/dist/activities/execute-cursor/adapter.js +119 -0
  42. package/dist/activities/execute-cursor/adapter.js.map +1 -0
  43. package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
  44. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  45. package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
  46. package/dist/activities/execute-cursor/capture-flow.js +17 -26
  47. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  48. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
  49. package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
  50. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
  51. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
  52. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
  53. package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
  54. package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
  55. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  56. package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
  57. package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
  58. package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
  59. package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
  60. package/dist/activities/execute-cursor/message-translator.js +76 -27
  61. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  62. package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
  63. package/dist/activities/execute-cursor/prompt-builder.js +171 -0
  64. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  65. package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
  66. package/dist/activities/execute-cursor/rejection-capture.js +5 -7
  67. package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
  68. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
  69. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  70. package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
  71. package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
  72. package/dist/activities/execute-cursor/turn-settle.js +507 -0
  73. package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
  74. package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
  75. package/dist/activities/execute-cursor/turn-setup.js +496 -0
  76. package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
  77. package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
  78. package/dist/activities/execute-cursor/turn-stream.js +70 -135
  79. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  80. package/dist/activities/execute-cursor/turn.d.ts +43 -0
  81. package/dist/activities/execute-cursor/turn.js +222 -0
  82. package/dist/activities/execute-cursor/turn.js.map +1 -0
  83. package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
  84. package/dist/activities/execute-cursor/usage-pricing.js +50 -0
  85. package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
  86. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
  87. package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
  88. package/dist/activities/execute-deep-agent/environment.js +1 -1
  89. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/setup.js +3 -2
  91. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  92. package/dist/activities/hydrate-workflow-execution.js +1 -1
  93. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  94. package/dist/config.d.ts +15 -0
  95. package/dist/config.js.map +1 -1
  96. package/dist/harness/capabilities.d.ts +87 -0
  97. package/dist/harness/capabilities.js +37 -0
  98. package/dist/harness/capabilities.js.map +1 -0
  99. package/dist/harness/persist-chokepoint.d.ts +74 -0
  100. package/dist/harness/persist-chokepoint.js +88 -0
  101. package/dist/harness/persist-chokepoint.js.map +1 -0
  102. package/dist/harness/registry.d.ts +106 -0
  103. package/dist/harness/registry.js +168 -0
  104. package/dist/harness/registry.js.map +1 -0
  105. package/dist/harness/run-turn.d.ts +53 -0
  106. package/dist/harness/run-turn.js +528 -0
  107. package/dist/harness/run-turn.js.map +1 -0
  108. package/dist/harness/stop-controller.d.ts +50 -0
  109. package/dist/harness/stop-controller.js +62 -0
  110. package/dist/harness/stop-controller.js.map +1 -0
  111. package/dist/harness/terminal-table.d.ts +141 -0
  112. package/dist/harness/terminal-table.js +239 -0
  113. package/dist/harness/terminal-table.js.map +1 -0
  114. package/dist/harness/turn-context.d.ts +523 -0
  115. package/dist/harness/turn-context.js +739 -0
  116. package/dist/harness/turn-context.js.map +1 -0
  117. package/dist/harness/types.d.ts +448 -0
  118. package/dist/harness/types.js +64 -0
  119. package/dist/harness/types.js.map +1 -0
  120. package/dist/harness/usage-accumulator.d.ts +68 -0
  121. package/dist/harness/usage-accumulator.js +101 -0
  122. package/dist/harness/usage-accumulator.js.map +1 -0
  123. package/dist/harness-adapters.d.ts +27 -0
  124. package/dist/harness-adapters.js +28 -0
  125. package/dist/harness-adapters.js.map +1 -0
  126. package/dist/main.js.map +1 -1
  127. package/dist/runner-manager.js +64 -48
  128. package/dist/runner-manager.js.map +1 -1
  129. package/dist/runner.d.ts +5 -4
  130. package/dist/runner.js +65 -64
  131. package/dist/runner.js.map +1 -1
  132. package/dist/shared/approval-policy.d.ts +1 -1
  133. package/dist/shared/approval-policy.js.map +1 -1
  134. package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
  135. package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
  136. package/dist/shared/attachment-resolver.js.map +1 -0
  137. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
  138. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
  139. package/dist/shared/blueprint-resolver.js.map +1 -0
  140. package/dist/shared/channel-attachment.d.ts +1 -1
  141. package/dist/shared/channel-attachment.js.map +1 -1
  142. package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
  143. package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
  144. package/dist/shared/cost-guard.js.map +1 -0
  145. package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
  146. package/dist/shared/env-resolver.js.map +1 -0
  147. package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
  148. package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
  149. package/dist/shared/exact-apply.js.map +1 -0
  150. package/dist/shared/execution-context.d.ts +35 -0
  151. package/dist/shared/execution-context.js +38 -0
  152. package/dist/shared/execution-context.js.map +1 -0
  153. package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
  154. package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
  155. package/dist/shared/extract-structured-output.js.map +1 -0
  156. package/dist/shared/filereview/capture.d.ts +1 -1
  157. package/dist/shared/filereview/capture.js +1 -1
  158. package/dist/shared/heartbeat.d.ts +13 -11
  159. package/dist/shared/heartbeat.js +14 -29
  160. package/dist/shared/heartbeat.js.map +1 -1
  161. package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
  162. package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
  163. package/dist/shared/persist-decision.js.map +1 -0
  164. package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
  165. package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
  166. package/dist/shared/skill-resolver.js.map +1 -0
  167. package/dist/shared/subagent-rows.d.ts +21 -0
  168. package/dist/shared/subagent-rows.js +34 -0
  169. package/dist/shared/subagent-rows.js.map +1 -0
  170. package/dist/shared/worker-shutdown.d.ts +43 -16
  171. package/dist/shared/worker-shutdown.js +28 -13
  172. package/dist/shared/worker-shutdown.js.map +1 -1
  173. package/dist/shared/workspace/platform-dir.d.ts +1 -1
  174. package/dist/shared/workspace/platform-dir.js +1 -1
  175. package/dist/shared/workspace/session-provision.d.ts +54 -0
  176. package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
  177. package/dist/shared/workspace/session-provision.js.map +1 -0
  178. package/package.json +4 -4
  179. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +325 -0
  180. package/src/__test-utils__/config-fixture.ts +63 -0
  181. package/src/__test-utils__/execution-record-fixture.ts +104 -0
  182. package/src/__test-utils__/harness-boot-order-child.ts +58 -0
  183. package/src/__test-utils__/harness-contract/contract.ts +581 -0
  184. package/src/__test-utils__/harness-contract/recording-sink.ts +107 -0
  185. package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
  186. package/src/__test-utils__/harness-contract/scripted-adapter.ts +349 -0
  187. package/src/__test-utils__/harness-contract/types.ts +176 -0
  188. package/src/__test-utils__/hermetic-activity.ts +502 -0
  189. package/src/__test-utils__/model-registry-fixture.ts +72 -0
  190. package/src/__test-utils__/module-specifiers.ts +82 -0
  191. package/src/__test-utils__/proto-helpers.ts +25 -0
  192. package/src/__test-utils__/turn-input-fixture.ts +155 -0
  193. package/src/__tests__/harness-boot-order.test.ts +77 -0
  194. package/src/__tests__/harness-contract.test.ts +27 -0
  195. package/src/__tests__/runner-manager.test.ts +18 -0
  196. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
  197. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
  198. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
  199. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
  200. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +342 -0
  201. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +368 -0
  202. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +254 -0
  203. package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
  204. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
  205. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
  206. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
  207. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
  208. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
  209. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
  210. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
  211. package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
  212. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
  213. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
  214. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
  215. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +40 -0
  216. package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
  217. package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
  218. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
  219. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
  220. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  221. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
  222. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
  223. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
  224. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
  225. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
  226. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
  227. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +42 -0
  228. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
  229. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +205 -0
  230. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
  231. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
  232. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
  233. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
  234. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
  235. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
  236. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
  237. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
  238. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
  239. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
  240. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
  241. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
  242. package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
  243. package/src/activities/execute-cursor/adapter.ts +138 -0
  244. package/src/activities/execute-cursor/approval-state.ts +2 -2
  245. package/src/activities/execute-cursor/capture-flow.ts +18 -41
  246. package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
  247. package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
  248. package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
  249. package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
  250. package/src/activities/execute-cursor/message-translator.ts +90 -45
  251. package/src/activities/execute-cursor/prompt-builder.ts +327 -12
  252. package/src/activities/execute-cursor/rejection-capture.ts +5 -12
  253. package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
  254. package/src/activities/execute-cursor/turn-boundary.ts +1 -1
  255. package/src/activities/execute-cursor/turn-settle.ts +601 -0
  256. package/src/activities/execute-cursor/turn-setup.ts +645 -0
  257. package/src/activities/execute-cursor/turn-stream.ts +84 -210
  258. package/src/activities/execute-cursor/turn.ts +240 -0
  259. package/src/activities/execute-cursor/usage-pricing.ts +67 -0
  260. package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
  261. package/src/activities/execute-deep-agent/environment.ts +1 -1
  262. package/src/activities/execute-deep-agent/setup.ts +3 -2
  263. package/src/activities/hydrate-workflow-execution.ts +1 -1
  264. package/src/config.ts +13 -0
  265. package/src/harness/__tests__/import-direction.test.ts +168 -0
  266. package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
  267. package/src/harness/__tests__/registry.test.ts +168 -0
  268. package/src/harness/__tests__/run-turn.test.ts +165 -0
  269. package/src/harness/__tests__/stop-controller.test.ts +49 -0
  270. package/src/harness/__tests__/turn-context.test.ts +178 -0
  271. package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
  272. package/src/harness/capabilities.ts +92 -0
  273. package/src/harness/persist-chokepoint.ts +112 -0
  274. package/src/harness/registry.ts +197 -0
  275. package/src/harness/run-turn.ts +618 -0
  276. package/src/harness/stop-controller.ts +83 -0
  277. package/src/harness/terminal-table.ts +276 -0
  278. package/src/harness/turn-context.ts +1028 -0
  279. package/src/harness/types.ts +473 -0
  280. package/src/harness/usage-accumulator.ts +122 -0
  281. package/src/harness-adapters.ts +30 -0
  282. package/src/main.ts +1 -1
  283. package/src/runner-manager.ts +68 -61
  284. package/src/runner.ts +67 -78
  285. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  286. package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
  287. package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
  288. package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
  289. package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
  290. package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
  291. package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
  292. package/src/shared/__tests__/subagent-rows.test.ts +34 -0
  293. package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
  294. package/src/shared/approval-policy.ts +1 -1
  295. package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
  296. package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
  297. package/src/shared/channel-attachment.ts +1 -1
  298. package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
  299. package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
  300. package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
  301. package/src/shared/execution-context.ts +45 -0
  302. package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
  303. package/src/shared/filereview/capture.ts +1 -1
  304. package/src/shared/heartbeat.ts +13 -33
  305. package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
  306. package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
  307. package/src/shared/subagent-rows.ts +39 -0
  308. package/src/shared/worker-shutdown.ts +55 -25
  309. package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
  310. package/src/shared/workspace/platform-dir.ts +1 -1
  311. package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
  312. package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
  313. package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
  314. package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
  315. package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
  316. package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
  317. package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
  318. package/dist/activities/execute-cursor/index.d.ts +0 -209
  319. package/dist/activities/execute-cursor/index.js +0 -2345
  320. package/dist/activities/execute-cursor/index.js.map +0 -1
  321. package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
  322. package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
  323. package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
  324. package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
  325. package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
  326. package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
  327. package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
  328. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
  329. package/src/activities/execute-cursor/index.ts +0 -2876
  330. package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
  331. /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Hermetic goldens: USER PAUSE versus WORKER SHUTDOWN — the two interruptions
3
+ * that THROW, and the byte-pinned copy that tells them apart.
4
+ *
5
+ * Invariant pinned (the throw-vs-return table the runtime owns,
6
+ * `harness/terminal-table.ts`; parent §5c): both interruptions end the activity with a thrown Temporal
7
+ * `CancelledFailure` (never a return), but they persist DIFFERENT terminal
8
+ * states the control plane keys on:
9
+ *
10
+ * - a user pause (Temporal delivers cancellation, no shutdown signal) persists
11
+ * EXECUTION_PAUSED with "Execution paused by user. Use resume to continue."
12
+ * and throws "Activity paused by orchestrator";
13
+ * - a worker shutdown (the runner's per-queue shutdown signal is aborted AND
14
+ * cancellation is delivered) persists EXECUTION_FAILED with the error copy
15
+ * "Execution interrupted: runner worker was shut down. Retry or resume." and
16
+ * throws "Activity cancelled (worker shutdown, not user pause)".
17
+ *
18
+ * The cloud channel decision table matches on that copy, so the goldens
19
+ * (`goldens/pause.status.json`, `goldens/worker-shutdown.status.json`) are the
20
+ * wire contract, character for character. Both interruptions are triggered from
21
+ * a script `effect` step — never a timer — so the instant is deterministic: the
22
+ * stream loop sees `isCancelled()` on its next iteration and never processes
23
+ * the event that follows.
24
+ *
25
+ * What the goldens ALSO pin: each path appends its system message exactly
26
+ * ONCE. The orchestrator this net was recorded on appended it twice — once in
27
+ * `resolvePreBoundaryTerminal` before the throw, once again in the outer
28
+ * `catch (CancelledFailure)`, which re-stamped and re-persisted — and the
29
+ * goldens pinned that double on purpose so S2 could reproduce the wire byte
30
+ * for byte (stigmer#1054, Q-S2-4). The runtime's `settleWith` is now the one
31
+ * writer of a terminal arm, so a second row has no code path to come from.
32
+ *
33
+ * Runtime phases exercised: the adapter's stop-signal cancel; the runtime's
34
+ * `classifyTurnInterruption`; the terminal table's pause and worker-shutdown
35
+ * arms through `settleWith`; the `finally` teardown under a thrown exit.
36
+ *
37
+ * Regeneration history:
38
+ * - S2 M3b (entry 20260911.03): timestamp-only diff — every terminal stamp
39
+ * one scripted second earlier: the adapter cancels the SDK run the instant
40
+ * the runtime's stop signal aborts (Q-M3-4), before the SDK double pulls,
41
+ * and the clock ticks on, one more step. Transcript unchanged.
42
+ * - 2026-09-12 (stigmer#1054, the PR after S2): one removed system row per
43
+ * golden, nothing else. The transcript now carries each terminal row once.
44
+ *
45
+ * Regenerate ONLY after a deliberate behavior change:
46
+ * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
47
+ */
48
+
49
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
50
+ import { toJson } from "@bufbuild/protobuf";
51
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
52
+ import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
53
+
54
+ vi.mock("@cursor/sdk", async () =>
55
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
56
+ );
57
+ vi.mock("../../../../client/stigmer-client.js", async () =>
58
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
59
+ );
60
+
61
+ import {
62
+ ScriptedClock,
63
+ createHermeticEnvironment,
64
+ threwCancelledFailure,
65
+ type HermeticEnvironment,
66
+ type InvocationControls,
67
+ } from "../../../../__test-utils__/hermetic-activity.js";
68
+ import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
69
+ import {
70
+ SDK_CATALOG,
71
+ beginCursorScenario,
72
+ cursorExecutionRecord,
73
+ runCursorTurn,
74
+ } from "../../__test-utils__/hermetic-cursor.js";
75
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
76
+
77
+ const USER_MESSAGE = "Refactor the parser module.";
78
+ const NEVER_SEEN = "This text must never reach the transcript.";
79
+
80
+ /**
81
+ * A turn that is interrupted mid-stream by `interrupt(controls)`: one assistant
82
+ * message lands, then the interruption, then an event the loop must NOT process.
83
+ */
84
+ function interruptedTurn(
85
+ agentId: string,
86
+ runId: string,
87
+ clock: ScriptedClock,
88
+ getControls: () => InvocationControls,
89
+ interrupt: (controls: InvocationControls) => void,
90
+ ): ScriptedCursorAgent {
91
+ const ev = sdkEvents(agentId, runId);
92
+ return new ScriptedCursorAgent({
93
+ agentId,
94
+ runIds: [runId],
95
+ observeStep: () => clock.tick(),
96
+ turns: [
97
+ [
98
+ step.event(ev.init()),
99
+ step.event(ev.assistant("Starting on the parser.")),
100
+ step.effect("interrupt the activity from outside", () => interrupt(getControls())),
101
+ step.event(ev.assistant(NEVER_SEEN)),
102
+ step.finished({ result: NEVER_SEEN }),
103
+ ],
104
+ ],
105
+ });
106
+ }
107
+
108
+ describe("ExecuteCursor hermetic — user pause vs worker shutdown", () => {
109
+ let env: HermeticEnvironment;
110
+ let registry: ReturnType<typeof stubRegistryFetch>;
111
+ const clock = new ScriptedClock();
112
+
113
+ beforeAll(() => {
114
+ env = createHermeticEnvironment();
115
+ registry = stubRegistryFetch();
116
+ clock.install();
117
+ });
118
+
119
+ afterAll(() => {
120
+ clock.uninstall();
121
+ registry.restore();
122
+ env.dispose();
123
+ });
124
+
125
+ it("a user pause persists PAUSED and throws CancelledFailure", async () => {
126
+ // ── Arrange ──────────────────────────────────────────────────────────────
127
+ clock.reset(); // both goldens read from :00
128
+ let controls: InvocationControls | undefined;
129
+ const agent = interruptedTurn(
130
+ "agent-hermetic-pause-0001",
131
+ "run-hermetic-pause-0001",
132
+ clock,
133
+ () => controls!,
134
+ (c) => c.cancel(),
135
+ );
136
+ const record = cursorExecutionRecord({ message: USER_MESSAGE });
137
+ const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
138
+
139
+ // ── Act ──────────────────────────────────────────────────────────────────
140
+ const invocation = await runCursorTurn(scenario, { onControls: (c) => (controls = c) });
141
+
142
+ // ── Assert: the throw and the persisted state ────────────────────────────
143
+ expect(threwCancelledFailure(invocation.outcome), "a pause THROWS CancelledFailure").toBe(true);
144
+ if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
145
+ expect(invocation.outcome.error.message).toBe("Activity paused by orchestrator");
146
+ expect(record.persistedPhases).toEqual([
147
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
148
+ ExecutionPhase.EXECUTION_PAUSED,
149
+ ]);
150
+ const final = record.lastFullStatus!;
151
+ expect(final.error, "a pause is not an error").toBe("");
152
+ expect(final.completedAt, "a paused turn is not complete").toBe("");
153
+ expect(
154
+ final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
155
+ ).toEqual(["Execution paused by user. Use resume to continue."]);
156
+ expect(final.messages.some((m) => m.content === NEVER_SEEN), "nothing after the cancel is processed").toBe(false);
157
+
158
+ const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
159
+ await expect(json).toMatchFileSnapshot("./goldens/pause.status.json");
160
+ });
161
+
162
+ it("a worker shutdown persists FAILED with the shutdown copy and throws CancelledFailure", async () => {
163
+ // ── Arrange ──────────────────────────────────────────────────────────────
164
+ clock.reset();
165
+ let controls: InvocationControls | undefined;
166
+ const agent = interruptedTurn(
167
+ "agent-hermetic-shutdown-0001",
168
+ "run-hermetic-shutdown-0001",
169
+ clock,
170
+ () => controls!,
171
+ (c) => {
172
+ // The runner-manager drains: the queue's shutdown signal aborts, then
173
+ // Temporal delivers the cancellation.
174
+ c.signalWorkerShutdown();
175
+ c.cancel();
176
+ },
177
+ );
178
+ const record = cursorExecutionRecord({ message: USER_MESSAGE });
179
+ const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
180
+
181
+ // ── Act ──────────────────────────────────────────────────────────────────
182
+ const invocation = await runCursorTurn(scenario, { onControls: (c) => (controls = c) });
183
+
184
+ // ── Assert: the throw and the persisted state ────────────────────────────
185
+ expect(threwCancelledFailure(invocation.outcome), "a shutdown THROWS CancelledFailure").toBe(true);
186
+ if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
187
+ expect(invocation.outcome.error.message).toBe("Activity cancelled (worker shutdown, not user pause)");
188
+ expect(record.persistedPhases).toEqual([
189
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
190
+ ExecutionPhase.EXECUTION_FAILED,
191
+ ]);
192
+ const final = record.lastFullStatus!;
193
+ expect(final.error).toBe("Execution interrupted: runner worker was shut down. Retry or resume.");
194
+ expect(final.completedAt).not.toBe("");
195
+ expect(
196
+ final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
197
+ ).toEqual([
198
+ "Execution interrupted: the runner worker was shut down while the agent was still running. You can retry or resume.",
199
+ ]);
200
+ expect(final.messages.some((m) => m.content === NEVER_SEEN)).toBe(false);
201
+
202
+ const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
203
+ await expect(json).toMatchFileSnapshot("./goldens/worker-shutdown.status.json");
204
+ });
205
+ });
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Hermetic golden: a PLAIN TURN through the whole `ExecuteCursor` activity.
3
+ *
4
+ * Invariant pinned: an execution with no tools, no workspace entries, no MCP
5
+ * servers and no skills — the user asks, the agent answers — runs the full
6
+ * setup pipeline and ends COMPLETED with exactly the transcript, usage and
7
+ * timestamps recorded in `goldens/plain-turn.status.json`, RETURNING a slim
8
+ * status to the workflow (never throwing). This is the baseline every other
9
+ * scenario is a delta from, and the first proof that the activity is byte-
10
+ * stable under the hermetic driver.
11
+ *
12
+ * Parent phase rows exercised (20260908.02 `T01_0_plan.md` §3a): normalize
13
+ * input / heartbeat; artifact storage + the `persist` chokepoint; fetch
14
+ * execution, session, agent, blueprint; resolve environment (NOT_FOUND ->
15
+ * empty); provision workspace (per-session dir); workspace turn lock; MCP
16
+ * resolve (none); skills (none); attachments (none); HITL gate install;
17
+ * pricing + model id + variant params; create the engine session; write
18
+ * `harness_state_id` back; prompt composition; usage accumulation; run the
19
+ * turn; terminal mapping COMPLETED -> return; cleanup.
20
+ *
21
+ * Nothing here is a live Cursor agent: `@cursor/sdk` is the scripted double
22
+ * (`__test-utils__/scripted-sdk.ts`) and the control plane is the in-memory
23
+ * execution record (`__test-utils__/hermetic-activity.ts`). No network, no
24
+ * `CURSOR_API_KEY`, no Temporal worker.
25
+ *
26
+ * Regenerate the golden ONLY after a deliberate behavior change:
27
+ * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
28
+ */
29
+
30
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
31
+ import { toJson } from "@bufbuild/protobuf";
32
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
33
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
34
+
35
+ // Both mocks are hoisted by vitest; they must live in this file. Each factory
36
+ // imports the reusable module and returns its mock surface.
37
+ vi.mock("@cursor/sdk", async () =>
38
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
39
+ );
40
+ vi.mock("../../../../client/stigmer-client.js", async () =>
41
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
42
+ );
43
+
44
+ import {
45
+ ScriptedClock,
46
+ createHermeticEnvironment,
47
+ type HermeticEnvironment,
48
+ } from "../../../../__test-utils__/hermetic-activity.js";
49
+ import { ScriptedCursorAgent, step } from "../../__test-utils__/scripted-agent.js";
50
+ import {
51
+ FIXTURE,
52
+ SDK_CATALOG,
53
+ beginCursorScenario,
54
+ cursorExecutionRecord,
55
+ runCursorTurn,
56
+ } from "../../__test-utils__/hermetic-cursor.js";
57
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
58
+
59
+ const AGENT_ID = "agent-hermetic-plain-0001";
60
+ const RUN_ID = "run-hermetic-plain-0001";
61
+ const USER_MESSAGE = "Say hello in five words or fewer.";
62
+ const ASSISTANT_TEXT = "Hello there, hermetic world.";
63
+
64
+ describe("ExecuteCursor hermetic — plain turn", () => {
65
+ let env: HermeticEnvironment;
66
+ let registry: ReturnType<typeof stubRegistryFetch>;
67
+ const clock = new ScriptedClock();
68
+
69
+ beforeAll(() => {
70
+ env = createHermeticEnvironment();
71
+ registry = stubRegistryFetch();
72
+ clock.install();
73
+ });
74
+
75
+ afterAll(() => {
76
+ clock.uninstall();
77
+ registry.restore();
78
+ env.dispose();
79
+ });
80
+
81
+ it("completes with the golden transcript and returns a slim COMPLETED status", async () => {
82
+ // ── Arrange ──────────────────────────────────────────────────────────────
83
+ const agent = new ScriptedCursorAgent({
84
+ agentId: AGENT_ID,
85
+ runIds: [RUN_ID],
86
+ observeStep: () => clock.tick(),
87
+ turns: [
88
+ [
89
+ step.event({ type: "system", subtype: "init", agent_id: AGENT_ID, run_id: RUN_ID }),
90
+ step.event({
91
+ type: "assistant",
92
+ agent_id: AGENT_ID,
93
+ run_id: RUN_ID,
94
+ message: { role: "assistant", content: [{ type: "text", text: ASSISTANT_TEXT }] },
95
+ }),
96
+ step.turnEnded({ inputTokens: 1_200, outputTokens: 40 }),
97
+ step.finished({
98
+ result: ASSISTANT_TEXT,
99
+ model: {
100
+ id: FIXTURE.model,
101
+ params: [
102
+ { id: "fast", value: "false" },
103
+ { id: "thinking", value: "false" },
104
+ ],
105
+ },
106
+ }),
107
+ ],
108
+ ],
109
+ });
110
+ const record = cursorExecutionRecord({ message: USER_MESSAGE });
111
+ const scenario = beginCursorScenario({
112
+ env,
113
+ clock,
114
+ record,
115
+ sdk: { agents: [agent], catalog: SDK_CATALOG },
116
+ });
117
+
118
+ // ── Act ──────────────────────────────────────────────────────────────────
119
+ const invocation = await runCursorTurn(scenario);
120
+
121
+ // ── Assert: the outcome the workflow sees ────────────────────────────────
122
+ expect(invocation.outcome.kind, "a completed turn RETURNS, never throws").toBe("returned");
123
+ const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
124
+ expect(slim.phase).toBe("EXECUTION_COMPLETED");
125
+ expect(slim.final_text).toBe(ASSISTANT_TEXT);
126
+ expect(slim, "the slim return carries no transcript").not.toHaveProperty("messages");
127
+
128
+ // ── Assert: what the control plane received, in order ────────────────────
129
+ expect(record.persistedPhases).toEqual([
130
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
131
+ ExecutionPhase.EXECUTION_COMPLETED,
132
+ ]);
133
+ expect(record.setupProgress, "the setup pipeline's phase labels, in order").toEqual([
134
+ "Fetching execution",
135
+ "Resolving agent blueprint",
136
+ "Resolving environment",
137
+ "Provisioning workspace",
138
+ "Resolving MCP servers",
139
+ "Resolving skills",
140
+ "Initializing Cursor agent",
141
+ ]);
142
+
143
+ // ── Assert: what the SDK was asked ───────────────────────────────────────
144
+ expect(scenario.sdk.resolutions).toHaveLength(1);
145
+ expect(scenario.sdk.resolutions[0].kind, "a fresh session creates, never resumes").toBe("create");
146
+ expect(scenario.sdk.resolutions[0].options?.model).toEqual({
147
+ id: FIXTURE.model,
148
+ params: [
149
+ { id: "fast", value: "false" },
150
+ { id: "thinking", value: "false" },
151
+ ],
152
+ });
153
+ expect(agent.sends).toHaveLength(1);
154
+ expect(String(agent.sends[0].message), "the prompt carries the user's message").toContain(USER_MESSAGE);
155
+ expect(agent.sends[0].hasOnDelta).toBe(true);
156
+
157
+ // ── Assert: the harness_state_id write-back ─────────────────────────────
158
+ expect(record.sessionUpdates).toHaveLength(1);
159
+ expect(record.sessionUpdates[0].spec?.harnessStateId).toBe(AGENT_ID);
160
+
161
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
162
+ expect(invocation.heartbeats.length, "the whole-activity heartbeat ran").toBeGreaterThan(0);
163
+ expect(registry.urls.every((u) => u.includes("/model-registry")), "no network beyond the registry").toBe(true);
164
+
165
+ // ── Assert: the golden ───────────────────────────────────────────────────
166
+ const finalStatus = record.lastFullStatus;
167
+ expect(finalStatus).toBeDefined();
168
+ const json = JSON.stringify(toJson(AgentExecutionStatusSchema, finalStatus!), null, 2) + "\n";
169
+ await expect(json).toMatchFileSnapshot("./goldens/plain-turn.status.json");
170
+ });
171
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Hermetic golden: POISONED-HANDLE RECOVERY — a resumed agent whose run fails
3
+ * with a transport error is replaced by a fresh agent, once, and the turn
4
+ * completes on the replacement.
5
+ *
6
+ * Invariant pinned: when `run.wait()` reports `error` on an agent that was
7
+ * RESUMED (`resolution.reason === "resumed_successfully"`) and the classifier
8
+ * files the error as `network` or `agent-stale`, the activity closes the stale
9
+ * handle, calls `Agent.create` for a fresh one, writes the fresh id to the
10
+ * session's `harness_state_id`, re-runs the turn on the fresh agent through the
11
+ * IDENTICAL stream loop and boundary, and ends COMPLETED. The retry happens at
12
+ * most once (`alreadyRetriedWithFreshAgent`). The golden
13
+ * (`goldens/recovery-fresh-agent.status.json`) pins what the user sees after a
14
+ * recovered turn.
15
+ *
16
+ * Parent phase rows exercised beyond the earlier scenarios: `Agent.resume`
17
+ * through `resolveAgentWithTransportRecovery` (a second execution in a session:
18
+ * `thread_id` set, no parked agent); the `status: ERROR` stream event feeding
19
+ * `streamErrorMessage`; `run.wait()` error mapping; the error classifier
20
+ * (`error-classifier.ts` NETWORK_PATTERNS, the same shapes its own tests pin);
21
+ * the fresh-agent recovery spine (`runRecoveryStream`); the second
22
+ * `harness_state_id` write-back.
23
+ *
24
+ * Regenerate ONLY after a deliberate behavior change:
25
+ * npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
26
+ */
27
+
28
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
29
+ import { toJson } from "@bufbuild/protobuf";
30
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
31
+ import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
32
+
33
+ vi.mock("@cursor/sdk", async () =>
34
+ (await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
35
+ );
36
+ vi.mock("../../../../client/stigmer-client.js", async () =>
37
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
38
+ );
39
+
40
+ import {
41
+ ScriptedClock,
42
+ createHermeticEnvironment,
43
+ type HermeticEnvironment,
44
+ } from "../../../../__test-utils__/hermetic-activity.js";
45
+ import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
46
+ import {
47
+ FIXTURE,
48
+ SDK_CATALOG,
49
+ beginCursorScenario,
50
+ cursorExecutionRecord,
51
+ runCursorTurn,
52
+ } from "../../__test-utils__/hermetic-cursor.js";
53
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
54
+
55
+ const STALE_AGENT_ID = "agent-hermetic-stale-0001";
56
+ const FRESH_AGENT_ID = "agent-hermetic-fresh-0002";
57
+ const RUN_STALE = "run-hermetic-stale-0001";
58
+ const RUN_FRESH = "run-hermetic-fresh-0001";
59
+ const USER_MESSAGE = "Summarize what changed since yesterday.";
60
+ // A real network-class failure shape (error-classifier NETWORK_PATTERNS:
61
+ // "unavailable", "econnreset", "fetch failed").
62
+ const TRANSPORT_ERROR = "UNAVAILABLE: fetch failed (ECONNRESET) while streaming the run";
63
+ const FINAL_TEXT = "Two files changed since yesterday: notes.md and README.md.";
64
+
65
+ describe("ExecuteCursor hermetic — poisoned-handle recovery on a fresh agent", () => {
66
+ let env: HermeticEnvironment;
67
+ let registry: ReturnType<typeof stubRegistryFetch>;
68
+ const clock = new ScriptedClock();
69
+
70
+ beforeAll(() => {
71
+ env = createHermeticEnvironment();
72
+ registry = stubRegistryFetch();
73
+ clock.install();
74
+ });
75
+
76
+ afterAll(() => {
77
+ clock.uninstall();
78
+ registry.restore();
79
+ env.dispose();
80
+ });
81
+
82
+ it("replaces the stale resumed agent once and completes on the fresh one", async () => {
83
+ // ── Arrange ──────────────────────────────────────────────────────────────
84
+ const evStale = sdkEvents(STALE_AGENT_ID, RUN_STALE);
85
+ const evFresh = sdkEvents(FRESH_AGENT_ID, RUN_FRESH);
86
+ const stale = new ScriptedCursorAgent({
87
+ agentId: STALE_AGENT_ID,
88
+ runIds: [RUN_STALE],
89
+ observeStep: () => clock.tick(),
90
+ turns: [
91
+ [
92
+ step.event(evStale.init()),
93
+ step.event(evStale.assistant("Let me look at the recent changes.")),
94
+ step.event(evStale.status("ERROR", TRANSPORT_ERROR)),
95
+ step.errored({ result: TRANSPORT_ERROR }),
96
+ ],
97
+ ],
98
+ });
99
+ const fresh = new ScriptedCursorAgent({
100
+ agentId: FRESH_AGENT_ID,
101
+ runIds: [RUN_FRESH],
102
+ observeStep: () => clock.tick(),
103
+ turns: [
104
+ [
105
+ step.event(evFresh.init()),
106
+ step.event(evFresh.assistant(FINAL_TEXT)),
107
+ step.turnEnded({ inputTokens: 2_800, outputTokens: 55 }),
108
+ step.finished({ result: FINAL_TEXT, model: { id: FIXTURE.model, params: [] } }),
109
+ ],
110
+ ],
111
+ });
112
+ const record = cursorExecutionRecord({ message: USER_MESSAGE });
113
+ const scenario = beginCursorScenario({
114
+ env,
115
+ clock,
116
+ record,
117
+ // `Agent.create` hands out `fresh`; `Agent.resume(STALE)` finds `stale`,
118
+ // the previous turn's agent the session knows only by harness_state_id.
119
+ sdk: { agents: [fresh], resumableAgents: [stale], catalog: SDK_CATALOG },
120
+ });
121
+
122
+ // ── Act: the second execution in the session resumes the stale handle ────
123
+ const invocation = await runCursorTurn(scenario, { threadId: STALE_AGENT_ID, turnSeq: 0 });
124
+
125
+ // ── Assert: outcome ──────────────────────────────────────────────────────
126
+ expect(invocation.outcome.kind, "a recovered turn RETURNS like any completion").toBe("returned");
127
+ const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
128
+ expect(slim.phase).toBe("EXECUTION_COMPLETED");
129
+ expect(slim.final_text).toBe(FINAL_TEXT);
130
+ expect(record.persistedPhases).toEqual([
131
+ ExecutionPhase.EXECUTION_IN_PROGRESS,
132
+ ExecutionPhase.EXECUTION_COMPLETED,
133
+ ]);
134
+
135
+ // ── Assert: the recovery spine ───────────────────────────────────────────
136
+ expect(scenario.sdk.resolutions.map((r) => [r.kind, r.agentId])).toEqual([
137
+ ["resume", STALE_AGENT_ID],
138
+ ["create", FRESH_AGENT_ID],
139
+ ]);
140
+ expect(stale.sends, "the stale handle ran exactly once").toHaveLength(1);
141
+ expect(stale.closeCalls, "the poisoned handle is disposed").toBe(1);
142
+ expect(fresh.sends, "the fresh agent ran the turn exactly once — no second retry").toHaveLength(1);
143
+ expect(String(fresh.sends[0].message), "the rebuilt prompt carries the user's message").toContain(USER_MESSAGE);
144
+
145
+ // The session now points at the fresh agent (the stale id is superseded).
146
+ const lastSessionWrite = record.sessionUpdates.at(-1);
147
+ expect(lastSessionWrite?.spec?.harnessStateId).toBe(FRESH_AGENT_ID);
148
+
149
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
150
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
151
+
152
+ // ── Assert: the golden ───────────────────────────────────────────────────
153
+ const json = JSON.stringify(toJson(AgentExecutionStatusSchema, record.lastFullStatus!), null, 2) + "\n";
154
+ await expect(json).toMatchFileSnapshot("./goldens/recovery-fresh-agent.status.json");
155
+ });
156
+ });