@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,645 @@
1
+ /**
2
+ * The Cursor harness's setup — what this engine needs before it can be sent
3
+ * a turn, as small named steps over the runtime's resolved `TurnInput`, each
4
+ * returning a typed slice (the `SetupResult` mold, never one long function).
5
+ *
6
+ * Everything harness-agnostic has already happened when these run: the
7
+ * execution, session and blueprint are fetched, the workspace is provisioned
8
+ * and locked, the MCP servers and policies are merged, the skills are mounted,
9
+ * the attachments are resolved. What remains is Cursor's own reading of that
10
+ * record: the cursor mode; the row facts beside the runtime's approval
11
+ * verdicts; the SDK's MCP config; the prompt-shaped view of the attachments;
12
+ * the HITL gate (hook script, approval state, grants, denial watcher,
13
+ * capture baseline, progress substrate); the catalog validation of the
14
+ * requested model; the credential, the sub-agents, the variant params and
15
+ * the agent itself (parked or resolved); the bind of a new agent's id through
16
+ * the sink; the prompt in one of its four shapes; the vision payload; the
17
+ * pricer; the OTel span.
18
+ *
19
+ * Every step reads `TurnInput` and the sink and nothing else of the runtime's.
20
+ * The `execution_setup` cold-start line is emitted here, once the agent is
21
+ * resolved, over the runtime's timeline (`sink.setupTiming`), so the one
22
+ * timeline reads end to end.
23
+ *
24
+ * Moved from `index.ts` `executeCursorInner` phases 4c to 10c at S2 M3b; the
25
+ * step bodies are the orchestrator's, verbatim where nothing changed.
26
+ */
27
+
28
+ import type { SDKUserMessage } from "@cursor/sdk";
29
+ import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
30
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
31
+ import { InteractionMode } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
32
+ import { CursorMode } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
33
+
34
+ import type { Config } from "../../config.js";
35
+ import type { TurnInput, TurnSink } from "../../harness/types.js";
36
+ import { isReinvocation } from "../../harness/turn-context.js";
37
+ import { toCursorImages } from "../../shared/attachment-vision.js";
38
+ import { emitTimingLog } from "../../shared/cold-start-timing.js";
39
+ import { deriveExecutionFingerprintKey } from "../../shared/approval-fingerprint.js";
40
+ import { isUnattendedApprovalMode } from "../../shared/approval-policy.js";
41
+ import { excludeAppliedFromGrants } from "../../shared/exact-apply.js";
42
+ import { getRunnerHitlMasterSecret } from "../../shared/fingerprint-secret.js";
43
+ import { newProgressCaptureState, type ProgressCaptureState, type ProgressSubstrate } from "../../shared/filereview/progress.js";
44
+ import { enabledToolsBySlug } from "../../shared/mcp-enabled-tools.js";
45
+ import { ensureHitlDir } from "../../shared/workspace/platform-dir.js";
46
+ import { computeAgentFingerprint, takeCachedAgent } from "./agent-session-cache.js";
47
+ import { buildApprovalGrants, buildApprovalState, emitCursorGrantReceipts, reconstructAdjudicatedApprovals, watchDenialLedger, type ApprovalGrant } from "./approval-state.js";
48
+ import { buildCursorProgressSubstrate, captureBaselineToLedger } from "./capture-flow.js";
49
+ import { CURSOR_CAPABILITIES } from "./cursor-capabilities.js";
50
+ import { toCursorMcpConfig, validateMcpServerEnv } from "./cursor-mcp-config.js";
51
+ import { determineCursorMode, isCloudMode } from "./cursor-mode.js";
52
+ import { closeProxySessions } from "./http2-interceptor.js";
53
+ import { seededSubAgentsOf } from "./message-translator.js";
54
+ import { ensureLoaded as ensurePricingLoaded, resolveModelId } from "./model-pricing.js";
55
+ import {
56
+ appendStructuredOutputDirective,
57
+ buildPrompt,
58
+ primarySendCarriesImages,
59
+ promptCarriesStandingContext,
60
+ type AttachmentPromptEntry,
61
+ type BuildPromptInput,
62
+ type VisionPromptInfo,
63
+ } from "./prompt-builder.js";
64
+ import { resolveServiceTierParams } from "./service-tier.js";
65
+ import {
66
+ createAgent,
67
+ createCloudAgent,
68
+ resolveAgentWithTransportRecovery,
69
+ type AgentResolution,
70
+ type CreateAgentOptions,
71
+ type CreateCloudAgentOptions,
72
+ } from "./session-lifecycle.js";
73
+ import { composeTurnRecoveryDigest } from "./turn-recovery.js";
74
+ import type { TurnStreamState } from "./turn-stream.js";
75
+ import { buildCursorSubAgentDefinitions } from "./subagent-config.js";
76
+ import { CursorUsagePricer } from "./usage-pricing.js";
77
+ import { installHitlGate, removeHitlGate, type HitlGateHandle } from "./workspace-setup.js";
78
+
79
+ /**
80
+ * The runner config this harness reads per turn, as a named slice
81
+ * (`shared/workspace/session-provision.ts` `SessionProvisionConfig` is the
82
+ * idiom): a whole `Config` satisfies it, a test constructs these fields and
83
+ * nothing else.
84
+ */
85
+ export type CursorAdapterConfig = Pick<
86
+ Config,
87
+ "proxyEndpoint" | "cursorApiKey" | "stigmerToken" | "stigmerTokenRef" | "workspaceRootDir" | "cloudModeEnabled" | "agentResolveTimeoutMs"
88
+ >;
89
+
90
+ export type CursorAgentMode = "cloud" | "local";
91
+
92
+ /** The row facts the runtime's approval verdicts sit beside (the contract's rule: the adapter reads the ROW for anything else it needs). */
93
+ export interface AdjudicatedRows {
94
+ /** Create-vs-resume, derived from the state id exactly as the runtime derives it. */
95
+ readonly isReinvocation: boolean;
96
+ /** The sub-agent rows the accumulator re-registers on a resume. */
97
+ readonly seededSubAgents: readonly SubAgentExecution[];
98
+ /** The pending-approval protos the grant builder and the reinvocation prompt render. */
99
+ readonly adjudicatedApprovals: PendingApproval[];
100
+ /** The content digest that authorizes an approved edit by its exact bytes (a sibling edit to the same file re-gates). */
101
+ readonly adjudicatedContentDigests: Map<string, string>;
102
+ }
103
+
104
+ /** The HITL gate as installed for this turn, and what the stream and the boundary read from it. */
105
+ export interface CursorGate {
106
+ /** Session HITL directory (runner-owned, outside the workspace): hook script, approval-state file, denial ledger. */
107
+ readonly hitlDir: string;
108
+ readonly hitlGate: HitlGateHandle;
109
+ readonly approvalGrants: ApprovalGrant[] | undefined;
110
+ /** The pre-turn tree in capture mode, pinned before the agent runs; the boundary diffs against it. */
111
+ readonly baselineTree: string | undefined;
112
+ readonly progressSubstrate: ProgressSubstrate | undefined;
113
+ readonly progressState: ProgressCaptureState;
114
+ /** Closes the denial-ledger watcher; idempotent. */
115
+ readonly stopDenialWatcher: () => void;
116
+ /** Restores the workspace's `.cursor/hooks.json` (issue #173); the runtime removes the `.stigmer` link after it. */
117
+ readonly removeGate: () => Promise<void>;
118
+ }
119
+
120
+ /** The engine this turn runs on: how it was resolved and everything a recovery needs to resolve it again. */
121
+ export interface CursorEngine {
122
+ readonly agentMode: CursorAgentMode;
123
+ readonly cursorMode: CursorMode;
124
+ readonly requestedModel: string;
125
+ readonly validatedModel: string;
126
+ readonly effectiveApiKey: string;
127
+ readonly createOptions: CreateAgentOptions | CreateCloudAgentOptions;
128
+ readonly agentFingerprint: string;
129
+ /** Replaced by a recovery spine when a fresh agent takes over the turn. */
130
+ resolution: AgentResolution;
131
+ readonly modelParams: Awaited<ReturnType<typeof resolveServiceTierParams>>;
132
+ readonly usagePricer: CursorUsagePricer;
133
+ }
134
+
135
+ /** The prompt this turn sends, and the facts every send site (primary and both recoveries) needs. */
136
+ export interface CursorPrompt {
137
+ readonly effectivePrompt: string;
138
+ /** The turn's full vision payload; a recovery send always carries it (issue #366's vision corollary). */
139
+ readonly turnImages: ReturnType<typeof toCursorImages>;
140
+ /** What the PRIMARY send carries: nothing on a HITL re-invocation of a resumed agent, whose conversation holds the images already. */
141
+ readonly primarySendImages: ReturnType<typeof toCursorImages>;
142
+ readonly toSendMessage: (prompt: string, images: { data: string; mimeType: string }[]) => string | SDKUserMessage;
143
+ readonly promptEstimatedTokens: number;
144
+ /** The fresh-agent recovery prompt, composed at recovery time from the transcript as it then stands. */
145
+ readonly buildRecoveryPrompt: (fresh: AgentResolution) => Promise<string>;
146
+ }
147
+
148
+ // ── Steps ───────────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Cloud Cursor agents are disabled platform-wide (see determineCursorMode),
152
+ * so every session runs LOCAL. Any persisted cursor_mode is deliberately
153
+ * ignored so a session can never route to the cloud path while it is
154
+ * disabled — even one that was created when cloud was enabled.
155
+ */
156
+ export function resolveCursorMode(input: TurnInput, config: CursorAdapterConfig): { cursorMode: CursorMode; agentMode: CursorAgentMode } {
157
+ const cursorMode = determineCursorMode(input.blueprint.sessionSpec.workspaceEntries, config.cloudModeEnabled);
158
+ return { cursorMode, agentMode: isCloudMode(cursorMode) ? "cloud" : "local" };
159
+ }
160
+
161
+ /** The adapter's own read of the adjudicated rows the runtime already turned into verdicts. */
162
+ export function readAdjudicatedRows(input: TurnInput): AdjudicatedRows {
163
+ const reinvoked = isReinvocation(CURSOR_CAPABILITIES.stateIdSource, input);
164
+ const adjudicated = reinvoked ? reconstructAdjudicatedApprovals(input.execution.status?.messages ?? []) : undefined;
165
+ return {
166
+ isReinvocation: reinvoked,
167
+ seededSubAgents: seededSubAgentsOf(input.execution),
168
+ adjudicatedApprovals: adjudicated?.pendingApprovals ?? [],
169
+ adjudicatedContentDigests: adjudicated?.contentDigests ?? new Map(),
170
+ };
171
+ }
172
+
173
+ /**
174
+ * The Cursor SDK config, projected from the runtime's final server list
175
+ * exactly once, plus the MCP env pre-flight (diagnostic, non-blocking).
176
+ */
177
+ export function projectMcpConfig(input: TurnInput): ReturnType<typeof toCursorMcpConfig> {
178
+ const mcpWarnings = validateMcpServerEnv(input.mcp.servers, input.blueprint.mergedMcpServerUsages);
179
+ if (mcpWarnings.length > 0) {
180
+ console.warn(
181
+ `ExecuteCursor MCP pre-flight warnings: execution=${input.executionId}\n` +
182
+ mcpWarnings.map((w) => ` - ${w}`).join("\n"),
183
+ );
184
+ }
185
+ return toCursorMcpConfig(input.mcp.servers);
186
+ }
187
+
188
+ /** The prompt-shaped projections of the runtime's attachments: the `<input_files>` entries and the vision disclosure. */
189
+ export function prepareAttachmentPrompt(input: TurnInput): { attachmentEntries: AttachmentPromptEntry[]; visionPromptInfo: VisionPromptInfo | undefined } {
190
+ const { visionImages, visionNotViewable } = input.attachments;
191
+ const attachmentEntries = input.attachments.results.map((a) => ({
192
+ path: a.relativePath,
193
+ ...(a.renamedFrom !== undefined ? { renamedFrom: a.renamedFrom } : {}),
194
+ ...(a.downloadUrl !== undefined ? { downloadUrl: a.downloadUrl } : {}),
195
+ }));
196
+ const visionPromptInfo =
197
+ visionImages.length > 0 || visionNotViewable.length > 0
198
+ ? { inlineFilenames: visionImages.map((v) => v.filename), notViewable: visionNotViewable }
199
+ : undefined;
200
+ return { attachmentEntries, visionPromptInfo };
201
+ }
202
+
203
+ /**
204
+ * Install the HITL approval gate BEFORE resolving the agent.
205
+ *
206
+ * The gate's runtime artifacts (hook script, approval-state file, denial
207
+ * ledger) live in the session HITL directory OUTSIDE the workspace; only a
208
+ * minimal, merged, transient .cursor/hooks.json is written into the repo,
209
+ * pointing at the hook script by absolute path. The hook is scoped to this
210
+ * runner's own process so the user's interactive IDE — sharing the same repo
211
+ * hooks.json — is never gated (issue #173). Installing here (rather than
212
+ * after agent create/resume) guarantees the hook is present no matter when
213
+ * the SDK reads hook config, and the adapter's finally restores the repo.
214
+ *
215
+ * On reinvocation, turn the user's approvals into tool-identity grants so
216
+ * the resumed agent's re-attempt (which carries a fresh tool-call id) is
217
+ * allowed through. Exact-applied writes are EXCLUDED from the grants: with no
218
+ * grant, a further write to that file is re-gated (the user sees every change).
219
+ * Capture mode: pin the pre-turn baseline tree before the agent runs (and
220
+ * before the gate is installed, though the gate files are excluded from the
221
+ * capture anyway). The turn-end capture diffs the post-turn tree against this
222
+ * to build the per-file cards; the baseline ref is also what a reject reverts
223
+ * to on resume. Covers a fresh turn and the approved-irreversible resume
224
+ * fall-through (the agent will run and may make further edits).
225
+ */
226
+ export async function installGate(input: TurnInput, sink: TurnSink, rows: AdjudicatedRows, streamState: TurnStreamState): Promise<CursorGate> {
227
+ const { executionId, sessionId, workspace, mcp, approvalDecisions, appliedToolCallIds, artifactStorage } = input;
228
+ const { primaryDir, gitWorkspace, captureMode, changeSetId } = workspace;
229
+ const globalBypass = mcp.leases.global;
230
+
231
+ let baselineTree: string | undefined;
232
+ if (captureMode && primaryDir) {
233
+ // Pin the pre-turn tree AND author BASELINE_CAPTURED so the projection can
234
+ // materialize the change set (status CAPTURING) before any candidate exists.
235
+ // The event rides the next persist; CAPTURING does not arm the unified gate.
236
+ baselineTree = await captureBaselineToLedger({ status: sink.status, gitRoot: primaryDir, executionId, changeSetId, gitWorkspace });
237
+ }
238
+
239
+ const hitlDir = await ensureHitlDir(sessionId);
240
+ const grantApprovals = excludeAppliedFromGrants(rows.adjudicatedApprovals, appliedToolCallIds);
241
+ const approvalGrants = approvalDecisions.size > 0
242
+ ? buildApprovalGrants(grantApprovals, approvalDecisions, rows.adjudicatedContentDigests)
243
+ : undefined;
244
+ if (approvalGrants && approvalGrants.length > 0 && !globalBypass) {
245
+ emitCursorGrantReceipts(approvalGrants, deriveExecutionFingerprintKey(getRunnerHitlMasterSecret(), executionId), executionId);
246
+ }
247
+ // CAS capture requires artifact storage to persist blobs
248
+ // (captureCandidateToLedger throws without it). In a git tree, captureMode
249
+ // alone governs tracked-file capture (no storage needed) and captureIgnored is
250
+ // the narrower switch (git tree + storage) that also captures gitignored
251
+ // writes. In a non-git workspace ALL capture is CAS, so captureMode already
252
+ // required storage — captureIgnored then equals captureMode. When storage is
253
+ // absent a git tree keeps gating gitignored writes and a non-git workspace
254
+ // falls back to the deny-gate entirely (no regression).
255
+ const captureIgnored = captureMode && !!artifactStorage;
256
+ // Unattended approval mode (DD-014): approver-less surfaces (channels,
257
+ // guest shares) stamp APPROVAL_MODE_UNATTENDED; the hook then records
258
+ // approval denials with the non-pausing "unattended" kind, so the
259
+ // first-denial stop never fires and the turn boundary settles the denied
260
+ // calls as SKIPPED instead of pausing a turn nobody can approve.
261
+ const approvalState = buildApprovalState(
262
+ mcp.policies,
263
+ globalBypass,
264
+ mcp.leases.categories,
265
+ approvalGrants,
266
+ captureMode,
267
+ captureIgnored,
268
+ gitWorkspace,
269
+ isUnattendedApprovalMode(input.execution),
270
+ // The enabled_tools capability manifest (issue #350): restricted
271
+ // servers' allow-lists, enforced by the hook's "disabled" arm ahead of
272
+ // every approval bypass. The Cursor SDK config cannot hide a server's
273
+ // tools, so this deny-at-call is the harness's enforcement.
274
+ enabledToolsBySlug(mcp.servers),
275
+ );
276
+ const hitlGate = await installHitlGate({ workspaceRoot: primaryDir, hitlDir, approvalState, runnerPid: process.pid });
277
+ // Issue #205 diagnosability: the merge preserved the user's own hooks on
278
+ // the gating events, and Cursor runs every configured hook — so any of
279
+ // these can deny this turn's tools without writing our denial ledger. Log
280
+ // the exposure up front; the turn boundary uses the same list to name the
281
+ // likely culprit if it detects an unattributed hook block.
282
+ if (hitlGate.foreignGatingHooks.length > 0) {
283
+ console.warn(
284
+ `ExecuteCursor: workspace hooks.json carries ${hitlGate.foreignGatingHooks.length} ` +
285
+ `foreign gating hook(s) [${hitlGate.foreignGatingHooks.join(", ")}] — a deny from ` +
286
+ `any of them blocks the runner's tools outside Stigmer's approval flow ` +
287
+ `(execution=${executionId})`,
288
+ );
289
+ }
290
+ // Arm the denial watcher as soon as the gate exists. The per-turn ledger
291
+ // reset may flip the flag once before the run starts; the loop's read then
292
+ // sees an empty ledger and clears it — harmless by construction.
293
+ const stopDenialWatcher = watchDenialLedger(hitlDir, () => {
294
+ streamState.denialLedgerDirty = true;
295
+ });
296
+ sink.setupTiming.mark("install_hitl_gate");
297
+
298
+ // Mid-run live capture (DD-32 / DD-33): choose the progress substrate for this
299
+ // turn's workspace shape ONCE (git / non-git CAS / hybrid). It owns its own
300
+ // short-circuit cache across the loop's persists; the floor lives in
301
+ // progressState. Undefined outside capture mode — writes are deny-gated and
302
+ // nothing is captured.
303
+ const progressSubstrate = buildCursorProgressSubstrate({
304
+ captureMode,
305
+ gitWorkspace,
306
+ workspaceRoot: primaryDir,
307
+ baselineTree,
308
+ executionId,
309
+ hitlDir,
310
+ storage: artifactStorage,
311
+ });
312
+
313
+ return {
314
+ hitlDir,
315
+ hitlGate,
316
+ approvalGrants,
317
+ baselineTree,
318
+ progressSubstrate,
319
+ progressState: newProgressCaptureState(),
320
+ stopDenialWatcher,
321
+ removeGate: () => removeHitlGate(hitlGate),
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Resolve the Cursor agent (parked, created, resumed, or created after a
327
+ * resume failure), emit the cold-start line, and bind a NEW agent's id
328
+ * through the sink before anything else runs (Q-S2-11: a rejected bind ends
329
+ * the turn `failed`; the sink rejects and the caller's catch names it).
330
+ *
331
+ * The requested name and the effective tier and thinking mode are the
332
+ * runtime's; validating the NAME against Cursor's catalog is this harness's.
333
+ */
334
+ export async function resolveEngine(input: TurnInput, sink: TurnSink, config: CursorAdapterConfig, mode: { cursorMode: CursorMode; agentMode: CursorAgentMode }): Promise<CursorEngine> {
335
+ const { executionId, sessionId, threadId, blueprint, workspace, session } = input;
336
+ const { cursorMode, agentMode } = mode;
337
+ const mcpConfig = projectMcpConfig(input);
338
+
339
+ // Phase 5d: Ensure model pricing registry is populated before validation
340
+ await ensurePricingLoaded();
341
+ sink.setupTiming.mark("load_pricing");
342
+
343
+ const requestedModel = input.model.requested;
344
+ const validatedModel = resolveModelId(requestedModel);
345
+ if (validatedModel !== requestedModel) {
346
+ console.log(`ExecuteCursor model resolved: execution=${executionId}, requested="${requestedModel}", using="${validatedModel}"`);
347
+ }
348
+
349
+ await sink.reportProgress("Initializing Cursor agent");
350
+
351
+ // In proxy mode, use the stigmer token as the API key — the proxy
352
+ // validates it and injects the real Cursor API key server-side.
353
+ // In direct mode, use the user's own CURSOR_API_KEY.
354
+ const effectiveApiKey = config.proxyEndpoint
355
+ ? (config.stigmerTokenRef?.current ?? config.stigmerToken ?? config.cursorApiKey)
356
+ : config.cursorApiKey;
357
+ if (!effectiveApiKey || effectiveApiKey === "proxy-managed") {
358
+ const source = config.proxyEndpoint ? "proxy (STIGMER_TOKEN)" : "direct (CURSOR_API_KEY)";
359
+ throw new Error(
360
+ `No Cursor API credential available. Mode=${source}, ` +
361
+ `proxyEndpoint=${config.proxyEndpoint ?? "unset"}, ` +
362
+ `hasStigmerToken=${!!config.stigmerToken}, ` +
363
+ `hasTokenRef=${!!config.stigmerTokenRef?.current}`,
364
+ );
365
+ }
366
+
367
+ // Register blueprint sub-agents with the Cursor SDK so the parent can
368
+ // delegate to them by name via the Task tool. Re-supplied on every
369
+ // create/resume (the SDK does not persist agent config across resume).
370
+ const cursorSubAgents = buildCursorSubAgentDefinitions(blueprint.subAgents);
371
+ if (cursorSubAgents) {
372
+ console.log(
373
+ `ExecuteCursor registering ${Object.keys(cursorSubAgents).length} custom sub-agent(s): ` +
374
+ `execution=${executionId}, names=${Object.keys(cursorSubAgents).join(", ")}`,
375
+ );
376
+ }
377
+
378
+ // Translate the tier + thinking mode into the explicit variant params
379
+ // sent with every create/resume. Never a bare { id }: the catalog's
380
+ // default variant is account-influenced and picks the served variant
381
+ // (#357 fast pricing, #772 thinking).
382
+ const modelParams = await resolveServiceTierParams({
383
+ apiKey: effectiveApiKey,
384
+ modelId: validatedModel,
385
+ tier: input.model.serviceTier,
386
+ thinking: input.model.thinkingMode,
387
+ executionId,
388
+ });
389
+
390
+ const createOptions: CreateAgentOptions | CreateCloudAgentOptions = agentMode === "cloud"
391
+ ? {
392
+ apiKey: effectiveApiKey,
393
+ model: validatedModel || undefined,
394
+ modelParams,
395
+ repos: blueprint.cloudRepos,
396
+ sessionId,
397
+ mcpServers: mcpConfig,
398
+ agents: cursorSubAgents,
399
+ }
400
+ : {
401
+ apiKey: effectiveApiKey,
402
+ model: validatedModel,
403
+ modelParams,
404
+ workspaceDirs: [...workspace.dirs],
405
+ sessionId,
406
+ workspaceRootDir: config.workspaceRootDir,
407
+ mcpServers: mcpConfig,
408
+ agents: cursorSubAgents,
409
+ };
410
+
411
+ // Agent.create/Agent.resume have no timeout of their own — a degraded
412
+ // transport (dead proxy connection, stale HTTP/2 session) hangs them
413
+ // forever, which the periodic heartbeat would happily keep alive. Each
414
+ // attempt is bounded; on expiry the wrapper resets the proxy transport
415
+ // and retries once, so a stale-session hang recovers without failing the
416
+ // execution. A second expiry propagates a plain Error to the adapter's
417
+ // catch, which settles the turn `failed`.
418
+ const resolveTimeoutSeconds = Math.round(config.agentResolveTimeoutMs / 1000);
419
+ // Close the segment since load_pricing here so the resolve_agent segment
420
+ // below measures the SDK Agent.create/resume call alone, not the
421
+ // progress-report gRPC + options assembly above (issue #209: resolve_agent
422
+ // is the largest user-visible setup segment; this split keeps its
423
+ // historical meaning — the SDK call was already 98%+ of it).
424
+ sink.setupTiming.mark("prepare_agent");
425
+
426
+ // Reuse the previous turn's agent when this session parked one (#215). A
427
+ // checkout hit skips Agent.resume() AND — the real win — keeps the SDK
428
+ // executor lease alive, so agent.send() re-acquires the warm executor
429
+ // instead of re-spawning every stdio MCP server (the measured 2.2–3.2s
430
+ // `send_returned` tax). The fingerprint covers the full acquisition
431
+ // config, so any drift (rotated credential, edited MCP servers, model
432
+ // change) falls through to a fresh resolve.
433
+ const agentFingerprint = computeAgentFingerprint(createOptions as unknown as Record<string, unknown>);
434
+ const parkedAgent = takeCachedAgent(sessionId, agentFingerprint, threadId ?? "");
435
+ let resolution: AgentResolution;
436
+ if (parkedAgent) {
437
+ console.log(`ExecuteCursor reusing parked session agent: execution=${executionId}, session=${sessionId}, agentId=${parkedAgent.agentId}`);
438
+ resolution = {
439
+ agent: parkedAgent as AgentResolution["agent"],
440
+ agentId: parkedAgent.agentId,
441
+ isNew: false,
442
+ resumed: true,
443
+ mode: agentMode,
444
+ // The parked handle IS the live conversation — every consumer of
445
+ // "resumed_successfully" (prompt selection, poisoned-handle
446
+ // recovery eligibility) wants exactly those semantics.
447
+ reason: "resumed_successfully",
448
+ };
449
+ } else {
450
+ resolution = await resolveAgentWithTransportRecovery({
451
+ harnessStateId: threadId,
452
+ createOptions,
453
+ mode: agentMode,
454
+ timeoutMs: config.agentResolveTimeoutMs,
455
+ buildTimeoutMessage: (finalAttempt) =>
456
+ `Cursor agent ${threadId ? "resume" : "create"} timed out after ${resolveTimeoutSeconds}s ` +
457
+ `(${config.proxyEndpoint ? `via proxy ${config.proxyEndpoint}` : "direct Cursor API connection"}). ` +
458
+ `The transport connection is likely dead. ` +
459
+ (finalAttempt
460
+ ? `An automatic retry on a fresh transport connection also timed out. ` +
461
+ `Retry the message later; if this persists, check proxy and network health.`
462
+ : `Resetting the transport and retrying automatically.`),
463
+ resetTransport: closeProxySessions,
464
+ });
465
+ }
466
+
467
+ console.log(
468
+ `ExecuteCursor agent resolved: execution=${executionId}, ` +
469
+ `reason=${resolution.reason}, mode=${resolution.mode}, ` +
470
+ `agentId=${resolution.agentId}, resumed=${resolution.resumed}` +
471
+ (resolution.resumeFailureDetail ? `, failureDetail=${resolution.resumeFailureDetail}` : ""),
472
+ );
473
+ sink.setupTiming.mark("resolve_agent");
474
+ emitTimingLog("execution_setup", {
475
+ execution_id: executionId,
476
+ session_id: sessionId,
477
+ harness: "cursor",
478
+ agent_resumed: resolution.resumed,
479
+ cursor_mode: agentMode,
480
+ mcp_server_count: blueprint.mergedMcpServerUsages.length,
481
+ skill_count: blueprint.mergedSkillRefs.length,
482
+ workspace_entry_count: session.spec?.workspaceEntries?.length ?? 0,
483
+ }, sink.setupTiming);
484
+
485
+ // A NEW agent's id is bound at once, before anything else runs, so a crash
486
+ // mid-turn still resumes on the next invocation. The mode is this
487
+ // harness's field on the session record, set before the runtime writes it
488
+ // (one writer per field, Q-S2-11); the runtime sets harness_state_id and
489
+ // clears the slug.
490
+ if (resolution.isNew && resolution.agentId) {
491
+ if (blueprint.sessionSpec.cursorMode === CursorMode.UNSPECIFIED) {
492
+ blueprint.sessionSpec.cursorMode = cursorMode;
493
+ }
494
+ try {
495
+ await sink.bindHarnessState(resolution.agentId);
496
+ } catch (bindErr) {
497
+ // The handle this function just created has no owner yet (the adapter
498
+ // receives the engine only when this returns) and its id was never
499
+ // saved, so no later turn can resume it: close it here, or its executor
500
+ // lease and MCP subprocesses outlive the failed turn with nothing to
501
+ // release them (S2 M4, Q-M4-8). Then let the rejection settle the turn
502
+ // `failed` as R3 rules.
503
+ try {
504
+ resolution.agent.close();
505
+ } catch {
506
+ /* best effort */
507
+ }
508
+ throw bindErr;
509
+ }
510
+ console.log(`Stored Cursor agentId=${resolution.agentId} as harness_state_id, cursorMode=${CursorMode[cursorMode]} on session ${sessionId}`);
511
+ }
512
+
513
+ return {
514
+ agentMode,
515
+ cursorMode,
516
+ requestedModel,
517
+ validatedModel,
518
+ effectiveApiKey,
519
+ createOptions,
520
+ agentFingerprint,
521
+ resolution,
522
+ modelParams,
523
+ usagePricer: new CursorUsagePricer(validatedModel, input.model.serviceTier, modelParams),
524
+ };
525
+ }
526
+
527
+ /** Create a fresh agent for a recovery spine (the same options as the primary resolve). */
528
+ export async function createFreshAgent(engine: CursorEngine): Promise<AgentResolution["agent"]> {
529
+ return engine.agentMode === "cloud"
530
+ ? createCloudAgent(engine.createOptions as CreateCloudAgentOptions)
531
+ : createAgent(engine.createOptions as CreateAgentOptions);
532
+ }
533
+
534
+ /**
535
+ * Build the prompt in the shape the resolution calls for (`prompt-builder.ts`
536
+ * `buildPrompt`), with the per-turn directives that ride every send this
537
+ * turn makes (the structured-output contract, the implement-plan directive)
538
+ * and the vision payload policy.
539
+ */
540
+ export async function buildTurnPrompt(input: TurnInput, sink: TurnSink, engine: CursorEngine, rows: AdjudicatedRows): Promise<CursorPrompt> {
541
+ const { executionId, blueprint, standing, attachments, workspace, approvalDecisions, appliedToolCallIds, structuredOutputSchema } = input;
542
+ const spec = input.execution.spec!;
543
+ const { attachmentEntries, visionPromptInfo } = prepareAttachmentPrompt(input);
544
+ const interactionMode = spec.executionConfig?.interactionMode ?? InteractionMode.UNSPECIFIED;
545
+ const buildFromPlan = spec.executionConfig?.buildFromPlan ?? false;
546
+
547
+ const shared = (): Omit<BuildPromptInput, "resolution" | "recalledMemories" | "turnRecoveryDigest"> => ({
548
+ approvalDecisions,
549
+ instructions: blueprint.instructions,
550
+ userMessage: spec.message,
551
+ skills: input.skills,
552
+ channelMessaging: input.mcp.channelMessaging,
553
+ subAgents: blueprint.subAgents,
554
+ workspaceDirs: [...workspace.dirs],
555
+ workspaceFileRefs: spec.workspaceFileRefs ?? [],
556
+ attachments: attachmentEntries,
557
+ vision: visionPromptInfo,
558
+ downloadUrlKind: input.artifactStorage?.downloadUrlKind,
559
+ pendingApprovals: rows.adjudicatedApprovals,
560
+ appliedToolCallIds,
561
+ interactionMode,
562
+ buildFromPlan,
563
+ contextBridge: standing.contextBridge,
564
+ senderIdentity: standing.senderIdentity,
565
+ sessionContext: standing.sessionContext,
566
+ declaredPreferences: standing.declaredPreferences,
567
+ conversationCatchup: standing.conversationCatchup,
568
+ });
569
+
570
+ // The memory selection is the runtime's memoized thunk; whether THIS prompt
571
+ // carries it is decided by how the agent resolved — a successfully resumed
572
+ // agent already holds its standing context. Deliberately NOT decided once
573
+ // here: a mid-send poisoned-handle failure rebuilds on a FRESH agent whose
574
+ // recovery prompt does carry it, so that build site awaits the same thunk.
575
+ const recalledMemories = promptCarriesStandingContext(engine.resolution.reason)
576
+ ? await standing.selectRecalledMemories()
577
+ : undefined;
578
+
579
+ const prompt = buildPrompt({
580
+ ...shared(),
581
+ resolution: engine.resolution,
582
+ recalledMemories,
583
+ // The turn's recorded transcript, seeded from the persisted execution on
584
+ // a reinvocation. Consumed only by the HITL-recovery shape — reached from
585
+ // HERE when the stored handle failed to resume at resolution time (issue
586
+ // #366 crossing 2).
587
+ turnRecoveryDigest: rows.isReinvocation ? composeTurnRecoveryDigest(sink.status.messages) : undefined,
588
+ });
589
+
590
+ // The structured output instruction is a per-turn directive, so like
591
+ // buildFromPlan it must ride every prompt this turn sends — the primary AND
592
+ // the poisoned-handle recovery rebuild (the transport retry re-sends
593
+ // effectivePrompt and inherits it).
594
+ const effectivePrompt = appendStructuredOutputDirective(prompt, structuredOutputSchema);
595
+
596
+ // The turn's vision payload. The invariant is "images accompany the user's
597
+ // turn message, wherever the conversation does not already hold them"
598
+ // (primarySendCarriesImages): the ONLY send that skips them is a HITL
599
+ // re-invocation of a successfully RESUMED agent, whose native conversation
600
+ // carries the images from the original send. Every send that starts an
601
+ // empty conversation re-delivers them — the ordinary first/fresh-agent
602
+ // primary send, the HITL primary send after a resolution-time resume
603
+ // failure, and both mid-send recovery retries (which always run on a fresh
604
+ // agent, so their sites pass turnImages unconditionally). Attachments
605
+ // re-resolve on every invocation, so the bytes are in hand even on a
606
+ // re-invocation.
607
+ const turnImages = toCursorImages(attachments.visionImages);
608
+ const primarySendImages = primarySendCarriesImages(approvalDecisions, engine.resolution.reason) ? turnImages : [];
609
+ const toSendMessage = (sendPrompt: string, images: { data: string; mimeType: string }[]): string | SDKUserMessage =>
610
+ images.length > 0 ? { text: sendPrompt, images } : sendPrompt;
611
+
612
+ const promptChars = effectivePrompt.length;
613
+ const promptEstimatedTokens = Math.ceil(promptChars / 4);
614
+ console.log(
615
+ `ExecuteCursor prompt built: execution=${executionId}, ` +
616
+ `chars=${promptChars}, estimatedTokens=${promptEstimatedTokens}, ` +
617
+ `resolution=${engine.resolution.reason}, mode=${engine.resolution.mode}`,
618
+ );
619
+
620
+ return {
621
+ effectivePrompt,
622
+ turnImages,
623
+ primarySendImages,
624
+ toSendMessage,
625
+ promptEstimatedTokens,
626
+ // Same per-turn directive rule as buildFromPlan: a structured-output turn
627
+ // keeps its output contract on the rebuilt prompt (the transport retry
628
+ // re-sends effectivePrompt and inherits it without help).
629
+ buildRecoveryPrompt: async (fresh) =>
630
+ appendStructuredOutputDirective(
631
+ buildPrompt({
632
+ ...shared(),
633
+ resolution: fresh,
634
+ // Lazily selected: the primary send may have been a resumed-agent
635
+ // shape that carried no memories, but this fresh agent's prompt must.
636
+ recalledMemories: await standing.selectRecalledMemories(),
637
+ // Composed fresh: the failed primary stream may have appended partial
638
+ // work onto the transcript, and the replacement agent should know it.
639
+ turnRecoveryDigest: composeTurnRecoveryDigest(sink.status.messages),
640
+ }),
641
+ structuredOutputSchema,
642
+ ),
643
+ };
644
+ }
645
+