@stigmer/runner 3.14.1 → 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 (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
@@ -0,0 +1,618 @@
1
+ /**
2
+ * The turn runtime — one agent turn as a Temporal activity, written once for
3
+ * every harness, with the engine behind the adapter contract (`types.ts`).
4
+ *
5
+ * What runs here and nowhere else: the activity's identity and its
6
+ * execution-scoped context; the status and the single persist chokepoint;
7
+ * the periodic heartbeat; the stop controller and its four producers (the
8
+ * stall watchdog, the cost cap, the platform STOP, Temporal's cancellation);
9
+ * the resolution phases (`turn-context.ts`) composed into the `TurnInput`;
10
+ * the sink the adapter is handed; the terminal table (`terminal-table.ts`)
11
+ * with its throw-vs-return rule, applied by `settleWith`, the one writer of
12
+ * a terminal; the completion epilogue (final text,
13
+ * structured output, plan artifact, write-back); the finally that releases
14
+ * the workspace lock last. The adapter owns its SDK slice inside `runTurn`
15
+ * and its own teardown inside its own `finally`, which runs BEFORE this
16
+ * module's (the gate is torn down before the lock is released, as it always
17
+ * was).
18
+ *
19
+ * Shape: one activity per registry row (`createHarnessActivities`), each a
20
+ * closure over the adapter, the activity's byte-pinned name and one shared
21
+ * `StigmerClient`; `runTurnActivity` is the body. Every `console` line here
22
+ * names the activity (`ExecuteCursor started: …`) so the log reads as it did
23
+ * before the extraction, and never a harness literal — the harness is a
24
+ * registry row, not a branch.
25
+ *
26
+ * Extracted from `activities/execute-cursor/index.ts` `executeCursorInner`
27
+ * (S2 M3; the milestone log in stigmer-cloud
28
+ * `_projects/2026-09/20260911.03.sp.turn-runtime-extraction/` has every
29
+ * ruling). Its twelve resolution phases came out at M2; what came out here is
30
+ * everything between them and after the engine: the seventeen hermetic
31
+ * goldens under `execute-cursor/__tests__/hermetic/` pin the result byte
32
+ * for byte, and `__tests__/run-turn.test.ts` proves the table against the
33
+ * scripted fake without any engine at all.
34
+ */
35
+
36
+ import { setMaxListeners } from "node:events";
37
+ import { heartbeat, CancelledFailure, Context } from "@temporalio/activity";
38
+ import { create, type JsonObject } from "@bufbuild/protobuf";
39
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
40
+ import { ExecutionPhase, InteractionMode, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
41
+
42
+ import type { Config } from "../config.js";
43
+ import type { StigmerClient } from "../client/stigmer-client.js";
44
+ import type { NormalizedActivityInput } from "../shared/activity-input.js";
45
+ import { loadArtifactStorageConfig, resolveUsableArtifactStorage } from "../shared/artifact-storage.js";
46
+ import { TimingRecorder } from "../shared/cold-start-timing.js";
47
+ import { costCapExceeded } from "../shared/cost-guard.js";
48
+ import { runWithExecutionContext } from "../shared/execution-context.js";
49
+ import { startHeartbeat } from "../shared/heartbeat.js";
50
+ import { describeExecutionError } from "../shared/model-error.js";
51
+ import { publishPlanArtifact } from "../shared/plan-artifact.js";
52
+ import { StallTimeoutError, startStallWatchdog, type StallWatchdog } from "../shared/stall-watchdog.js";
53
+ import { reportSetupProgress, slimStatus, utcTimestamp } from "../shared/status.js";
54
+ import { cancelInProgressSubAgentProtos } from "../shared/subagent-rows.js";
55
+ import { removeStigmerSymlink } from "../shared/workspace/stigmer-link.js";
56
+ import {
57
+ cancellationReasonOf,
58
+ classifyTurnInterruption,
59
+ getShutdownSignalForQueue,
60
+ type TurnInterruption,
61
+ } from "../shared/worker-shutdown.js";
62
+ import { PersistChokepoint } from "./persist-chokepoint.js";
63
+ import { StopController } from "./stop-controller.js";
64
+ import {
65
+ applyTerminalArm,
66
+ costCapArm,
67
+ failedArm,
68
+ fileReviewResolvedArm,
69
+ infrastructureCancelArm,
70
+ pauseArm,
71
+ platformStopArm,
72
+ rejectedByUserArm,
73
+ stallArm,
74
+ TERMINAL_COPY,
75
+ unexpectedErrorArm,
76
+ workerShutdownArm,
77
+ workspaceLockTimeoutArm,
78
+ type TerminalArm,
79
+ } from "./terminal-table.js";
80
+ import { resolveTurnContext, type ResolutionDeps, type TurnFrame, type TurnSettlement } from "./turn-context.js";
81
+ import type { HarnessAdapter, TurnInput, TurnSink } from "./types.js";
82
+ import { UsageAccumulator } from "./usage-accumulator.js";
83
+
84
+ /**
85
+ * Interval of the activity-wide periodic heartbeat. Started before any phase
86
+ * runs: setup phases make network calls (blueprint resolution, workspace
87
+ * clone, MCP backfill, engine create) that can stall, and a pulse only
88
+ * between them leaves every individual call uncovered — the production
89
+ * stale-proxy incident hung inside `Agent.create` with zero heartbeats and
90
+ * surfaced as an opaque five-minute Temporal timeout. The details name the
91
+ * current step so a stall is attributed. Safe ONLY because every call below
92
+ * is itself bounded (the resolve timeout, the stall watchdog): an unbounded
93
+ * hang under a live heartbeat would keep a dead activity alive forever.
94
+ */
95
+ const PERIODIC_HEARTBEAT_MS = 30_000;
96
+
97
+ /**
98
+ * Vendor SDKs register abort listeners on the cancellation signal for each
99
+ * concurrent tool call (fetch, MCP, shell). With 10+ parallel tools, Node's
100
+ * default limit of 10 triggers `MaxListenersExceededWarning` — a diagnostic
101
+ * warning, not a functional error, but it pollutes logs and creates false
102
+ * alarm fatigue. 25 covers observed peaks (~12 concurrent tools + the
103
+ * heartbeat + the shutdown signal + SDK internals) with headroom.
104
+ */
105
+ const CANCELLATION_SIGNAL_MAX_LISTENERS = 25;
106
+
107
+ /** What every activity of the registry shares: the adapter it drives, its wire name, and the one client. */
108
+ export interface TurnRuntimeDeps {
109
+ readonly adapter: HarnessAdapter;
110
+ readonly activityName: string;
111
+ readonly client: StigmerClient;
112
+ readonly config: Config;
113
+ }
114
+
115
+ /**
116
+ * Run one turn: the activity body behind every harness's registry row. Sets
117
+ * the execution-scoped context for the WHOLE activity (the interceptors stamp
118
+ * every proxy-bound request from it, and much of that traffic is the
119
+ * runtime's own: offload, attachments, the epilogue's extraction) and runs
120
+ * the turn inside it.
121
+ */
122
+ export async function runTurnActivity(deps: TurnRuntimeDeps, input: NormalizedActivityInput): Promise<unknown> {
123
+ const { executionId, threadId, turnSeq } = input;
124
+ console.log(
125
+ `${deps.activityName} started: execution=${executionId}, threadId=${threadId || "(new)"}, turnSeq=${turnSeq}`,
126
+ );
127
+ return runWithExecutionContext(executionId, () => runTurn(deps, input));
128
+ }
129
+
130
+ /**
131
+ * Where the activity ends up once the table has been applied: return the
132
+ * slim status, or throw after the finally. The thrown value is always a
133
+ * `CancelledFailure` — the table's rule ("THROW when the workflow must see
134
+ * the activity as cancelled", `terminal-table.ts`) as a type.
135
+ */
136
+ type Settled = { readonly kind: "return"; readonly value: unknown } | { readonly kind: "throw"; readonly error: CancelledFailure };
137
+
138
+ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): Promise<unknown> {
139
+ const { adapter, activityName, client, config } = deps;
140
+ const { executionId } = input;
141
+
142
+ const status = create(AgentExecutionStatusSchema, {
143
+ phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
144
+ startedAt: utcTimestamp(),
145
+ });
146
+
147
+ // Cold-start timeline of this turn's setup: one mark after each phase, the
148
+ // adapter's own segments marked through the sink, emitted by the adapter
149
+ // once its engine is resolved (an early return skips it — partial setups
150
+ // are not comparable cold-start samples).
151
+ const setupTiming = new TimingRecorder();
152
+
153
+ // Artifact storage for offloading oversized tool outputs out of the
154
+ // persisted status and for publishing the plan artifact. Resolved once so
155
+ // every persist sees it. Best-effort: `undefined`, never a throw, when no
156
+ // substrate works; an absent store disables offload (the aggregate size cap
157
+ // still applies) and flips capture mode off (deny-gate fallback).
158
+ const artifactStorage = await resolveUsableArtifactStorage(loadArtifactStorageConfig(config), { executionId });
159
+ setupTiming.mark("resolve_artifact_storage");
160
+
161
+ const frame: TurnFrame = { primaryDir: undefined, releaseWorkspaceLock: undefined, writeback: null };
162
+ const stop = new StopController();
163
+ let usage: UsageAccumulator | undefined;
164
+ let heartbeatPhase = "setup";
165
+ let lastActivityDetail: string | undefined;
166
+ let watchdog: StallWatchdog | undefined;
167
+
168
+ const cancellationSignal = Context.current().cancellationSignal;
169
+ const shutdownSignal = getShutdownSignalForQueue(Context.current().info.taskQueue);
170
+ const periodicHeartbeat = startHeartbeat(PERIODIC_HEARTBEAT_MS, () => ({ phase: heartbeatPhase, execution: executionId }));
171
+
172
+ const chokepoint = new PersistChokepoint({
173
+ client,
174
+ executionId,
175
+ status,
176
+ offload: artifactStorage ? { artifactStorage, executionId } : undefined,
177
+ usage: () => usage,
178
+ heartbeat,
179
+ onPlatformStop: () => stop.stop({ kind: "platform-stop" }),
180
+ });
181
+
182
+ try {
183
+ setMaxListeners(CANCELLATION_SIGNAL_MAX_LISTENERS, cancellationSignal);
184
+ } catch {
185
+ // An AbortSignal that does not support setMaxListeners (an older SDK):
186
+ // the warning is harmless, so this is not.
187
+ }
188
+ const onCancellation = (): void => stop.stop({ kind: "cancellation" });
189
+ if (cancellationSignal.aborted) onCancellation();
190
+ else cancellationSignal.addEventListener("abort", onCancellation, { once: true });
191
+
192
+ /** The interruption Temporal delivered, read when the table needs it. */
193
+ const interruption = (): TurnInterruption =>
194
+ classifyTurnInterruption({
195
+ cancellationReason: cancellationReasonOf(cancellationSignal),
196
+ shutdownSignalAborted: shutdownSignal?.aborted ?? false,
197
+ });
198
+
199
+ /**
200
+ * The one writer of a terminal: apply the arm, persist it ONCE, and end
201
+ * the activity the way the arm's disposition says. A thrown arm also marks
202
+ * the in-progress sub-agent rows cancelled before that persist, so the
203
+ * status the workflow reads back is the whole terminal state in one write.
204
+ * (The file-review resume in `settleResolution` is the one place that
205
+ * applies an arm itself: its write-back must land between the arm and the
206
+ * persist.)
207
+ *
208
+ * The table decides what is written and whether the activity throws; the
209
+ * caller may supply the `CancelledFailure` to throw. The catch does, to
210
+ * rethrow the failure it caught (the caught failure IS the evidence, #776)
211
+ * or to throw the after-error variant of the copy; every other caller lets
212
+ * the arm's own message stand.
213
+ */
214
+ const settleWith = async (arm: TerminalArm, failure?: CancelledFailure): Promise<Settled> => {
215
+ applyTerminalArm(status, arm);
216
+ if (arm.disposition.kind === "return") {
217
+ await chokepoint.write();
218
+ return { kind: "return", value: slimStatus(status) };
219
+ }
220
+ cancelInProgressSubAgentProtos(status.subAgentExecutions);
221
+ await chokepoint.write();
222
+ return { kind: "throw", error: failure ?? new CancelledFailure(arm.disposition.message) };
223
+ };
224
+
225
+ // ── The turn ended during resolution ──────────────────────────────────────
226
+
227
+ async function settleResolution(settlement: TurnSettlement): Promise<Settled> {
228
+ switch (settlement.kind) {
229
+ case "workspace-lock-timeout": {
230
+ const outcome = await settleWith(workspaceLockTimeoutArm(settlement.error));
231
+ console.warn(`${activityName} workspace lock timeout: execution=${executionId}`);
232
+ return outcome;
233
+ }
234
+ case "rejected-by-user":
235
+ // A reject of an irreversible action (shell/MCP) fails the execution.
236
+ return settleWith(rejectedByUserArm());
237
+ case "file-review-resolved": {
238
+ // Push the APPROVED tree — reconcile snapped rejected files back to
239
+ // baseline, so what finalize commits is exactly what the user kept.
240
+ // After reconcile, before persist, never on a failed reconcile
241
+ // (diverged bytes must not reach the remote).
242
+ applyTerminalArm(status, fileReviewResolvedArm(settlement));
243
+ if (!settlement.failed && frame.writeback) {
244
+ await frame.writeback.finalize();
245
+ }
246
+ await chokepoint.write();
247
+ console.log(
248
+ `${activityName} file-review resume short-circuit: execution=${executionId}, ` +
249
+ `failed=${settlement.failed}, discarded=${settlement.discardedPaths.length}`,
250
+ );
251
+ return { kind: "return", value: slimStatus(status) };
252
+ }
253
+ default: {
254
+ const exhaustive: never = settlement;
255
+ throw new Error(`${activityName}: unknown resolution settlement ${String(exhaustive)}`);
256
+ }
257
+ }
258
+ }
259
+
260
+ // ── The engine's turn and its outcome ─────────────────────────────────────
261
+
262
+ async function runEngineTurn(turn: TurnInput): Promise<Settled> {
263
+ const maxCostUsd = turn.execution.spec?.executionConfig?.maxCostUsd ?? 0;
264
+ const accumulator = new UsageAccumulator(turn.model.serviceTier, turn.model.thinkingMode);
265
+ usage = accumulator;
266
+
267
+ // The stall watchdog: the periodic heartbeat proves the process is alive,
268
+ // not that the engine is progressing; if the engine wedges, nothing
269
+ // arrives and the turn would hang forever. Armed for the adapter's whole
270
+ // stretch (Q-M3-13) and reset by every `recordActivity`; on stall the
271
+ // signal aborts, the adapter cancels its run, and the table reports the
272
+ // stall with the last detail the adapter named.
273
+ watchdog = startStallWatchdog(config.cursorStreamStallTimeoutMs, (idleMs) => {
274
+ const error = new StallTimeoutError(idleMs, lastActivityDetail ? `last tool: ${lastActivityDetail}` : undefined);
275
+ console.warn(
276
+ `${activityName} stall detected: execution=${executionId}, idleMs=${idleMs}, lastTool=${lastActivityDetail ?? "none"}`,
277
+ );
278
+ stop.stop({ kind: "stall", error });
279
+ });
280
+ const armedWatchdog = watchdog;
281
+
282
+ const sink: TurnSink = {
283
+ status,
284
+ stopSignal: stop.signal,
285
+ setupTiming,
286
+ requestPersist: () => chokepoint.request(),
287
+ recordActivity: (detail) => {
288
+ if (detail !== undefined) lastActivityDetail = detail;
289
+ armedWatchdog.recordActivity();
290
+ },
291
+ reportUsage: (delta) => {
292
+ accumulator.addTurn(delta);
293
+ // max_cost_usd enforcement (cost-guard.ts): the running estimate only
294
+ // advances here, so this is the single check point; the abort is how
295
+ // the adapter learns to end its run.
296
+ const estimated = accumulator.snapshot().estimatedCostUsd;
297
+ if (!stop.evidence.costCapExceeded && costCapExceeded(maxCostUsd, estimated)) {
298
+ console.warn(
299
+ `${activityName} cost cap exceeded: execution=${executionId}, ` +
300
+ `estimatedCostUsd=${estimated.toFixed(4)}, maxCostUsd=${maxCostUsd.toFixed(2)}`,
301
+ );
302
+ stop.stop({ kind: "cost-cap" });
303
+ }
304
+ },
305
+ reportProgress: (label) => reportSetupProgress(client, executionId, label),
306
+ bindHarnessState: async (harnessStateId) => {
307
+ turn.session.spec!.harnessStateId = harnessStateId;
308
+ // Clear slug to avoid re-validation of potentially invalid
309
+ // server-generated slugs. BuildUpdateStateStep preserves the existing
310
+ // slug from the database record.
311
+ if (turn.session.metadata) turn.session.metadata.slug = "";
312
+ await client.updateSession(turn.session);
313
+ console.log(`Stored ${harnessStateId} as harness_state_id on session ${turn.sessionId}`);
314
+ },
315
+ };
316
+
317
+ heartbeatPhase = "harness";
318
+ const outcome = await adapter.runTurn(turn, sink);
319
+ armedWatchdog.stop();
320
+ heartbeatPhase = "epilogue";
321
+
322
+ switch (outcome.kind) {
323
+ case "completed":
324
+ return completeTurn(turn, ExecutionPhase.EXECUTION_COMPLETED);
325
+ case "cancelled":
326
+ return completeTurn(turn, ExecutionPhase.EXECUTION_CANCELLED);
327
+ case "awaiting_approval": {
328
+ // Pause for review exactly like the native harness: the adapter put
329
+ // the WAITING rows on the transcript; flip the phase, persist, and
330
+ // RETURN to the workflow, which waits for the approval signal and
331
+ // reinvokes.
332
+ status.phase = ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL;
333
+ await chokepoint.write();
334
+ return { kind: "return", value: slimStatus(status) };
335
+ }
336
+ case "failed": {
337
+ const settledFailure = await settleWith(failedArm(outcome.message, outcome.surface));
338
+ console.error(
339
+ `${activityName} failed (${outcome.surface}): execution=${executionId}, error=${outcome.message}` +
340
+ (outcome.cause instanceof Error ? `, cause=${outcome.cause.message}` : ""),
341
+ );
342
+ return settledFailure;
343
+ }
344
+ case "interrupted":
345
+ return settleInterrupted(maxCostUsd, accumulator);
346
+ default: {
347
+ const exhaustive: never = outcome;
348
+ throw new Error(`${activityName}: unknown turn outcome ${JSON.stringify(exhaustive)}`);
349
+ }
350
+ }
351
+ }
352
+
353
+ /**
354
+ * The table for an `interrupted` outcome, in the orchestrator's precedence:
355
+ * a stall (FAILED, return) outranks everything; a cost cap (TERMINATED,
356
+ * return); then Temporal's cancellation as the signal's reason reads it —
357
+ * the runner's drain, the orchestrator's pause, an infrastructure cancel
358
+ * (all thrown); last a platform stop (COMPLETED, return).
359
+ */
360
+ async function settleInterrupted(maxCostUsd: number, accumulator: UsageAccumulator): Promise<Settled> {
361
+ const evidence = stop.evidence;
362
+ if (evidence.stall) {
363
+ const outcome = await settleWith(stallArm(evidence.stall));
364
+ console.warn(`${activityName} stalled: execution=${executionId}, error=${status.error}`);
365
+ return outcome;
366
+ }
367
+ if (evidence.costCapExceeded) {
368
+ const estimated = accumulator.snapshot().estimatedCostUsd;
369
+ const outcome = await settleWith(costCapArm(maxCostUsd, estimated));
370
+ console.warn(
371
+ `${activityName} terminated (cost cap): execution=${executionId}, ` +
372
+ `estimatedCostUsd=${estimated.toFixed(4)}, maxCostUsd=${maxCostUsd.toFixed(2)}`,
373
+ );
374
+ return outcome;
375
+ }
376
+ const delivered = interruption();
377
+ switch (delivered) {
378
+ case "worker-shutdown":
379
+ console.log(`${activityName} interrupted (worker shutdown): execution=${executionId}`);
380
+ return settleWith(workerShutdownArm());
381
+ case "pause":
382
+ console.log(`${activityName} paused: execution=${executionId}`);
383
+ return settleWith(pauseArm());
384
+ case "infrastructure":
385
+ console.log(`${activityName} interrupted (infrastructure cancel): execution=${executionId}`);
386
+ return settleWith(infrastructureCancelArm());
387
+ case "none":
388
+ break;
389
+ default: {
390
+ const exhaustive: never = delivered;
391
+ throw new Error(`${activityName}: unknown interruption ${String(exhaustive)}`);
392
+ }
393
+ }
394
+ if (evidence.platformStop) {
395
+ console.log(`${activityName} completed (platform stop): execution=${executionId}`);
396
+ return settleWith(platformStopArm());
397
+ }
398
+ // The adapter settled `interrupted` with the signal live: a contract
399
+ // violation, reported as the runner's own failure rather than guessed at.
400
+ return settleWith(unexpectedErrorArm("HarnessContractError", `${adapter.name} settled interrupted with no stop`));
401
+ }
402
+
403
+ /**
404
+ * A turn the engine finished (COMPLETED) or ended cancelled on its own
405
+ * (CANCELLED): the final text from the last AI row; the structured output
406
+ * the execution asked for, extracted from that text unless the adapter
407
+ * already folded it onto the status; the plan artifact in plan mode; the
408
+ * write-back safety net; the one persist that makes COMPLETED and the
409
+ * structured output visible atomically; the slim return with `final_text`
410
+ * and `structured` beside it.
411
+ */
412
+ async function completeTurn(turn: TurnInput, phase: ExecutionPhase): Promise<Settled> {
413
+ status.completedAt = utcTimestamp();
414
+ status.phase = phase;
415
+
416
+ let structuredOutput: unknown = status.structuredOutput;
417
+ let finalText: string | undefined;
418
+
419
+ if (phase === ExecutionPhase.EXECUTION_COMPLETED) {
420
+ finalText = [...status.messages].reverse().find((m) => m.type === MessageType.MESSAGE_AI)?.content;
421
+
422
+ if (structuredOutput === undefined && turn.structuredOutputSchema && finalText) {
423
+ structuredOutput = await extractStructuredOutputFromText(turn, finalText);
424
+ if (structuredOutput !== undefined) {
425
+ status.structuredOutput = structuredOutput as JsonObject;
426
+ }
427
+ }
428
+
429
+ // Plan mode: publish the final plan message as a plan artifact (named
430
+ // from the plan's title); the only artifact path a harness without an
431
+ // auto-publish pipeline has.
432
+ const interactionMode = turn.execution.spec?.executionConfig?.interactionMode ?? InteractionMode.UNSPECIFIED;
433
+ if (interactionMode === InteractionMode.PLAN && finalText && turn.artifactStorage) {
434
+ try {
435
+ await publishPlanArtifact({ status, executionId, planText: finalText, artifactStorage: turn.artifactStorage });
436
+ } catch (err) {
437
+ console.warn(`${activityName} plan artifact publish skipped (non-fatal): execution=${executionId}, error=${err}`);
438
+ }
439
+ }
440
+
441
+ // Write-back safety net on terminal completion. A capture-mode turn
442
+ // with captured changes paused for review instead, so reaching here
443
+ // means no reviewable delta this turn and this is normally a no-op —
444
+ // it exists for stragglers outside the capture, and never runs
445
+ // mid-turn.
446
+ if (frame.writeback) {
447
+ await frame.writeback.finalize();
448
+ }
449
+ }
450
+
451
+ // NOW persist — the subscriber sees the phase and structured_output atomically.
452
+ await chokepoint.write();
453
+ console.log(
454
+ `${activityName} completed: execution=${executionId}, phase=${ExecutionPhase[status.phase]}, ` +
455
+ `hasStructuredOutput=${structuredOutput !== undefined}` +
456
+ (status.error ? `, error=${status.error}` : ""),
457
+ );
458
+
459
+ const slim = slimStatus(status) as Record<string, unknown>;
460
+ if (finalText !== undefined) slim.final_text = finalText;
461
+ if (structuredOutput !== undefined) slim.structured = structuredOutput;
462
+ return { kind: "return", value: slim };
463
+ }
464
+
465
+ /**
466
+ * Tier 1 + 1.5: JSON.parse, code-fence extraction, heuristic brace match.
467
+ * Tier 2: LLM extraction with withStructuredOutput — deterministic,
468
+ * function-calling guarantees schema-conformant output. Each tier logs
469
+ * its verdict; a failed LLM extraction is logged and yields nothing.
470
+ */
471
+ async function extractStructuredOutputFromText(turn: TurnInput, finalText: string): Promise<unknown> {
472
+ const { extractJsonFromText } = await import("../shared/extract-json.js");
473
+ const fromText = extractJsonFromText(finalText);
474
+ if (fromText !== undefined) {
475
+ console.log(
476
+ `${activityName} structured output extracted (text): execution=${executionId}, finalTextLength=${finalText.length}`,
477
+ );
478
+ return fromText;
479
+ }
480
+ console.log(
481
+ `${activityName} text extraction failed, trying LLM extraction: execution=${executionId}, ` +
482
+ `finalTextLength=${finalText.length}`,
483
+ );
484
+ try {
485
+ const { extractStructuredOutput } = await import("../shared/extract-structured-output.js");
486
+ const extracted = await extractStructuredOutput(finalText, turn.structuredOutputSchema!, config, turn.model.requested);
487
+ if (extracted !== undefined) {
488
+ console.log(`${activityName} structured output extracted (LLM): execution=${executionId}`);
489
+ }
490
+ return extracted;
491
+ } catch (extractErr) {
492
+ const errMsg = extractErr instanceof Error ? extractErr.message : String(extractErr);
493
+ console.error(
494
+ `${activityName} structured output extraction FAILED: execution=${executionId}, ` +
495
+ `requestedModel=${turn.model.requested}, finalTextLength=${finalText.length}, error=${errMsg}`,
496
+ );
497
+ return undefined;
498
+ }
499
+ }
500
+
501
+ // ── Something threw ───────────────────────────────────────────────────────
502
+
503
+ /**
504
+ * The catch. An adapter never throws (its contract), so what lands here is
505
+ * the runtime's own: a `CancelledFailure` from a resolution phase (the
506
+ * lock wait), an unexpected error during resolution or the epilogue, or a
507
+ * contract violation. The cancellation arms are the table's throw arms
508
+ * through `settleWith`, with the failure this catch chooses to throw; the
509
+ * generic arm is the `internal` failure surface, returned.
510
+ */
511
+ async function settleThrown(err: unknown): Promise<Settled> {
512
+ const cancelledArm = (): TerminalArm | undefined => {
513
+ const delivered = interruption();
514
+ switch (delivered) {
515
+ case "worker-shutdown":
516
+ console.log(`${activityName} cancelled (worker shutdown) for execution ${executionId}`);
517
+ return workerShutdownArm();
518
+ case "pause":
519
+ console.log(`${activityName} cancelled (pause) for execution ${executionId}`);
520
+ return pauseArm();
521
+ case "infrastructure":
522
+ console.log(`${activityName} cancelled (infrastructure) for execution ${executionId}`);
523
+ return infrastructureCancelArm();
524
+ case "none":
525
+ return undefined;
526
+ default: {
527
+ const exhaustive: never = delivered;
528
+ throw new Error(`${activityName}: unknown interruption ${String(exhaustive)}`);
529
+ }
530
+ }
531
+ };
532
+
533
+ if (err instanceof CancelledFailure) {
534
+ // The cancellation arrived as a throw (the lock wait observes the
535
+ // signal); the caught failure IS the evidence (#776). A thrown
536
+ // cancellation with no delivered cancellation on the signal can only be
537
+ // the runtime's own `checkCancellation`-style throw: infrastructure.
538
+ return settleWith(cancelledArm() ?? infrastructureCancelArm(), err);
539
+ }
540
+
541
+ const errDetail = err instanceof Error ? err.message : String(err);
542
+ const arm = cancelledArm();
543
+ if (arm !== undefined) {
544
+ // A non-cancellation error while cancelled was most likely caused by the
545
+ // cancellation itself (a teardown mid-flight) and must not overwrite the
546
+ // state the workflow already expects: a pause stays PAUSED; anything
547
+ // else reads as the interruption it was.
548
+ if (arm.phase === ExecutionPhase.EXECUTION_PAUSED) {
549
+ console.log(`${activityName} error during pause (treating as pause): execution=${executionId}, error=${errDetail}`);
550
+ return settleWith(arm, new CancelledFailure(TERMINAL_COPY.pause.throwMessageAfterError));
551
+ }
552
+ console.log(`${activityName} error during infrastructure cancel: execution=${executionId}, error=${errDetail}`);
553
+ return settleWith(
554
+ { ...infrastructureCancelArm(), error: `Execution interrupted: ${errDetail}` },
555
+ new CancelledFailure(TERMINAL_COPY.infrastructureCancel.throwMessageAfterError),
556
+ );
557
+ }
558
+
559
+ // Unwrap + classify before formatting: a model error arrives
560
+ // MiddlewareError-wrapped with raw provider prose; non-model errors keep
561
+ // the root error's own identity.
562
+ const { errorType, errorMessage } = describeExecutionError(err, { proxyMode: !!config.proxyEndpoint });
563
+ console.error(`${activityName} failed: execution=${executionId}, [${errorType}] ${errorMessage}`);
564
+ return settleWith(unexpectedErrorArm(errorType, errorMessage));
565
+ }
566
+ // ── Drive ─────────────────────────────────────────────────────────────────
567
+
568
+ let settled: Settled;
569
+ try {
570
+ const resolutionDeps: ResolutionDeps = {
571
+ input,
572
+ client,
573
+ config,
574
+ status,
575
+ artifactStorage,
576
+ timing: setupTiming,
577
+ signal: cancellationSignal,
578
+ heartbeat,
579
+ enterPhase: (step) => {
580
+ heartbeatPhase = step;
581
+ heartbeat();
582
+ },
583
+ reportProgress: (label) => reportSetupProgress(client, executionId, label),
584
+ };
585
+ const resolution = await resolveTurnContext(resolutionDeps, adapter.capabilities, frame, () => chokepoint.write());
586
+ if (resolution.kind === "settled") {
587
+ settled = await settleResolution(resolution.settlement);
588
+ } else {
589
+ settled = await runEngineTurn(resolution.input);
590
+ }
591
+ } catch (err) {
592
+ settled = await settleThrown(err);
593
+ } finally {
594
+ periodicHeartbeat.stop();
595
+ watchdog?.stop();
596
+ cancellationSignal.removeEventListener("abort", onCancellation);
597
+ // Remove the workspace's `.stigmer` link the skill and attachment phases
598
+ // created, so attaching a real repo leaves it untouched between turns
599
+ // (issue #173). After the adapter's own teardown (its gate), before the
600
+ // lock. Idempotent and non-throwing.
601
+ if (frame.primaryDir) await removeStigmerSymlink(frame.primaryDir);
602
+ // Release the workspace turn lock LAST — every mutation of this turn has
603
+ // landed, and the next queued turn must not baseline before that.
604
+ // Idempotent and non-throwing, so it can never mask the turn's outcome.
605
+ await frame.releaseWorkspaceLock?.();
606
+ }
607
+
608
+ switch (settled.kind) {
609
+ case "return":
610
+ return settled.value;
611
+ case "throw":
612
+ throw settled.error;
613
+ default: {
614
+ const exhaustive: never = settled;
615
+ throw new Error(`${activityName}: unknown settlement ${JSON.stringify(exhaustive)}`);
616
+ }
617
+ }
618
+ }