@stigmer/runner 3.14.1 → 3.15.1

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 (320) 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 +28 -5
  9. package/dist/__test-utils__/hermetic-activity.js +23 -7
  10. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  11. package/dist/__test-utils__/mock-workspace.d.ts +15 -0
  12. package/dist/__test-utils__/mock-workspace.js +21 -0
  13. package/dist/__test-utils__/mock-workspace.js.map +1 -0
  14. package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
  15. package/dist/__test-utils__/model-registry-fixture.js +66 -0
  16. package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
  17. package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
  18. package/dist/__test-utils__/turn-input-fixture.js +118 -0
  19. package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
  20. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
  21. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
  22. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
  23. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
  24. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
  25. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  26. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
  27. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  28. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +38 -62
  29. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +47 -121
  30. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  31. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +17 -1
  32. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +27 -4
  33. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
  34. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +52 -11
  35. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +66 -7
  36. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
  37. package/dist/activities/execute-cursor/adapter.d.ts +55 -0
  38. package/dist/activities/execute-cursor/adapter.js +119 -0
  39. package/dist/activities/execute-cursor/adapter.js.map +1 -0
  40. package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
  41. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  42. package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
  43. package/dist/activities/execute-cursor/capture-flow.js +17 -26
  44. package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
  45. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
  46. package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
  47. package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
  48. package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
  49. package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
  50. package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
  51. package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
  52. package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
  53. package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
  54. package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
  55. package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
  56. package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
  57. package/dist/activities/execute-cursor/message-translator.js +76 -27
  58. package/dist/activities/execute-cursor/message-translator.js.map +1 -1
  59. package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
  60. package/dist/activities/execute-cursor/prompt-builder.js +171 -0
  61. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  62. package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
  63. package/dist/activities/execute-cursor/rejection-capture.js +5 -7
  64. package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
  65. package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
  66. package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
  67. package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
  68. package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
  69. package/dist/activities/execute-cursor/turn-settle.js +507 -0
  70. package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
  71. package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
  72. package/dist/activities/execute-cursor/turn-setup.js +496 -0
  73. package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
  74. package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
  75. package/dist/activities/execute-cursor/turn-stream.js +70 -135
  76. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  77. package/dist/activities/execute-cursor/turn.d.ts +43 -0
  78. package/dist/activities/execute-cursor/turn.js +222 -0
  79. package/dist/activities/execute-cursor/turn.js.map +1 -0
  80. package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
  81. package/dist/activities/execute-cursor/usage-pricing.js +50 -0
  82. package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
  83. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
  84. package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
  85. package/dist/activities/execute-deep-agent/environment.js +1 -1
  86. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  87. package/dist/activities/execute-deep-agent/setup.js +3 -2
  88. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  89. package/dist/activities/hydrate-workflow-execution.js +1 -1
  90. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  91. package/dist/config.d.ts +15 -0
  92. package/dist/config.js.map +1 -1
  93. package/dist/harness/capabilities.d.ts +16 -0
  94. package/dist/harness/capabilities.js +1 -0
  95. package/dist/harness/capabilities.js.map +1 -1
  96. package/dist/harness/persist-chokepoint.d.ts +74 -0
  97. package/dist/harness/persist-chokepoint.js +88 -0
  98. package/dist/harness/persist-chokepoint.js.map +1 -0
  99. package/dist/harness/registry.d.ts +44 -5
  100. package/dist/harness/registry.js +61 -5
  101. package/dist/harness/registry.js.map +1 -1
  102. package/dist/harness/run-turn.d.ts +53 -0
  103. package/dist/harness/run-turn.js +528 -0
  104. package/dist/harness/run-turn.js.map +1 -0
  105. package/dist/harness/stop-controller.d.ts +50 -0
  106. package/dist/harness/stop-controller.js +62 -0
  107. package/dist/harness/stop-controller.js.map +1 -0
  108. package/dist/harness/terminal-table.d.ts +141 -0
  109. package/dist/harness/terminal-table.js +239 -0
  110. package/dist/harness/terminal-table.js.map +1 -0
  111. package/dist/harness/turn-context.d.ts +523 -0
  112. package/dist/harness/turn-context.js +739 -0
  113. package/dist/harness/turn-context.js.map +1 -0
  114. package/dist/harness/types.d.ts +232 -52
  115. package/dist/harness/types.js +17 -8
  116. package/dist/harness/types.js.map +1 -1
  117. package/dist/harness/usage-accumulator.d.ts +68 -0
  118. package/dist/harness/usage-accumulator.js +101 -0
  119. package/dist/harness/usage-accumulator.js.map +1 -0
  120. package/dist/harness-adapters.d.ts +27 -0
  121. package/dist/harness-adapters.js +28 -0
  122. package/dist/harness-adapters.js.map +1 -0
  123. package/dist/main.js.map +1 -1
  124. package/dist/runner-manager.js +64 -48
  125. package/dist/runner-manager.js.map +1 -1
  126. package/dist/runner.d.ts +5 -4
  127. package/dist/runner.js +65 -64
  128. package/dist/runner.js.map +1 -1
  129. package/dist/shared/approval-policy.d.ts +1 -1
  130. package/dist/shared/approval-policy.js.map +1 -1
  131. package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
  132. package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
  133. package/dist/shared/attachment-resolver.js.map +1 -0
  134. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
  135. package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
  136. package/dist/shared/blueprint-resolver.js.map +1 -0
  137. package/dist/shared/channel-attachment.d.ts +1 -1
  138. package/dist/shared/channel-attachment.js.map +1 -1
  139. package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
  140. package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
  141. package/dist/shared/cost-guard.js.map +1 -0
  142. package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
  143. package/dist/shared/env-resolver.js.map +1 -0
  144. package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
  145. package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
  146. package/dist/shared/exact-apply.js.map +1 -0
  147. package/dist/shared/execution-context.d.ts +35 -0
  148. package/dist/shared/execution-context.js +38 -0
  149. package/dist/shared/execution-context.js.map +1 -0
  150. package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
  151. package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
  152. package/dist/shared/extract-structured-output.js.map +1 -0
  153. package/dist/shared/filereview/capture.d.ts +1 -1
  154. package/dist/shared/filereview/capture.js +1 -1
  155. package/dist/shared/heartbeat.d.ts +13 -11
  156. package/dist/shared/heartbeat.js +14 -29
  157. package/dist/shared/heartbeat.js.map +1 -1
  158. package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
  159. package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
  160. package/dist/shared/persist-decision.js.map +1 -0
  161. package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
  162. package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
  163. package/dist/shared/skill-resolver.js.map +1 -0
  164. package/dist/shared/subagent-rows.d.ts +21 -0
  165. package/dist/shared/subagent-rows.js +34 -0
  166. package/dist/shared/subagent-rows.js.map +1 -0
  167. package/dist/shared/worker-shutdown.d.ts +43 -16
  168. package/dist/shared/worker-shutdown.js +28 -13
  169. package/dist/shared/worker-shutdown.js.map +1 -1
  170. package/dist/shared/workspace/platform-dir.d.ts +1 -1
  171. package/dist/shared/workspace/platform-dir.js +1 -1
  172. package/dist/shared/workspace/session-provision.d.ts +54 -0
  173. package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
  174. package/dist/shared/workspace/session-provision.js.map +1 -0
  175. package/package.json +4 -4
  176. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +110 -14
  177. package/src/__test-utils__/execution-record-fixture.ts +104 -0
  178. package/src/__test-utils__/harness-boot-order-child.ts +58 -0
  179. package/src/__test-utils__/harness-contract/contract.ts +128 -83
  180. package/src/__test-utils__/harness-contract/recording-sink.ts +15 -4
  181. package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
  182. package/src/__test-utils__/harness-contract/scripted-adapter.ts +92 -32
  183. package/src/__test-utils__/harness-contract/types.ts +85 -9
  184. package/src/__test-utils__/hermetic-activity.ts +33 -8
  185. package/src/__test-utils__/model-registry-fixture.ts +72 -0
  186. package/src/__test-utils__/module-specifiers.ts +82 -0
  187. package/src/__test-utils__/turn-input-fixture.ts +155 -0
  188. package/src/__tests__/harness-boot-order.test.ts +77 -0
  189. package/src/__tests__/harness-contract.test.ts +7 -5
  190. package/src/__tests__/runner-manager.test.ts +18 -0
  191. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
  192. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
  193. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
  194. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
  195. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +65 -145
  196. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +30 -4
  197. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +96 -8
  198. package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
  199. package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
  200. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
  201. package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
  202. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
  203. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
  204. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +1 -1
  205. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -1
  206. package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
  207. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +1 -6
  208. package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
  209. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
  210. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
  211. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
  212. package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
  213. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
  214. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
  215. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
  216. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +2 -7
  217. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
  218. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +22 -18
  219. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +1 -1
  220. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +1 -1
  221. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
  222. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
  223. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
  224. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +1 -1
  225. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
  226. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
  227. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
  228. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
  229. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
  230. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
  231. package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
  232. package/src/activities/execute-cursor/adapter.ts +138 -0
  233. package/src/activities/execute-cursor/approval-state.ts +2 -2
  234. package/src/activities/execute-cursor/capture-flow.ts +18 -41
  235. package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
  236. package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
  237. package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
  238. package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
  239. package/src/activities/execute-cursor/message-translator.ts +90 -45
  240. package/src/activities/execute-cursor/prompt-builder.ts +327 -12
  241. package/src/activities/execute-cursor/rejection-capture.ts +5 -12
  242. package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
  243. package/src/activities/execute-cursor/turn-boundary.ts +1 -1
  244. package/src/activities/execute-cursor/turn-settle.ts +601 -0
  245. package/src/activities/execute-cursor/turn-setup.ts +645 -0
  246. package/src/activities/execute-cursor/turn-stream.ts +84 -210
  247. package/src/activities/execute-cursor/turn.ts +240 -0
  248. package/src/activities/execute-cursor/usage-pricing.ts +67 -0
  249. package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
  250. package/src/activities/execute-deep-agent/environment.ts +1 -1
  251. package/src/activities/execute-deep-agent/setup.ts +3 -2
  252. package/src/activities/hydrate-workflow-execution.ts +1 -1
  253. package/src/config.ts +13 -0
  254. package/src/harness/__tests__/import-direction.test.ts +168 -0
  255. package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
  256. package/src/harness/__tests__/registry.test.ts +1 -0
  257. package/src/harness/__tests__/run-turn.test.ts +165 -0
  258. package/src/harness/__tests__/stop-controller.test.ts +49 -0
  259. package/src/harness/__tests__/turn-context.test.ts +178 -0
  260. package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
  261. package/src/harness/capabilities.ts +17 -0
  262. package/src/harness/persist-chokepoint.ts +112 -0
  263. package/src/harness/registry.ts +79 -5
  264. package/src/harness/run-turn.ts +618 -0
  265. package/src/harness/stop-controller.ts +83 -0
  266. package/src/harness/terminal-table.ts +276 -0
  267. package/src/harness/turn-context.ts +1028 -0
  268. package/src/harness/types.ts +248 -53
  269. package/src/harness/usage-accumulator.ts +122 -0
  270. package/src/harness-adapters.ts +30 -0
  271. package/src/main.ts +1 -1
  272. package/src/runner-manager.ts +68 -61
  273. package/src/runner.ts +67 -78
  274. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  275. package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
  276. package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
  277. package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
  278. package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
  279. package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
  280. package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
  281. package/src/shared/__tests__/subagent-rows.test.ts +34 -0
  282. package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
  283. package/src/shared/approval-policy.ts +1 -1
  284. package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
  285. package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
  286. package/src/shared/channel-attachment.ts +1 -1
  287. package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
  288. package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
  289. package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
  290. package/src/shared/execution-context.ts +45 -0
  291. package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
  292. package/src/shared/filereview/capture.ts +1 -1
  293. package/src/shared/heartbeat.ts +13 -33
  294. package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
  295. package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
  296. package/src/shared/subagent-rows.ts +39 -0
  297. package/src/shared/worker-shutdown.ts +55 -25
  298. package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
  299. package/src/shared/workspace/platform-dir.ts +1 -1
  300. package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
  301. package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
  302. package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
  303. package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
  304. package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
  305. package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
  306. package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
  307. package/dist/activities/execute-cursor/index.d.ts +0 -209
  308. package/dist/activities/execute-cursor/index.js +0 -2345
  309. package/dist/activities/execute-cursor/index.js.map +0 -1
  310. package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
  311. package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
  312. package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
  313. package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
  314. package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
  315. package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
  316. package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
  317. package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
  318. package/src/activities/execute-cursor/index.ts +0 -2876
  319. package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
  320. /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
@@ -9,15 +9,16 @@
9
9
  * run.wait() strips to a bare { status: "error" }.
10
10
  *
11
11
  * This module installs a process-level handler that extracts ConnectError
12
- * fields and stores them in a Map keyed by executionId. The execute-cursor
13
- * activity reads from this Map after run.wait() resolves to enrich the
12
+ * fields and stores them in a Map keyed by executionId (read from the
13
+ * runtime's execution-scoped store, `shared/execution-context.ts`). The
14
+ * Cursor adapter reads from this Map after run.wait() resolves to enrich the
14
15
  * error message.
15
16
  *
16
17
  * Forum reference:
17
18
  * https://forum.cursor.com/t/agent-send-wait-returns-bare-status-error-while-connectrpc-unauthenticated-leaks-as-unhandledrejection/161203
18
19
  */
19
20
 
20
- import { AsyncLocalStorage } from "node:async_hooks";
21
+ import { getExecutionContext } from "../../shared/execution-context.js";
21
22
 
22
23
  export interface CapturedRejection {
23
24
  code: string;
@@ -29,14 +30,6 @@ const capturedRejections = new Map<string, CapturedRejection>();
29
30
 
30
31
  const REJECTION_TTL_MS = 5 * 60 * 1000;
31
32
 
32
- let executionContextRef: AsyncLocalStorage<{ executionId: string }> | null = null;
33
-
34
- export function setExecutionContextRef(
35
- ctx: AsyncLocalStorage<{ executionId: string }>,
36
- ): void {
37
- executionContextRef = ctx;
38
- }
39
-
40
33
  export function getCapturedRejection(executionId: string): CapturedRejection | undefined {
41
34
  return capturedRejections.get(executionId);
42
35
  }
@@ -65,7 +58,7 @@ function extractConnectCode(err: Error & { code?: string }): string {
65
58
  export function handleUnhandledRejection(reason: unknown): void {
66
59
  if (isConnectError(reason)) {
67
60
  const code = extractConnectCode(reason);
68
- const executionId = executionContextRef?.getStore()?.executionId;
61
+ const executionId = getExecutionContext().getStore()?.executionId;
69
62
 
70
63
  if (executionId) {
71
64
  capturedRejections.set(executionId, {
@@ -47,6 +47,7 @@ import type {
47
47
  ModelParameterValue,
48
48
  } from "@cursor/sdk";
49
49
  import { withTimeout, TimeoutError } from "../../shared/with-timeout.js";
50
+ import type { CloudRepo } from "../../shared/blueprint-resolver.js";
50
51
  import type { CursorMcpServerConfig } from "./cursor-mcp-config.js";
51
52
 
52
53
  // ---------------------------------------------------------------------------
@@ -137,11 +138,6 @@ export interface ResumeAgentOptions {
137
138
  // Public types — cloud mode
138
139
  // ---------------------------------------------------------------------------
139
140
 
140
- export interface CloudRepo {
141
- url: string;
142
- startingRef?: string;
143
- }
144
-
145
141
  export interface CreateCloudAgentOptions {
146
142
  apiKey: string;
147
143
  model?: string;
@@ -109,7 +109,7 @@ export interface TurnBoundaryOptions {
109
109
  /** CAS blob store for gitignored/non-git captures; undefined → git-only capture. */
110
110
  readonly artifactStorage: ArtifactStorage | undefined;
111
111
  /** Merged approval policies, threaded to the denied-call reconcile for gate provenance. */
112
- readonly mergedPolicies: Map<string, MergedToolPolicy>;
112
+ readonly mergedPolicies: ReadonlyMap<string, MergedToolPolicy>;
113
113
  /**
114
114
  * The first-denial-stop's run.cancel() promise, when a denial stopped this
115
115
  * run. Awaited (timeboxed) before the ledger read: run.cancel() races the
@@ -0,0 +1,601 @@
1
+ /**
2
+ * The Cursor harness's settle — from a consumed stream to a `TurnOutcome`.
3
+ *
4
+ * In order: the post-stream finalize (transcript, enrichments, sub-agents,
5
+ * the usage snapshot's carrier, the recorder flush, one persist so the UI
6
+ * sees settled rows); the runtime's stop, if it fired, settled as
7
+ * `interrupted` with nothing further; the turn boundary (`turn-boundary.ts`,
8
+ * whole, Q-S2-1) that authors this turn's change set and overlays the hook's
9
+ * denials as WAITING rows, ending `awaiting_approval` when it says so or
10
+ * failing on an unattributed hook block (#205); `run.wait()` and the
11
+ * classifier; the two recovery spines (a poisoned resumed handle, a
12
+ * transport timeout on a created agent), each running the IDENTICAL stream,
13
+ * finalize and boundary against a fresh agent at most once; the collapse of
14
+ * redundant same-identity tool-call twins; and the outcome.
15
+ *
16
+ * What this module never does: write a phase, a terminal copy or
17
+ * `completedAt` — those are the runtime's (`harness/run-turn.ts`,
18
+ * `terminal-table.ts`); the outcome carries what the runtime cannot read
19
+ * from the status (`failed`'s message and surface, `cancelled`). It does
20
+ * mutate `sink.status`'s transcript rows, which are this harness's to write.
21
+ *
22
+ * Moved from `index.ts` `executeCursorInner` phases 11 to 13 at S2 M3b; the
23
+ * bodies are the orchestrator's, with the runtime's arms taken out.
24
+ */
25
+
26
+ import { create } from "@bufbuild/protobuf";
27
+ import type { Run } from "@cursor/sdk";
28
+ import type { ConversationTurn } from "@cursor/sdk";
29
+ import { StreamingUsageSummarySchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/usage_pb";
30
+
31
+ import type { TurnInput, TurnOutcome, TurnSink } from "../../harness/types.js";
32
+ import { TimingRecorder, emitTimingLog } from "../../shared/cold-start-timing.js";
33
+ import { StreamingUpdateScheduler, loadStreamingConfig } from "../../shared/streaming-scheduler.js";
34
+ import { createCursorEventRecorder } from "./cursor-event-recorder.js";
35
+ import { DeltaEnricher } from "./delta-enricher.js";
36
+ import {
37
+ extractRunErrorSources,
38
+ formatClassifiedError,
39
+ shouldRetryWithFreshAgent,
40
+ synthesizeError,
41
+ } from "./error-classifier.js";
42
+ import { closeProxySessions } from "./http2-interceptor.js";
43
+ import { MessageAccumulator, collapseRedundantToolCallTwins } from "./message-translator.js";
44
+ import { clearCapturedRejection, getCapturedRejection } from "./rejection-capture.js";
45
+ import { TodoTracker } from "./todo-tracker.js";
46
+ import { runTurnBoundary, type TurnBoundaryResult } from "./turn-boundary.js";
47
+ import {
48
+ consumeCursorTurnStream,
49
+ makeCursorTurnOnDelta,
50
+ type CursorTurnStreamDeps,
51
+ type TurnOnDeltaDeps,
52
+ type TurnStreamState,
53
+ } from "./turn-stream.js";
54
+ import type { AdjudicatedRows, CursorEngine, CursorGate, CursorPrompt, CursorAdapterConfig } from "./turn-setup.js";
55
+ import { createFreshAgent } from "./turn-setup.js";
56
+
57
+ /** Everything the settle reads that setup produced. */
58
+ export interface CursorTurnFrame {
59
+ readonly input: TurnInput;
60
+ readonly sink: TurnSink;
61
+ readonly config: CursorAdapterConfig;
62
+ readonly engine: CursorEngine;
63
+ readonly gate: CursorGate;
64
+ readonly prompt: CursorPrompt;
65
+ readonly rows: AdjudicatedRows;
66
+ readonly streamState: TurnStreamState;
67
+ }
68
+
69
+ /**
70
+ * Send the primary prompt, consume the stream, and settle. The one entry the
71
+ * adapter calls after setup; every collaborator the stream loop needs is
72
+ * built here, once, and shared with both recovery spines.
73
+ */
74
+ export async function streamAndSettle(frame: CursorTurnFrame): Promise<TurnOutcome> {
75
+ const { input, sink, config, engine, gate, prompt, rows, streamState } = frame;
76
+ const { executionId, sessionId, blueprint, workspace, mcp } = input;
77
+ const { status } = sink;
78
+
79
+ const deltaEnricher = new DeltaEnricher();
80
+ const todoTracker = new TodoTracker(status.todos);
81
+ const eventRecorder = createCursorEventRecorder(executionId);
82
+
83
+ // The two recovery retries (poisoned-handle / transport-timeout) below run at
84
+ // most once per turn; this guard is the latch.
85
+ let alreadyRetriedWithFreshAgent = false;
86
+
87
+ // The shared onDelta only needs the sink/pricer/enricher/state subset, and
88
+ // it is wired at SEND time — before the accumulator exists — so it takes the
89
+ // narrow deps. The primary send and both retry sends reuse this object.
90
+ const onDeltaDeps: TurnOnDeltaDeps = {
91
+ sink,
92
+ usagePricer: engine.usagePricer,
93
+ deltaEnricher,
94
+ promptEstimatedTokens: prompt.promptEstimatedTokens,
95
+ executionId,
96
+ state: streamState,
97
+ };
98
+
99
+ // Issue #209 forensics: the SDK acquires the local executor — the piece
100
+ // that actually spawns stdio MCP servers — inside send(), AFTER the
101
+ // execution_setup timeline has already been emitted. This one-shot
102
+ // timeline makes that previously invisible window measurable:
103
+ // `send_returned` covers the send() call itself, `first_delta` the wait
104
+ // until the SDK's first delta. Primary send only — the recovery retries
105
+ // below rebuild the agent and would skew the user-perceived turn start
106
+ // this measures. No delta (immediate pause/failure) → no line.
107
+ const turnStartTiming = new TimingRecorder();
108
+ let turnFirstEventEmitted = false;
109
+ const primaryOnDelta = makeCursorTurnOnDelta(onDeltaDeps);
110
+
111
+ const run = await engine.resolution.agent.send(prompt.toSendMessage(prompt.effectivePrompt, prompt.primarySendImages), {
112
+ onDelta: (event) => {
113
+ if (!turnFirstEventEmitted) {
114
+ turnFirstEventEmitted = true;
115
+ turnStartTiming.mark("first_delta");
116
+ emitTimingLog("turn_first_event", {
117
+ execution_id: executionId,
118
+ session_id: sessionId,
119
+ harness: "cursor",
120
+ agent_resumed: engine.resolution.resumed,
121
+ mcp_server_count: blueprint.mergedMcpServerUsages.length,
122
+ }, turnStartTiming);
123
+ }
124
+ primaryOnDelta(event);
125
+ },
126
+ });
127
+ sink.recordActivity();
128
+ // Normally send() resolves before any delta arrives, making send_returned
129
+ // the first segment; if a delta beat it, the line is already emitted and
130
+ // adding a mark now would be meaningless.
131
+ if (!turnFirstEventEmitted) {
132
+ turnStartTiming.mark("send_returned");
133
+ }
134
+
135
+ // Everything at an index >= this was produced by THIS turn's stream — the
136
+ // positional turn boundary the approved-command provenance (DD-28) scopes
137
+ // its qualification to. Snapshotted before the accumulator can append.
138
+ const turnStartMessageIndex = status.messages.length;
139
+
140
+ const accumulator = new MessageAccumulator(status.messages, {
141
+ mergedPolicies: mcp.policies,
142
+ provenance: { globalBypass: mcp.leases.global, leasedCategories: mcp.leases.categories },
143
+ workspaceRoot: workspace.primaryDir,
144
+ seededSubAgents: rows.seededSubAgents,
145
+ });
146
+ // Shared cadence with the native harness: discrete state changes force a
147
+ // flush; high-frequency token deltas ride this scheduler's time cadence
148
+ // (env-tunable via STREAMING_* — see loadStreamingConfig).
149
+ const scheduler = new StreamingUpdateScheduler(loadStreamingConfig());
150
+
151
+ // Full deps for the shared stream loop, all keyed off the single stream
152
+ // state. Consumed by the primary stream here and by both recovery retries.
153
+ const streamDeps: CursorTurnStreamDeps = {
154
+ ...onDeltaDeps,
155
+ accumulator,
156
+ todoTracker,
157
+ eventRecorder,
158
+ scheduler,
159
+ progressSubstrate: gate.progressSubstrate,
160
+ progressState: gate.progressState,
161
+ changeSetId: workspace.changeSetId,
162
+ hitlDir: gate.hitlDir,
163
+ };
164
+
165
+ // Post-stream finalize, shared by the primary turn and both recovery retries:
166
+ // finalize the transcript + streaming flags, mark any in-flight sub-agent
167
+ // CANCELLED on a stopped turn, keep the usage summary's carrier current,
168
+ // flush the recorder, and persist so the UI sees the settled rows.
169
+ const finalizeStreamPhase = async (): Promise<void> => {
170
+ accumulator.finalize();
171
+ deltaEnricher.finalize(status.messages);
172
+ // A stopped turn (pause, shutdown, stall, cost cap, platform stop) aborts
173
+ // the Cursor SDK run, so any sub-agent the parent had delegated is no
174
+ // longer executing. Mark it CANCELLED rather than leaving a permanent
175
+ // IN_PROGRESS "zombie" in the final snapshot (parity with the native
176
+ // harness's cancelSubAgents()).
177
+ if (sink.stopSignal.aborted) {
178
+ accumulator.cancelInProgressSubAgents();
179
+ }
180
+ status.subAgentExecutions = accumulator.subAgentExecutions;
181
+ await eventRecorder?.flush();
182
+ console.log(
183
+ `ExecuteCursor stream ended: execution=${executionId}, events=${streamState.eventCount}, messages=${status.messages.length}, subAgents=${status.subAgentExecutions.length}`,
184
+ );
185
+ // Persist immediately after finalize so the UI sees correct tool-call
186
+ // statuses before the boundary / run.wait() / the runtime's epilogue.
187
+ await sink.requestPersist();
188
+ sink.recordActivity();
189
+ };
190
+
191
+ // The turn boundary — author this turn's change set to the file_review
192
+ // ledger (CANDIDATE_CAPTURED) and overlay the hook's denials as
193
+ // WAITING_APPROVAL gate rows. The full pipeline and its ordering rationale
194
+ // live in turn-boundary.ts; this closure binds the turn's state so the
195
+ // recovery retries (which re-run the agent AFTER this primary call) can
196
+ // re-enter the IDENTICAL pipeline — a retry's edits must reach the ledger
197
+ // or they silently escape review.
198
+ const runBoundary = (denialSettled?: Promise<void>): Promise<TurnBoundaryResult> =>
199
+ runTurnBoundary({
200
+ status,
201
+ executionId,
202
+ changeSetId: workspace.changeSetId,
203
+ hitlDir: gate.hitlDir,
204
+ captureMode: workspace.captureMode,
205
+ baselineTree: gate.baselineTree,
206
+ primaryWorkspaceDir: workspace.primaryDir,
207
+ gitWorkspace: workspace.gitWorkspace,
208
+ turnStartMessageIndex,
209
+ approvalGrants: gate.approvalGrants,
210
+ globalBypass: mcp.leases.global,
211
+ seededSubAgents: rows.seededSubAgents,
212
+ artifactStorage: input.artifactStorage,
213
+ mergedPolicies: mcp.policies,
214
+ denialCancelSettled: denialSettled,
215
+ foreignGatingHooks: gate.hitlGate.foreignGatingHooks,
216
+ });
217
+
218
+ // The boundary mutated the transcript in place and asked to pause for
219
+ // review: the runtime flips the phase, persists, and returns to the
220
+ // workflow, which waits for the approval/file-review signal and reinvokes.
221
+ const awaitingApproval = (boundary: TurnBoundaryResult): TurnOutcome => {
222
+ console.log(
223
+ `ExecuteCursor returning WAITING_FOR_APPROVAL: ${boundary.deniedToolCallCount} gated tool(s), ` +
224
+ `${boundary.capturedChangeCount} file card(s) pending`,
225
+ );
226
+ return { kind: "awaiting_approval" };
227
+ };
228
+
229
+ // Issue #205: a tool was blocked by a hook Stigmer does not own (the merge
230
+ // preserves the user's own gating hooks, and Cursor runs every one), so no
231
+ // approval can unblock it — an approval grants a token only OUR hook reads,
232
+ // and the foreign hook would deny the re-attempt forever. Completing would
233
+ // be the silent-failure shape the issue describes; instead fail with a
234
+ // diagnosable reason naming the blocked tools and the likely culprit. The
235
+ // user can fix this one: the `actionable` surface.
236
+ const unattributedHookBlock = (boundary: TurnBoundaryResult): TurnOutcome => {
237
+ const blockedTools = [...new Set(boundary.unattributedHookBlocks.map((b) => b.toolName))].join(", ");
238
+ const foreign = gate.hitlGate.foreignGatingHooks;
239
+ const culprit = foreign.length > 0
240
+ ? ` The workspace's .cursor/hooks.json registers hook(s) outside Stigmer's control ` +
241
+ `[${foreign.join(", ")}], which most likely denied it.`
242
+ : "";
243
+ const message =
244
+ `A Cursor hook outside Stigmer's approval gate blocked tool(s): ${blockedTools}.` +
245
+ culprit +
246
+ ` Stigmer cannot request approval on a foreign hook's behalf — remove or adjust ` +
247
+ `the hook in .cursor/hooks.json and retry.`;
248
+ console.error(
249
+ `ExecuteCursor failed (unattributed hook block): execution=${executionId}, ` +
250
+ `tools=[${blockedTools}], foreignHooks=[${foreign.join(", ")}]`,
251
+ );
252
+ return { kind: "failed", surface: "actionable", message };
253
+ };
254
+
255
+ // Re-enter the turn boundary for a recovery retry: author the retry's net
256
+ // change set to the file_review ledger and overlay any denials as gates —
257
+ // without this a retry's file edits silently escape review (production case
258
+ // aex_01kws27q1e2esvkqjpvectttxf). Returns undefined for a cancelled retry —
259
+ // there is no review to open. Passes denialCancelSettled so a first denial
260
+ // that stopped the RETRY waits for run.cancel() before the ledger read,
261
+ // exactly like the primary path.
262
+ const settleRetryTurn = async (retryResultStatus: string): Promise<TurnBoundaryResult | undefined> =>
263
+ retryResultStatus === "cancelled"
264
+ ? undefined
265
+ : runBoundary(streamState.firstDenialDetected ? streamState.denialCancelSettled : undefined);
266
+
267
+ // The shared recovery spine. A fresh agent runs the IDENTICAL stream loop,
268
+ // finalize, and stop check as the primary turn, then — on a normal
269
+ // completion or a first denial — waits and re-enters the boundary. The two
270
+ // recovery call sites below differ only in how they build the fresh
271
+ // agent/prompt and how they classify a retry ERROR.
272
+ type RecoveryOutcome =
273
+ | { proceeded: false; outcome: TurnOutcome }
274
+ | {
275
+ proceeded: true;
276
+ retryRun: Run;
277
+ retryResult: Awaited<ReturnType<Run["wait"]>>;
278
+ retryBoundary: TurnBoundaryResult | undefined;
279
+ };
280
+ const runRecoveryStream = async (freshAgent: typeof engine.resolution.agent, retryPrompt: string): Promise<RecoveryOutcome> => {
281
+ // The fresh agent is now the live handle: point the resolution at it so
282
+ // the adapter's disposition parks or closes THIS agent rather than the
283
+ // disposed one it replaced.
284
+ engine.resolution = { ...engine.resolution, agent: freshAgent, agentId: freshAgent.agentId, isNew: true };
285
+ streamState.streamErrorMessage = undefined;
286
+ // The retry always carries the turn's full image payload — never the
287
+ // primary send's HITL-trimmed set: the fresh agent's conversation is
288
+ // empty, so skipping them here would silently lose the user's photo on
289
+ // a recovered turn (issue #366's vision corollary).
290
+ const retryRun = await freshAgent.send(prompt.toSendMessage(retryPrompt, prompt.turnImages), {
291
+ onDelta: makeCursorTurnOnDelta(onDeltaDeps),
292
+ });
293
+ sink.recordActivity();
294
+ const reason = await consumeCursorTurnStream(retryRun, streamDeps);
295
+ await finalizeStreamPhase();
296
+ if (reason === "interrupted") return { proceeded: false, outcome: { kind: "interrupted" } };
297
+ const retryResult = await retryRun.wait();
298
+ sink.recordActivity();
299
+ console.log(`ExecuteCursor retry run.wait(): execution=${executionId}, retryResult=${JSON.stringify(retryResult)}`);
300
+ const retryBoundary = await settleRetryTurn(retryResult.status);
301
+ return { proceeded: true, retryRun, retryResult, retryBoundary };
302
+ };
303
+
304
+ // ── The primary stream ────────────────────────────────────────────────────
305
+
306
+ const reason = await consumeCursorTurnStream(run, streamDeps);
307
+ await finalizeStreamPhase();
308
+ if (reason === "interrupted") return { kind: "interrupted" };
309
+
310
+ // The denial-settle wait applies only when a first denial stopped THIS run;
311
+ // a normal completion passes no promise.
312
+ const boundary = await runBoundary(streamState.firstDenialDetected ? streamState.denialCancelSettled : undefined);
313
+ sink.recordActivity();
314
+ if (boundary.waiting) {
315
+ // A pausing turn is never silent, so an unattributed block alongside our
316
+ // own gate only warns (logged by the boundary) — the pause wins.
317
+ return awaitingApproval(boundary);
318
+ }
319
+ if (boundary.unattributedHookBlocks.length > 0) {
320
+ return unattributedHookBlock(boundary);
321
+ }
322
+
323
+ // ── run.wait() ────────────────────────────────────────────────────────────
324
+
325
+ const result = await run.wait();
326
+ sink.recordActivity();
327
+ console.log(`ExecuteCursor run.wait() result: execution=${executionId}, result=${JSON.stringify(result)}`);
328
+ // Echo sanity check only: result.model ECHOES the requested selection —
329
+ // the SDK never reports the variant that actually served the call
330
+ // (verified against the billing ledger, #357). A mismatch here means the
331
+ // SDK rewrote our selection (contract change), not variant drift; the
332
+ // authoritative requested-vs-billed reconciliation is the cloud billing
333
+ // handler's pricing_variant mismatch metric.
334
+ const echoedSelection = result.model;
335
+ if (echoedSelection) {
336
+ const idMatches = echoedSelection.id === engine.validatedModel;
337
+ // Compare id/value pairs explicitly, never serialized objects: the SDK
338
+ // may add fields to ModelParameterValue or reorder keys, and neither
339
+ // is contract drift.
340
+ const echoedParams = [...(echoedSelection.params ?? [])].sort((a, b) => a.id.localeCompare(b.id));
341
+ const paramsMatch =
342
+ echoedParams.length === engine.modelParams.length &&
343
+ echoedParams.every((p, i) => p.id === engine.modelParams[i].id && p.value === engine.modelParams[i].value);
344
+ if (!idMatches || !paramsMatch) {
345
+ console.warn(
346
+ `ExecuteCursor model selection echo mismatch (SDK contract drift?): ` +
347
+ `execution=${executionId}, ` +
348
+ `requested=${JSON.stringify({ id: engine.validatedModel, params: engine.modelParams })}, ` +
349
+ `echoed=${JSON.stringify(echoedSelection)}`,
350
+ );
351
+ }
352
+ }
353
+
354
+ let outcome: TurnOutcome;
355
+ switch (result.status) {
356
+ case "finished":
357
+ outcome = { kind: "completed" };
358
+ break;
359
+ case "cancelled":
360
+ outcome = { kind: "cancelled" };
361
+ break;
362
+ case "error": {
363
+ // Shape-aware extraction, NOT String(): the result's error fields are
364
+ // structured at runtime often enough that a bare coercion showed users
365
+ // "[object Object]" and shadowed every fallback source below (oss#299).
366
+ const runErrorSources = extractRunErrorSources(result);
367
+
368
+ // The SDK frequently resolves run.wait() to a bare { status: "error" }
369
+ // while the real reason (e.g. the original grpc-status 12 routing
370
+ // failure) lives on the failing conversation turn. Capture it here so
371
+ // the classified error is actionable instead of "no detail from SDK".
372
+ const conversationErrorText = await introspectConversation(run, executionId);
373
+
374
+ const capturedRejection = getCapturedRejection(executionId);
375
+ if (capturedRejection) clearCapturedRejection(executionId);
376
+
377
+ const classified = synthesizeError({
378
+ sdkError: runErrorSources.sdkError,
379
+ sdkResultFields: runErrorSources.sdkResultFields,
380
+ streamErrorMessage: streamState.streamErrorMessage,
381
+ capturedRejection,
382
+ conversationErrorText,
383
+ isResumedHandle: engine.resolution.reason === "resumed_successfully",
384
+ fallbackContext: { model: engine.validatedModel, mode: engine.agentMode, agentId: engine.resolution.agentId },
385
+ durationMs: (result as unknown as Record<string, unknown>).durationMs as number | undefined,
386
+ messageCount: status.messages.length,
387
+ proxyMode: !!config.proxyEndpoint,
388
+ });
389
+
390
+ console.error(
391
+ `ExecuteCursor agent error: execution=${executionId}, ` +
392
+ `classified=${JSON.stringify(classified)}, rawResult=${JSON.stringify(result)}`,
393
+ );
394
+
395
+ if (shouldRetryWithFreshAgent(classified) && engine.resolution.reason === "resumed_successfully" && !alreadyRetriedWithFreshAgent) {
396
+ alreadyRetriedWithFreshAgent = true;
397
+ console.warn(
398
+ `ExecuteCursor poisoned-handle recovery: execution=${executionId}, ` +
399
+ `disposing agent ${engine.resolution.agentId} and creating fresh agent`,
400
+ );
401
+
402
+ try { engine.resolution.agent.close(); } catch { /* best effort */ }
403
+
404
+ const freshAgent = await createFreshAgent(engine);
405
+ const freshPrompt = await prompt.buildRecoveryPrompt({
406
+ ...engine.resolution,
407
+ agent: freshAgent,
408
+ agentId: freshAgent.agentId,
409
+ isNew: true,
410
+ resumed: false,
411
+ reason: "created_after_resume_failure",
412
+ resumeFailureDetail: `poisoned-handle recovery: ${classified.message}`,
413
+ });
414
+ console.log(`ExecuteCursor retry with fresh agent: execution=${executionId}, newAgentId=${freshAgent.agentId}`);
415
+ await sink.bindHarnessState(freshAgent.agentId);
416
+
417
+ const recovery = await runRecoveryStream(freshAgent, freshPrompt);
418
+ if (!recovery.proceeded) return recovery.outcome;
419
+
420
+ const { retryRun, retryResult, retryBoundary } = recovery;
421
+ if (retryBoundary?.waiting) {
422
+ // The retry's edits/denials armed the gate — pause for review. On a
423
+ // retry error this supersedes the failure, exactly as on the primary
424
+ // path (a captured change pauses the turn before run.wait() is
425
+ // consulted).
426
+ console.log(`ExecuteCursor poisoned-handle recovery paused for review: execution=${executionId}`);
427
+ return awaitingApproval(retryBoundary);
428
+ }
429
+ if (retryBoundary && retryBoundary.unattributedHookBlocks.length > 0) {
430
+ return unattributedHookBlock(retryBoundary);
431
+ }
432
+
433
+ if (retryResult.status === "finished") {
434
+ console.log(`ExecuteCursor poisoned-handle recovery SUCCEEDED: execution=${executionId}`);
435
+ outcome = { kind: "completed" };
436
+ break;
437
+ }
438
+ if (retryResult.status === "cancelled") {
439
+ outcome = { kind: "cancelled" };
440
+ break;
441
+ }
442
+
443
+ const retryRejection = getCapturedRejection(executionId);
444
+ if (retryRejection) clearCapturedRejection(executionId);
445
+ const retryConversationErrorText = await introspectConversation(retryRun, executionId);
446
+ // Same shape-aware extraction as the primary error arm — the retry
447
+ // previously String()-coerced result.result alone, so a structured
448
+ // retry failure both read "[object Object]" and ignored the
449
+ // error/message/reason fields the primary arm consults.
450
+ const retryErrorSources = extractRunErrorSources(retryResult);
451
+ const retryClassified = synthesizeError({
452
+ sdkError: retryErrorSources.sdkError,
453
+ sdkResultFields: retryErrorSources.sdkResultFields,
454
+ streamErrorMessage: streamState.streamErrorMessage,
455
+ capturedRejection: retryRejection,
456
+ conversationErrorText: retryConversationErrorText,
457
+ isResumedHandle: false,
458
+ fallbackContext: { model: engine.validatedModel, mode: engine.agentMode, agentId: freshAgent.agentId },
459
+ proxyMode: !!config.proxyEndpoint,
460
+ });
461
+ const message = formatClassifiedError(retryClassified);
462
+ console.error(`ExecuteCursor poisoned-handle recovery FAILED: execution=${executionId}, retryError=${message}`);
463
+ outcome = { kind: "failed", surface: "engine", message };
464
+ break;
465
+ }
466
+
467
+ // Transport-timeout retry: fresh agent got 0 messages (degraded h2 session).
468
+ // Reset proxy sessions and try once with a new connection.
469
+ if (classified.category === "network" && classified.retryable && engine.resolution.reason !== "resumed_successfully" && !alreadyRetriedWithFreshAgent) {
470
+ alreadyRetriedWithFreshAgent = true;
471
+ console.warn(
472
+ `ExecuteCursor transport-timeout recovery: execution=${executionId}, ` +
473
+ `resetting proxy sessions and retrying with fresh agent`,
474
+ );
475
+
476
+ try { engine.resolution.agent.close(); } catch { /* best effort */ }
477
+ closeProxySessions();
478
+
479
+ const freshAgent = await createFreshAgent(engine);
480
+ await sink.bindHarnessState(freshAgent.agentId);
481
+
482
+ const recovery = await runRecoveryStream(freshAgent, prompt.effectivePrompt);
483
+ if (!recovery.proceeded) return recovery.outcome;
484
+
485
+ const { retryResult, retryBoundary } = recovery;
486
+ if (retryBoundary?.waiting) {
487
+ // The retry's edits/denials armed the gate — pause for review (see
488
+ // the poisoned-handle branch above for the precedence rationale).
489
+ console.log(`ExecuteCursor transport-timeout recovery paused for review: execution=${executionId}`);
490
+ return awaitingApproval(retryBoundary);
491
+ }
492
+ if (retryBoundary && retryBoundary.unattributedHookBlocks.length > 0) {
493
+ return unattributedHookBlock(retryBoundary);
494
+ }
495
+
496
+ if (retryResult.status === "finished") {
497
+ outcome = { kind: "completed" };
498
+ break;
499
+ }
500
+ outcome = { kind: "failed", surface: "engine", message: `Transport recovery failed: ${formatClassifiedError(classified)}` };
501
+ break;
502
+ }
503
+
504
+ outcome = { kind: "failed", surface: "engine", message: formatClassifiedError(classified) };
505
+ break;
506
+ }
507
+ default:
508
+ outcome = { kind: "completed" };
509
+ }
510
+
511
+ // Collapse any redundant same-identity tool-call twin born this turn before
512
+ // the terminal persist. On a resume turn the gated tool is already granted, so
513
+ // there is no denial ledger and reconcileDeniedToolCalls never runs — the
514
+ // extra attempt the model emits beside the approved action (a stuck RUNNING
515
+ // zombie, a denied-reported-as-success COMPLETED, or an all-no-change double)
516
+ // would otherwise persist as a second "No preview available" card. The shared
517
+ // routine keeps the diff/output carrier and blanks the rest to hidden SKIPPED
518
+ // rows in place, preserving each committed id so the finalize stays append-only.
519
+ const collapsedTwins = collapseRedundantToolCallTwins(status.messages);
520
+ if (collapsedTwins > 0) {
521
+ console.log(
522
+ `ExecuteCursor collapsed ${collapsedTwins} redundant tool-call twin(s) at ` +
523
+ `terminal finalize (kept in place as hidden SKIPPED rows): execution=${executionId}`,
524
+ );
525
+ }
526
+
527
+ return outcome;
528
+ }
529
+
530
+ /**
531
+ * Best-effort: read the failing run's conversation to recover the real error
532
+ * reason the SDK swallowed in run.wait(). Logs the (bounded) raw turns for deep
533
+ * diagnostics and returns a concise error string for the classifier.
534
+ *
535
+ * Strictly non-fatal — any failure (unsupported operation, transport error)
536
+ * returns undefined and never propagates into the execution's error path.
537
+ */
538
+ async function introspectConversation(run: Run, executionId: string): Promise<string | undefined> {
539
+ try {
540
+ if (!run.supports("conversation")) {
541
+ console.log(
542
+ `ExecuteCursor conversation introspection unsupported: execution=${executionId}, ` +
543
+ `reason=${run.unsupportedReason("conversation") ?? "n/a"}`,
544
+ );
545
+ return undefined;
546
+ }
547
+ const turns = await run.conversation();
548
+ const raw = JSON.stringify(turns);
549
+ const bounded = raw.length > 8000 ? `${raw.slice(0, 8000)}…(truncated ${raw.length} chars)` : raw;
550
+ console.error(`ExecuteCursor conversation introspection: execution=${executionId}, turns=${turns.length}, raw=${bounded}`);
551
+ return extractConversationErrorText(turns);
552
+ } catch (introspectErr) {
553
+ console.warn(
554
+ `ExecuteCursor conversation introspection failed (non-fatal): execution=${executionId}, ` +
555
+ `error=${introspectErr instanceof Error ? introspectErr.message : String(introspectErr)}`,
556
+ );
557
+ return undefined;
558
+ }
559
+ }
560
+
561
+ /**
562
+ * Walk the last conversation turn and collect human-meaningful error text
563
+ * (error-status payloads and `text`/`message`/`reason` strings). Schema-agnostic
564
+ * by design so it tolerates SDK conversation-shape changes. Returns undefined
565
+ * when nothing useful is found.
566
+ */
567
+ function extractConversationErrorText(turns: ConversationTurn[]): string | undefined {
568
+ if (!turns || turns.length === 0) return undefined;
569
+ const collected: string[] = [];
570
+
571
+ const visit = (node: unknown, depth: number): void => {
572
+ if (node == null || depth > 6 || typeof node !== "object") return;
573
+ if (Array.isArray(node)) {
574
+ for (const item of node) visit(item, depth + 1);
575
+ return;
576
+ }
577
+ const obj = node as Record<string, unknown>;
578
+ if (obj.status === "error" && obj.error != null) {
579
+ collected.push(typeof obj.error === "string" ? obj.error : JSON.stringify(obj.error));
580
+ }
581
+ for (const [key, value] of Object.entries(obj)) {
582
+ if ((key === "text" || key === "message" || key === "reason") && typeof value === "string" && value.trim().length > 0) {
583
+ collected.push(value.trim());
584
+ } else if (typeof value === "object" && value != null) {
585
+ visit(value, depth + 1);
586
+ }
587
+ }
588
+ };
589
+
590
+ visit(turns[turns.length - 1], 0);
591
+ if (collected.length === 0) return undefined;
592
+
593
+ const joined = [...new Set(collected)].join(" | ");
594
+ return joined.length > 600 ? `${joined.slice(0, 600)}…` : joined;
595
+ }
596
+
597
+ /** The `streaming_usage` carrier the OTel span end reads; the runtime keeps it current on every write. */
598
+ export function usageSnapshotFor(sink: TurnSink): { inputTokens: bigint; outputTokens: bigint } {
599
+ const summary = sink.status.streamingUsage ?? create(StreamingUsageSummarySchema, {});
600
+ return { inputTokens: summary.inputTokens, outputTokens: summary.outputTokens };
601
+ }