@stigmer/runner 3.15.3-dev.20260914121148 → 3.16.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 (592) hide show
  1. package/README.md +2 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/approval-contract/types.d.ts +1 -1
  4. package/dist/__test-utils__/hermetic-activity.d.ts +61 -7
  5. package/dist/__test-utils__/hermetic-activity.js +100 -9
  6. package/dist/__test-utils__/hermetic-activity.js.map +1 -1
  7. package/dist/__test-utils__/model-registry-fixture.d.ts +22 -3
  8. package/dist/__test-utils__/model-registry-fixture.js +22 -7
  9. package/dist/__test-utils__/model-registry-fixture.js.map +1 -1
  10. package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +2 -2
  11. package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +2 -2
  12. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +61 -0
  13. package/dist/activities/execute-cursor/__test-utils__/fold.js +94 -0
  14. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -0
  15. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
  17. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +47 -12
  18. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +41 -10
  19. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  20. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +15 -6
  21. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +17 -4
  22. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
  23. package/dist/activities/execute-cursor/adapter.d.ts +4 -4
  24. package/dist/activities/execute-cursor/adapter.js +4 -4
  25. package/dist/activities/execute-cursor/approval-policy.d.ts +10 -10
  26. package/dist/activities/execute-cursor/approval-policy.js +12 -23
  27. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  28. package/dist/activities/execute-cursor/approval-state.d.ts +35 -1
  29. package/dist/activities/execute-cursor/approval-state.js +65 -0
  30. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  31. package/dist/activities/execute-cursor/{message-translator.d.ts → boundary-rows.d.ts} +45 -341
  32. package/dist/activities/execute-cursor/boundary-rows.js +1018 -0
  33. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -0
  34. package/dist/activities/execute-cursor/cursor-capabilities.d.ts +1 -1
  35. package/dist/activities/execute-cursor/cursor-capabilities.js +1 -1
  36. package/dist/activities/execute-cursor/cursor-event-recorder.d.ts +33 -14
  37. package/dist/activities/execute-cursor/cursor-event-recorder.js +43 -21
  38. package/dist/activities/execute-cursor/cursor-event-recorder.js.map +1 -1
  39. package/dist/activities/execute-cursor/prompt-builder.d.ts +3 -3
  40. package/dist/activities/execute-cursor/prompt-builder.js +4 -4
  41. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  42. package/dist/activities/execute-cursor/service-tier.js +1 -2
  43. package/dist/activities/execute-cursor/service-tier.js.map +1 -1
  44. package/dist/activities/execute-cursor/sub-agent-steps.d.ts +79 -0
  45. package/dist/activities/execute-cursor/sub-agent-steps.js +180 -0
  46. package/dist/activities/execute-cursor/sub-agent-steps.js.map +1 -0
  47. package/dist/activities/execute-cursor/tool-result.d.ts +49 -0
  48. package/dist/activities/execute-cursor/tool-result.js +130 -0
  49. package/dist/activities/execute-cursor/tool-result.js.map +1 -0
  50. package/dist/activities/execute-cursor/translator.d.ts +178 -0
  51. package/dist/activities/execute-cursor/translator.js +485 -0
  52. package/dist/activities/execute-cursor/translator.js.map +1 -0
  53. package/dist/activities/execute-cursor/turn-boundary.d.ts +15 -5
  54. package/dist/activities/execute-cursor/turn-boundary.js +10 -14
  55. package/dist/activities/execute-cursor/turn-boundary.js.map +1 -1
  56. package/dist/activities/execute-cursor/turn-settle.d.ts +11 -7
  57. package/dist/activities/execute-cursor/turn-settle.js +47 -34
  58. package/dist/activities/execute-cursor/turn-settle.js.map +1 -1
  59. package/dist/activities/execute-cursor/turn-setup.d.ts +2 -2
  60. package/dist/activities/execute-cursor/turn-setup.js +5 -5
  61. package/dist/activities/execute-cursor/turn-setup.js.map +1 -1
  62. package/dist/activities/execute-cursor/turn-stream.d.ts +23 -14
  63. package/dist/activities/execute-cursor/turn-stream.js +41 -28
  64. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  65. package/dist/activities/execute-cursor/turn.d.ts +4 -4
  66. package/dist/activities/execute-cursor/turn.js +5 -5
  67. package/dist/activities/execute-cursor/usage-pricing.d.ts +2 -2
  68. package/dist/activities/execute-cursor/usage-pricing.js +2 -2
  69. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.d.ts +2 -2
  70. package/dist/activities/execute-deep-agent/__test-utils__/contract-subject.js +2 -2
  71. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +16 -11
  72. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -10
  73. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  74. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js +1 -1
  75. package/dist/activities/execute-deep-agent/__test-utils__/hitl-script.js.map +1 -1
  76. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.d.ts +2 -3
  77. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js +2 -3
  78. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model-module.js.map +1 -1
  79. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +12 -3
  80. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +18 -4
  81. package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -1
  82. package/dist/activities/execute-deep-agent/__test-utils__/v3-event-fixtures.d.ts +10 -1
  83. package/dist/activities/execute-deep-agent/__test-utils__/v3-event-fixtures.js +24 -11
  84. package/dist/activities/execute-deep-agent/__test-utils__/v3-event-fixtures.js.map +1 -1
  85. package/dist/activities/execute-deep-agent/adapter.d.ts +1 -1
  86. package/dist/activities/execute-deep-agent/adapter.js +1 -1
  87. package/dist/activities/execute-deep-agent/approval-file-change.d.ts +19 -19
  88. package/dist/activities/execute-deep-agent/approval-file-change.js +16 -15
  89. package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -1
  90. package/dist/activities/execute-deep-agent/deep-agent-capabilities.d.ts +2 -2
  91. package/dist/activities/execute-deep-agent/deep-agent-capabilities.js +2 -2
  92. package/dist/activities/execute-deep-agent/hitl.d.ts +2 -2
  93. package/dist/activities/execute-deep-agent/hitl.js +2 -2
  94. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +13 -3
  95. package/dist/activities/execute-deep-agent/inline-publisher.js +3 -3
  96. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  97. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +3 -3
  98. package/dist/activities/execute-deep-agent/prompt-builder.js +6 -7
  99. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  100. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +16 -8
  101. package/dist/activities/execute-deep-agent/streaming-side-effects.js +20 -33
  102. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  103. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +3 -3
  104. package/dist/activities/execute-deep-agent/subagent-transformer.js +1 -1
  105. package/dist/activities/execute-deep-agent/translator.d.ts +134 -0
  106. package/dist/activities/execute-deep-agent/translator.js +418 -0
  107. package/dist/activities/execute-deep-agent/translator.js.map +1 -0
  108. package/dist/activities/execute-deep-agent/turn-settle.d.ts +13 -7
  109. package/dist/activities/execute-deep-agent/turn-settle.js +34 -45
  110. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  111. package/dist/activities/execute-deep-agent/turn-setup.d.ts +8 -8
  112. package/dist/activities/execute-deep-agent/turn-setup.js +7 -7
  113. package/dist/activities/execute-deep-agent/turn-stream.d.ts +13 -10
  114. package/dist/activities/execute-deep-agent/turn-stream.js +44 -41
  115. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  116. package/dist/activities/execute-deep-agent/turn.d.ts +3 -3
  117. package/dist/activities/execute-deep-agent/turn.js +3 -3
  118. package/dist/activities/generate-session-subject.d.ts +25 -4
  119. package/dist/activities/generate-session-subject.js +17 -7
  120. package/dist/activities/generate-session-subject.js.map +1 -1
  121. package/dist/client/stigmer-client.d.ts +17 -6
  122. package/dist/client/stigmer-client.js +47 -14
  123. package/dist/client/stigmer-client.js.map +1 -1
  124. package/dist/config.d.ts +6 -3
  125. package/dist/config.js +4 -1
  126. package/dist/config.js.map +1 -1
  127. package/dist/harness/approval-decisions.d.ts +1 -1
  128. package/dist/harness/approval-decisions.js +1 -1
  129. package/dist/harness/capabilities.d.ts +21 -11
  130. package/dist/harness/capabilities.js +2 -2
  131. package/dist/harness/capture.d.ts +4 -4
  132. package/dist/harness/capture.js +4 -4
  133. package/dist/harness/persist-chokepoint.d.ts +3 -3
  134. package/dist/harness/persist-chokepoint.js +3 -3
  135. package/dist/harness/registry.d.ts +2 -2
  136. package/dist/harness/registry.js +2 -2
  137. package/dist/harness/run-turn.d.ts +6 -4
  138. package/dist/harness/run-turn.js +31 -9
  139. package/dist/harness/run-turn.js.map +1 -1
  140. package/dist/harness/stop-controller.d.ts +1 -1
  141. package/dist/harness/stop-controller.js +1 -1
  142. package/dist/harness/terminal-table.d.ts +7 -7
  143. package/dist/harness/terminal-table.js +7 -7
  144. package/dist/harness/transcript/builder.d.ts +182 -0
  145. package/dist/harness/transcript/builder.js +693 -0
  146. package/dist/harness/transcript/builder.js.map +1 -0
  147. package/dist/harness/transcript/events.d.ts +218 -0
  148. package/dist/harness/transcript/events.js +50 -0
  149. package/dist/harness/transcript/events.js.map +1 -0
  150. package/dist/harness/transcript/state.d.ts +110 -0
  151. package/dist/harness/transcript/state.js +148 -0
  152. package/dist/harness/transcript/state.js.map +1 -0
  153. package/dist/harness/turn-context.d.ts +54 -38
  154. package/dist/harness/turn-context.js +42 -43
  155. package/dist/harness/turn-context.js.map +1 -1
  156. package/dist/harness/types.d.ts +63 -22
  157. package/dist/harness/types.js +12 -11
  158. package/dist/harness/types.js.map +1 -1
  159. package/dist/harness/usage-accumulator.d.ts +2 -2
  160. package/dist/harness/usage-accumulator.js +2 -2
  161. package/dist/harness-adapters.d.ts +5 -4
  162. package/dist/harness-adapters.js +5 -4
  163. package/dist/harness-adapters.js.map +1 -1
  164. package/dist/interceptors/run-credential-activity.d.ts +28 -0
  165. package/dist/interceptors/run-credential-activity.js +56 -0
  166. package/dist/interceptors/run-credential-activity.js.map +1 -0
  167. package/dist/middleware/approval-gate.d.ts +6 -0
  168. package/dist/middleware/approval-gate.js +7 -57
  169. package/dist/middleware/approval-gate.js.map +1 -1
  170. package/dist/middleware/cost-advisory.d.ts +3 -3
  171. package/dist/middleware/cost-advisory.js +3 -3
  172. package/dist/middleware/index.d.ts +1 -1
  173. package/dist/middleware/index.js +1 -1
  174. package/dist/runner-manager.js +10 -4
  175. package/dist/runner-manager.js.map +1 -1
  176. package/dist/shared/activity-input.d.ts +9 -0
  177. package/dist/shared/activity-input.js +17 -0
  178. package/dist/shared/activity-input.js.map +1 -1
  179. package/dist/shared/approval-policy.d.ts +53 -0
  180. package/dist/shared/approval-policy.js +76 -0
  181. package/dist/shared/approval-policy.js.map +1 -1
  182. package/dist/shared/args-preview.d.ts +45 -22
  183. package/dist/shared/args-preview.js +51 -41
  184. package/dist/shared/args-preview.js.map +1 -1
  185. package/dist/shared/attachment-resolver.d.ts +4 -4
  186. package/dist/shared/attachment-resolver.js +4 -4
  187. package/dist/shared/attachment-zip.d.ts +2 -2
  188. package/dist/shared/attachment-zip.js +2 -2
  189. package/dist/shared/channel-attachment.d.ts +8 -5
  190. package/dist/shared/channel-attachment.js +10 -6
  191. package/dist/shared/channel-attachment.js.map +1 -1
  192. package/dist/shared/cost-guard.d.ts +2 -2
  193. package/dist/shared/cost-guard.js +2 -2
  194. package/dist/shared/exact-apply.d.ts +1 -1
  195. package/dist/shared/exact-apply.js +1 -1
  196. package/dist/shared/execution-context.d.ts +2 -2
  197. package/dist/shared/execution-context.js +2 -2
  198. package/dist/shared/extract-structured-output.d.ts +2 -2
  199. package/dist/shared/extract-structured-output.js +2 -2
  200. package/dist/shared/filereview/cas-touched.d.ts +2 -2
  201. package/dist/shared/filereview/cas-touched.js +2 -2
  202. package/dist/shared/filereview/progress.d.ts +13 -0
  203. package/dist/shared/filereview/progress.js +13 -0
  204. package/dist/shared/filereview/progress.js.map +1 -1
  205. package/dist/shared/heartbeat.d.ts +1 -1
  206. package/dist/shared/heartbeat.js +1 -1
  207. package/dist/shared/llm-backend.d.ts +0 -4
  208. package/dist/shared/llm-backend.js +0 -4
  209. package/dist/shared/llm-backend.js.map +1 -1
  210. package/dist/shared/memory-attachment.d.ts +16 -9
  211. package/dist/shared/memory-attachment.js +18 -10
  212. package/dist/shared/memory-attachment.js.map +1 -1
  213. package/dist/shared/model-error.d.ts +1 -2
  214. package/dist/shared/model-error.js +1 -2
  215. package/dist/shared/model-error.js.map +1 -1
  216. package/dist/shared/persist-decision.d.ts +18 -24
  217. package/dist/shared/persist-decision.js +19 -11
  218. package/dist/shared/persist-decision.js.map +1 -1
  219. package/dist/shared/prompt-sections.d.ts +4 -4
  220. package/dist/shared/prompt-sections.js +4 -4
  221. package/dist/shared/run-credential-store.d.ts +32 -0
  222. package/dist/shared/run-credential-store.js +39 -0
  223. package/dist/shared/run-credential-store.js.map +1 -0
  224. package/dist/shared/run-credential.d.ts +61 -0
  225. package/dist/shared/run-credential.js +68 -0
  226. package/dist/shared/run-credential.js.map +1 -0
  227. package/dist/shared/skill-resolver.d.ts +3 -3
  228. package/dist/shared/skill-resolver.js +3 -3
  229. package/dist/shared/status-offload.d.ts +1 -1
  230. package/dist/shared/status-offload.js +2 -2
  231. package/dist/shared/status-offload.js.map +1 -1
  232. package/dist/shared/streaming-scheduler.d.ts +2 -7
  233. package/dist/shared/streaming-scheduler.js +6 -14
  234. package/dist/shared/streaming-scheduler.js.map +1 -1
  235. package/dist/shared/subagent-rows.d.ts +2 -2
  236. package/dist/shared/subagent-rows.js +2 -2
  237. package/dist/shared/synthesized-attachment.d.ts +30 -3
  238. package/dist/shared/synthesized-attachment.js +26 -0
  239. package/dist/shared/synthesized-attachment.js.map +1 -1
  240. package/dist/shared/todos.d.ts +3 -3
  241. package/dist/shared/todos.js +3 -3
  242. package/dist/shared/tool-row.d.ts +19 -1
  243. package/dist/shared/tool-row.js +36 -2
  244. package/dist/shared/tool-row.js.map +1 -1
  245. package/dist/shared/worker-shutdown.d.ts +1 -1
  246. package/dist/shared/worker-shutdown.js +1 -1
  247. package/dist/shared/workspace/session-provision.d.ts +1 -1
  248. package/dist/shared/workspace/session-provision.js +1 -1
  249. package/dist/shared/workspace/sources/empty.d.ts +1 -1
  250. package/dist/shared/workspace/sources/empty.js +1 -2
  251. package/dist/shared/workspace/sources/empty.js.map +1 -1
  252. package/dist/shared/workspace/sources/git.js +4 -4
  253. package/dist/shared/workspace/sources/git.js.map +1 -1
  254. package/dist/shared/workspace/sources/local-path.js +6 -6
  255. package/dist/shared/workspace/sources/local-path.js.map +1 -1
  256. package/dist/shared/workspace/stigmer-link.d.ts +1 -1
  257. package/dist/shared/workspace/stigmer-link.js +1 -1
  258. package/dist/shared/workspace/types.d.ts +2 -5
  259. package/dist/shared/workspace/types.js +0 -6
  260. package/dist/shared/workspace/types.js.map +1 -1
  261. package/dist/shared/workspace/writeback-coordinator.d.ts +17 -4
  262. package/dist/shared/workspace/writeback-coordinator.js +7 -9
  263. package/dist/shared/workspace/writeback-coordinator.js.map +1 -1
  264. package/dist/worker.js +10 -3
  265. package/dist/worker.js.map +1 -1
  266. package/dist/workflow-source.d.ts +10 -0
  267. package/dist/workflow-source.js +12 -0
  268. package/dist/workflow-source.js.map +1 -1
  269. package/dist/workflows/execute-from-execution.d.ts +15 -0
  270. package/dist/workflows/execute-from-execution.js +11 -0
  271. package/dist/workflows/execute-from-execution.js.map +1 -1
  272. package/dist/workflows/index.d.ts +5 -3
  273. package/dist/workflows/index.js +5 -3
  274. package/dist/workflows/index.js.map +1 -1
  275. package/dist/workflows/interceptors/run-credential.d.ts +34 -0
  276. package/dist/workflows/interceptors/run-credential.js +71 -0
  277. package/dist/workflows/interceptors/run-credential.js.map +1 -0
  278. package/package.json +4 -4
  279. package/src/__test-utils__/__tests__/execution-record-barrier.test.ts +70 -0
  280. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +35 -1
  281. package/src/__test-utils__/approval-contract/contract.ts +1 -1
  282. package/src/__test-utils__/approval-contract/types.ts +1 -1
  283. package/src/__test-utils__/harness-boot-order-child.ts +1 -1
  284. package/src/__test-utils__/harness-contract/contract.ts +8 -8
  285. package/src/__test-utils__/harness-contract/recording-sink.ts +11 -1
  286. package/src/__test-utils__/harness-contract/runtime-contract.ts +52 -16
  287. package/src/__test-utils__/harness-contract/scripted-adapter.ts +47 -43
  288. package/src/__test-utils__/hermetic-activity.ts +113 -9
  289. package/src/__test-utils__/model-registry-fixture.ts +31 -7
  290. package/src/__test-utils__/module-specifiers.ts +32 -3
  291. package/src/__tests__/harness-boot-order.test.ts +1 -1
  292. package/src/__tests__/harness-contract.test.ts +1 -1
  293. package/src/__tests__/run-credential-e2e.test.ts +233 -0
  294. package/src/__tests__/run-credential-registration.test.ts +80 -0
  295. package/src/__tests__/runner-manager.test.ts +1 -1
  296. package/src/activities/__tests__/generate-session-subject-wire.test.ts +119 -0
  297. package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +1 -1
  298. package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +2 -2
  299. package/src/activities/execute-cursor/__test-utils__/fold.ts +116 -0
  300. package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +1 -1
  301. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +67 -17
  302. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +19 -5
  303. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +2 -1
  304. package/src/activities/execute-cursor/__tests__/approval-state.test.ts +14 -21
  305. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +3 -3
  306. package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +2 -1
  307. package/src/activities/execute-cursor/__tests__/cursor-event-recorder.test.ts +142 -0
  308. package/src/activities/execute-cursor/__tests__/cursor-fetch-interceptor-bypass.test.ts +19 -8
  309. package/src/activities/execute-cursor/__tests__/cursor-sdk-auth-smoke.test.ts +10 -8
  310. package/src/activities/execute-cursor/__tests__/cursor-transcript-live.test.ts +298 -0
  311. package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +4 -2
  312. package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +1 -1
  313. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +1 -1
  314. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -1
  315. package/src/activities/execute-cursor/__tests__/hermetic/goldens/mcp-tool-call.status.json +71 -0
  316. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-task-note.status.json +73 -0
  317. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +78 -0
  318. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +72 -0
  319. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +72 -0
  320. package/src/activities/execute-cursor/__tests__/hermetic/goldens/sub-agent-delegation.status.json +109 -0
  321. package/src/activities/execute-cursor/__tests__/hermetic/goldens/thinking-turn.status.json +78 -0
  322. package/src/activities/execute-cursor/__tests__/hermetic/goldens/todo-write.status.json +119 -0
  323. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-error.status.json +68 -0
  324. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-only-step.status.json +67 -0
  325. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +1 -1
  326. package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +7 -7
  327. package/src/activities/execute-cursor/__tests__/hermetic/mcp-tool-call.test.ts +146 -0
  328. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +5 -5
  329. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +2 -1
  330. package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +2 -1
  331. package/src/activities/execute-cursor/__tests__/hermetic/sdk-task-note.test.ts +122 -0
  332. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +276 -0
  333. package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +8 -8
  334. package/src/activities/execute-cursor/__tests__/hermetic/sub-agent-delegation.test.ts +196 -0
  335. package/src/activities/execute-cursor/__tests__/hermetic/thinking-turn.test.ts +145 -0
  336. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +1 -1
  337. package/src/activities/execute-cursor/__tests__/hermetic/todo-write.test.ts +165 -0
  338. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +3 -3
  339. package/src/activities/execute-cursor/__tests__/hermetic/tool-error.test.ts +138 -0
  340. package/src/activities/execute-cursor/__tests__/hermetic/tool-only-step.test.ts +130 -0
  341. package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +13 -1
  342. package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +2 -2
  343. package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +43 -60
  344. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +23 -28
  345. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +2 -1
  346. package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +1 -1
  347. package/src/activities/execute-cursor/__tests__/prompt-goldens.test.ts +5 -5
  348. package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +11 -13
  349. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +8 -11
  350. package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +8 -22
  351. package/src/activities/execute-cursor/__tests__/translator.test.ts +594 -0
  352. package/src/activities/execute-cursor/__tests__/turn-boundary.test.ts +8 -5
  353. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +87 -63
  354. package/src/activities/execute-cursor/adapter.ts +4 -4
  355. package/src/activities/execute-cursor/approval-policy.ts +12 -24
  356. package/src/activities/execute-cursor/approval-state.ts +66 -1
  357. package/src/activities/execute-cursor/boundary-rows.ts +1132 -0
  358. package/src/activities/execute-cursor/cursor-capabilities.ts +1 -1
  359. package/src/activities/execute-cursor/cursor-event-recorder.ts +57 -28
  360. package/src/activities/execute-cursor/fetch-interceptor.ts +1 -1
  361. package/src/activities/execute-cursor/http2-interceptor.ts +1 -1
  362. package/src/activities/execute-cursor/prompt-builder.ts +6 -6
  363. package/src/activities/execute-cursor/service-tier.ts +1 -2
  364. package/src/activities/execute-cursor/sub-agent-steps.ts +188 -0
  365. package/src/activities/execute-cursor/tool-result.ts +128 -0
  366. package/src/activities/execute-cursor/translator.ts +541 -0
  367. package/src/activities/execute-cursor/turn-boundary.ts +21 -14
  368. package/src/activities/execute-cursor/turn-settle.ts +46 -34
  369. package/src/activities/execute-cursor/turn-setup.ts +5 -5
  370. package/src/activities/execute-cursor/turn-stream.ts +49 -46
  371. package/src/activities/execute-cursor/turn.ts +5 -5
  372. package/src/activities/execute-cursor/usage-pricing.ts +2 -2
  373. package/src/activities/execute-deep-agent/__test-utils__/contract-subject.ts +2 -2
  374. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +23 -11
  375. package/src/activities/execute-deep-agent/__test-utils__/hitl-script.ts +1 -1
  376. package/src/activities/execute-deep-agent/__test-utils__/scripted-model-module.ts +2 -3
  377. package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +25 -4
  378. package/src/activities/execute-deep-agent/__test-utils__/v3-event-fixtures.ts +25 -11
  379. package/src/activities/execute-deep-agent/__tests__/adapter-graph-is-sdk-free.test.ts +1 -1
  380. package/src/activities/execute-deep-agent/__tests__/adapter-is-temporal-free.test.ts +1 -1
  381. package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +6 -17
  382. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +25 -8
  383. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn1.status.json +2 -7
  384. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve-all-lease.turn2.status.json +4 -14
  385. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/approve.turn2.status.json +1 -5
  386. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +4 -7
  387. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/gate.turn1.status.json +1 -5
  388. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +2 -9
  389. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +2 -9
  390. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +1 -4
  391. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +1 -4
  392. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/platform-stop.status.json +1 -4
  393. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +109 -76
  394. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/reject.turn2.status.json +1 -5
  395. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn2.status.json +2 -10
  396. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sequential-gates.turn3.status.json +2 -10
  397. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/skip.turn2.status.json +1 -5
  398. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/sub-agent-delegation.status.json +3 -6
  399. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/tool-call.status.json +1 -4
  400. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/worker-shutdown.status.json +1 -4
  401. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/write-todos.status.json +92 -0
  402. package/src/activities/execute-deep-agent/__tests__/hermetic/harness-contract.test.ts +3 -3
  403. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-approve-all-lease.test.ts +9 -5
  404. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-round-trips.test.ts +3 -3
  405. package/src/activities/execute-deep-agent/__tests__/hermetic/hitl-sequential-gates.test.ts +3 -3
  406. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +7 -6
  407. package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +10 -11
  408. package/src/activities/execute-deep-agent/__tests__/hermetic/plain-turn.test.ts +4 -4
  409. package/src/activities/execute-deep-agent/__tests__/hermetic/platform-stop.test.ts +9 -8
  410. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +21 -13
  411. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +2 -2
  412. package/src/activities/execute-deep-agent/__tests__/hermetic/structured-output.test.ts +7 -7
  413. package/src/activities/execute-deep-agent/__tests__/hermetic/sub-agent-delegation.test.ts +28 -12
  414. package/src/activities/execute-deep-agent/__tests__/hermetic/tool-call.test.ts +21 -21
  415. package/src/activities/execute-deep-agent/__tests__/hermetic/workspace-lock-timeout.test.ts +5 -5
  416. package/src/activities/execute-deep-agent/__tests__/hermetic/write-todos.test.ts +160 -0
  417. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +35 -30
  418. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +3 -4
  419. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +12 -10
  420. package/src/activities/execute-deep-agent/__tests__/scripted-model.test.ts +13 -3
  421. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +26 -18
  422. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +1 -1
  423. package/src/activities/execute-deep-agent/__tests__/{subagent-tracker.test.ts → translator-subagents.test.ts} +76 -57
  424. package/src/activities/execute-deep-agent/__tests__/{v3-status-builder.test.ts → translator-through-builder.test.ts} +213 -299
  425. package/src/activities/execute-deep-agent/__tests__/translator.test.ts +505 -0
  426. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +11 -29
  427. package/src/activities/execute-deep-agent/adapter.ts +1 -1
  428. package/src/activities/execute-deep-agent/approval-file-change.ts +20 -22
  429. package/src/activities/execute-deep-agent/deep-agent-capabilities.ts +2 -2
  430. package/src/activities/execute-deep-agent/hitl.ts +2 -2
  431. package/src/activities/execute-deep-agent/inline-publisher.ts +16 -5
  432. package/src/activities/execute-deep-agent/prompt-builder.ts +6 -7
  433. package/src/activities/execute-deep-agent/streaming-side-effects.ts +22 -31
  434. package/src/activities/execute-deep-agent/subagent-transformer.ts +3 -3
  435. package/src/activities/execute-deep-agent/translator.ts +463 -0
  436. package/src/activities/execute-deep-agent/turn-settle.ts +35 -43
  437. package/src/activities/execute-deep-agent/turn-setup.ts +8 -8
  438. package/src/activities/execute-deep-agent/turn-stream.ts +53 -51
  439. package/src/activities/execute-deep-agent/turn.ts +3 -3
  440. package/src/activities/generate-session-subject.ts +29 -4
  441. package/src/client/__tests__/stigmer-client.test.ts +92 -0
  442. package/src/client/stigmer-client.ts +48 -14
  443. package/src/config.ts +6 -3
  444. package/src/harness/README.md +335 -0
  445. package/src/harness/__tests__/approval-decisions.test.ts +2 -2
  446. package/src/harness/__tests__/capture.test.ts +3 -3
  447. package/src/harness/__tests__/import-direction.test.ts +3 -2
  448. package/src/harness/__tests__/mount-skills.test.ts +4 -1
  449. package/src/harness/__tests__/readme-index.test.ts +339 -0
  450. package/src/harness/__tests__/run-turn.test.ts +131 -6
  451. package/src/harness/__tests__/transcript-is-engine-free.test.ts +83 -0
  452. package/src/harness/__tests__/transcript-writers.test.ts +191 -0
  453. package/src/harness/__tests__/turn-attachments-vision.test.ts +5 -2
  454. package/src/harness/__tests__/turn-context.test.ts +5 -5
  455. package/src/harness/approval-decisions.ts +1 -1
  456. package/src/harness/capabilities.ts +21 -11
  457. package/src/harness/capture.ts +4 -4
  458. package/src/harness/persist-chokepoint.ts +3 -3
  459. package/src/harness/registry.ts +2 -2
  460. package/src/harness/run-turn.ts +33 -9
  461. package/src/harness/stop-controller.ts +1 -1
  462. package/src/harness/terminal-table.ts +7 -7
  463. package/src/harness/transcript/__tests__/builder.test.ts +975 -0
  464. package/src/harness/transcript/__tests__/state.test.ts +143 -0
  465. package/src/harness/transcript/builder.ts +755 -0
  466. package/src/harness/transcript/events.ts +262 -0
  467. package/src/harness/transcript/state.ts +167 -0
  468. package/src/harness/turn-context.ts +61 -46
  469. package/src/harness/types.ts +64 -22
  470. package/src/harness/usage-accumulator.ts +2 -2
  471. package/src/harness-adapters.ts +5 -4
  472. package/src/interceptors/__tests__/run-credential-activity.test.ts +133 -0
  473. package/src/interceptors/run-credential-activity.ts +67 -0
  474. package/src/middleware/__tests__/cost-advisory.test.ts +2 -2
  475. package/src/middleware/approval-gate.ts +7 -74
  476. package/src/middleware/cost-advisory.ts +3 -3
  477. package/src/middleware/index.ts +1 -1
  478. package/src/runner-manager.ts +14 -4
  479. package/src/shared/__tests__/approval-policy.test.ts +45 -0
  480. package/src/shared/__tests__/args-preview.test.ts +40 -20
  481. package/src/shared/__tests__/attachment-naming.test.ts +1 -1
  482. package/src/shared/__tests__/attachment-resolver.test.ts +5 -5
  483. package/src/shared/__tests__/attachment-zip.test.ts +1 -1
  484. package/src/shared/__tests__/blueprint-resolver.test.ts +1 -1
  485. package/src/shared/__tests__/channel-attachment.test.ts +12 -0
  486. package/src/shared/__tests__/cost-guard.test.ts +1 -1
  487. package/src/shared/__tests__/exact-apply.test.ts +2 -1
  488. package/src/shared/__tests__/memory-attachment.test.ts +17 -0
  489. package/src/shared/__tests__/persist-decision.test.ts +19 -33
  490. package/src/shared/__tests__/prompt-sections.test.ts +1 -1
  491. package/src/shared/__tests__/run-credential.test.ts +111 -0
  492. package/src/shared/__tests__/stamp-flowed-rows.test.ts +1 -1
  493. package/src/shared/__tests__/status-offload.test.ts +1 -1
  494. package/src/shared/__tests__/streaming-scheduler.test.ts +8 -9
  495. package/src/shared/__tests__/todos.test.ts +1 -1
  496. package/src/shared/__tests__/tool-row.test.ts +1 -1
  497. package/src/shared/activity-input.ts +21 -0
  498. package/src/shared/approval-policy.ts +95 -0
  499. package/src/shared/args-preview.ts +51 -42
  500. package/src/shared/attachment-resolver.ts +4 -4
  501. package/src/shared/attachment-zip.ts +2 -2
  502. package/src/shared/channel-attachment.ts +14 -6
  503. package/src/shared/cost-guard.ts +2 -2
  504. package/src/shared/exact-apply.ts +1 -1
  505. package/src/shared/execution-context.ts +2 -2
  506. package/src/shared/extract-structured-output.ts +2 -2
  507. package/src/shared/filereview/__tests__/capture.test.ts +9 -5
  508. package/src/shared/filereview/__tests__/git-substrate.test.ts +4 -1
  509. package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +3 -3
  510. package/src/shared/filereview/cas-touched.ts +2 -2
  511. package/src/shared/filereview/progress.ts +13 -0
  512. package/src/shared/heartbeat.ts +1 -1
  513. package/src/shared/llm-backend.ts +0 -4
  514. package/src/shared/memory-attachment.ts +22 -10
  515. package/src/shared/model-error.ts +1 -2
  516. package/src/shared/persist-decision.ts +19 -28
  517. package/src/shared/prompt-sections.ts +4 -4
  518. package/src/shared/run-credential-store.ts +45 -0
  519. package/src/shared/run-credential.ts +70 -0
  520. package/src/shared/skill-resolver.ts +3 -3
  521. package/src/shared/status-offload.ts +2 -2
  522. package/src/shared/streaming-scheduler.ts +8 -13
  523. package/src/shared/subagent-rows.ts +2 -2
  524. package/src/shared/synthesized-attachment.ts +38 -3
  525. package/src/shared/todos.ts +3 -3
  526. package/src/shared/tool-row.ts +38 -2
  527. package/src/shared/worker-shutdown.ts +1 -1
  528. package/src/shared/workspace/__tests__/git-source.test.ts +3 -3
  529. package/src/shared/workspace/__tests__/provisioner.test.ts +5 -5
  530. package/src/shared/workspace/__tests__/session-provision.test.ts +2 -2
  531. package/src/shared/workspace/__tests__/writeback-coordinator.integration.test.ts +8 -9
  532. package/src/shared/workspace/__tests__/writeback-coordinator.test.ts +25 -25
  533. package/src/shared/workspace/provisioner.ts +1 -1
  534. package/src/shared/workspace/session-provision.ts +1 -1
  535. package/src/shared/workspace/sources/empty.ts +2 -2
  536. package/src/shared/workspace/sources/git.ts +3 -4
  537. package/src/shared/workspace/sources/local-path.ts +5 -6
  538. package/src/shared/workspace/stigmer-link.ts +1 -1
  539. package/src/shared/workspace/types.ts +2 -5
  540. package/src/shared/workspace/writeback-coordinator.ts +23 -12
  541. package/src/worker.ts +15 -3
  542. package/src/workflow-source.ts +15 -0
  543. package/src/workflows/__tests__/run-credential-interceptor.test.ts +178 -0
  544. package/src/workflows/execute-from-execution.ts +17 -0
  545. package/src/workflows/index.ts +5 -3
  546. package/src/workflows/interceptors/run-credential.ts +101 -0
  547. package/dist/activities/execute-cursor/delta-enricher.d.ts +0 -87
  548. package/dist/activities/execute-cursor/delta-enricher.js +0 -265
  549. package/dist/activities/execute-cursor/delta-enricher.js.map +0 -1
  550. package/dist/activities/execute-cursor/message-translator.js +0 -2040
  551. package/dist/activities/execute-cursor/message-translator.js.map +0 -1
  552. package/dist/activities/execute-cursor/todo-tracker.d.ts +0 -40
  553. package/dist/activities/execute-cursor/todo-tracker.js +0 -80
  554. package/dist/activities/execute-cursor/todo-tracker.js.map +0 -1
  555. package/dist/activities/execute-deep-agent/execution-state.d.ts +0 -54
  556. package/dist/activities/execute-deep-agent/execution-state.js +0 -68
  557. package/dist/activities/execute-deep-agent/execution-state.js.map +0 -1
  558. package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +0 -65
  559. package/dist/activities/execute-deep-agent/status-builder-shared.js +0 -90
  560. package/dist/activities/execute-deep-agent/status-builder-shared.js.map +0 -1
  561. package/dist/activities/execute-deep-agent/subagent-tracker.d.ts +0 -113
  562. package/dist/activities/execute-deep-agent/subagent-tracker.js +0 -400
  563. package/dist/activities/execute-deep-agent/subagent-tracker.js.map +0 -1
  564. package/dist/activities/execute-deep-agent/v3-events.d.ts +0 -102
  565. package/dist/activities/execute-deep-agent/v3-events.js +0 -35
  566. package/dist/activities/execute-deep-agent/v3-events.js.map +0 -1
  567. package/dist/activities/execute-deep-agent/v3-protocol-normalizer.d.ts +0 -15
  568. package/dist/activities/execute-deep-agent/v3-protocol-normalizer.js +0 -235
  569. package/dist/activities/execute-deep-agent/v3-protocol-normalizer.js.map +0 -1
  570. package/dist/activities/execute-deep-agent/v3-status-builder.d.ts +0 -111
  571. package/dist/activities/execute-deep-agent/v3-status-builder.js +0 -457
  572. package/dist/activities/execute-deep-agent/v3-status-builder.js.map +0 -1
  573. package/dist/shared/execution-status-writer.d.ts +0 -31
  574. package/dist/shared/execution-status-writer.js +0 -43
  575. package/dist/shared/execution-status-writer.js.map +0 -1
  576. package/src/activities/execute-cursor/__tests__/delta-enricher.test.ts +0 -310
  577. package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -1033
  578. package/src/activities/execute-cursor/__tests__/todo-tracker.test.ts +0 -498
  579. package/src/activities/execute-cursor/delta-enricher.ts +0 -307
  580. package/src/activities/execute-cursor/message-translator.ts +0 -2319
  581. package/src/activities/execute-cursor/todo-tracker.ts +0 -91
  582. package/src/activities/execute-deep-agent/__tests__/execution-state-extended.test.ts +0 -148
  583. package/src/activities/execute-deep-agent/__tests__/execution-state.test.ts +0 -154
  584. package/src/activities/execute-deep-agent/__tests__/status-builder-shared.test.ts +0 -47
  585. package/src/activities/execute-deep-agent/__tests__/v3-protocol-normalizer.test.ts +0 -324
  586. package/src/activities/execute-deep-agent/execution-state.ts +0 -78
  587. package/src/activities/execute-deep-agent/status-builder-shared.ts +0 -142
  588. package/src/activities/execute-deep-agent/subagent-tracker.ts +0 -479
  589. package/src/activities/execute-deep-agent/v3-events.ts +0 -153
  590. package/src/activities/execute-deep-agent/v3-protocol-normalizer.ts +0 -264
  591. package/src/activities/execute-deep-agent/v3-status-builder.ts +0 -565
  592. package/src/shared/execution-status-writer.ts +0 -56
@@ -0,0 +1,975 @@
1
+ /**
2
+ * `TranscriptBuilder` (`harness/transcript/builder.ts`), driven with
3
+ * `TranscriptEvent`s directly — one arm per rule the builder's header lists,
4
+ * in the order it lists them. These are the arms a third harness's author
5
+ * reads to learn what a translator's events do to the transcript; the arms
6
+ * that drive the builder through a real translator over a real wire are
7
+ * the translator's own (`execute-deep-agent/__tests__/`, the native
8
+ * translator's integration arms).
9
+ *
10
+ * Fence-clean by construction: this file names the proto runtime, the
11
+ * generated protos, `vitest` and the builder — nothing of any engine
12
+ * (`harness/__tests__/transcript-is-engine-free.test.ts`).
13
+ *
14
+ * Opened in #1097 with the row rules that are byte-identical on native
15
+ * and therefore have no golden (the upsert, the output stream, the gated
16
+ * `tool_error` stamp); `approval_proposed` and `system_note`, the every-row
17
+ * preview, the todo projection, and the rest of the header's rules —
18
+ * the row per id, the message boundary, the thinking row, the sub-agent
19
+ * rows, finalize, artifacts and write-backs, the dirty flag, the error
20
+ * guard — joined commit by commit in the same PR.
21
+ */
22
+
23
+ import { describe, it, expect } from "vitest";
24
+ import { create } from "@bufbuild/protobuf";
25
+ import { type AgentExecutionStatus, AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
26
+ import {
27
+ ApprovalAction,
28
+ ApprovalPolicySource,
29
+ MessageType,
30
+ SubAgentStatus,
31
+ TodoStatus,
32
+ ToolCallStatus,
33
+ ToolCallStreamingSource,
34
+ ToolKind,
35
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
36
+ import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
37
+ import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
38
+ import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
39
+ import { WorkspaceWriteBackSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
40
+ import { TranscriptBuilder } from "../builder.js";
41
+ import type { TranscriptEvent } from "../events.js";
42
+
43
+ /**
44
+ * A builder and the status it builds into, as a pair: the test writes through
45
+ * `sb` and reads `status` — the two handles production holds as
46
+ * `TurnSink.transcript` and `TurnSink.status`. The builder hands nothing back.
47
+ */
48
+ interface Fold {
49
+ readonly sb: TranscriptBuilder;
50
+ readonly status: AgentExecutionStatus;
51
+ }
52
+
53
+ function builder(): Fold {
54
+ const status = create(AgentExecutionStatusSchema, {});
55
+ return { sb: new TranscriptBuilder("exec-test", status), status };
56
+ }
57
+
58
+ function feed(fold: Fold, events: TranscriptEvent[]): Fold {
59
+ for (const e of events) fold.sb.apply(e);
60
+ return fold;
61
+ }
62
+
63
+ /** A text turn that proposes a tool, then the tool's start. */
64
+ function proposedCall(callId = "call-1", name = "read_file"): TranscriptEvent[] {
65
+ return [
66
+ { kind: "message_start", runId: "run-1" },
67
+ { kind: "text_delta", runId: "run-1", text: "Let me look." },
68
+ { kind: "message_finish", runId: "run-1" },
69
+ { kind: "tool_started", callId, name, input: { path: "/x" }, mcpServerSlug: "" },
70
+ ];
71
+ }
72
+
73
+ function rowOf(status: AgentExecutionStatus, callId: string) {
74
+ const row = status.messages.flatMap((m) => m.toolCalls).find((tc) => tc.id === callId);
75
+ if (!row) throw new Error(`no row ${callId}`);
76
+ return row;
77
+ }
78
+
79
+ describe("TranscriptBuilder — tool_finished and tool_error are upserts", () => {
80
+ it("a finish completes the row, stamps completedAt once and stores the result", () => {
81
+ const { status } = feed(builder(), [...proposedCall(), { kind: "tool_finished", callId: "call-1", result: "contents" }]);
82
+ const row = rowOf(status, "call-1");
83
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
84
+ expect(row.result).toBe("contents");
85
+ expect(row.completedAt).toContain("T");
86
+ expect(row.isStreaming).toBe(false);
87
+ });
88
+
89
+ it("a second completion never regresses a settled status and never clears a result with an empty one", () => {
90
+ const { status } = feed(builder(), [
91
+ ...proposedCall(),
92
+ { kind: "tool_error", callId: "call-1", message: "denied" },
93
+ { kind: "tool_finished", callId: "call-1", result: "" },
94
+ ]);
95
+ const row = rowOf(status, "call-1");
96
+ expect(row.status, "FAILED stays FAILED").toBe(ToolCallStatus.TOOL_CALL_FAILED);
97
+ expect(row.error).toBe("denied");
98
+ expect(row.result).toBe("");
99
+ });
100
+
101
+ it("a non-empty later result overwrites an earlier one; completedAt keeps its first stamp", () => {
102
+ const { status } = feed(builder(), [
103
+ ...proposedCall(),
104
+ { kind: "tool_finished", callId: "call-1", result: "partial" },
105
+ { kind: "tool_finished", callId: "call-1", result: "" },
106
+ { kind: "tool_finished", callId: "call-1", result: "final" },
107
+ ]);
108
+ const row = rowOf(status, "call-1");
109
+ expect(row.result).toBe("final");
110
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
111
+ expect(row.completedAt).not.toBe("");
112
+ });
113
+
114
+ it("an error fails the row with its message; an empty message never clears an earlier one", () => {
115
+ const { status } = feed(builder(), [
116
+ ...proposedCall(),
117
+ { kind: "tool_error", callId: "call-1", message: "EACCES" },
118
+ { kind: "tool_error", callId: "call-1", message: "" },
119
+ ]);
120
+ const row = rowOf(status, "call-1");
121
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
122
+ expect(row.error).toBe("EACCES");
123
+ });
124
+
125
+ it("a gated row's error stamps approvalRequestedAt once — the boundary's shape (Cursor)", () => {
126
+ const { sb, status } = feed(builder(), [
127
+ { kind: "message_start", runId: "run-1" },
128
+ { kind: "text_delta", runId: "run-1", text: "Running the build." },
129
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } },
130
+ { kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
131
+ ]);
132
+ const row = rowOf(status, "sh-1");
133
+ expect(row.requiresApproval).toBe(true);
134
+ expect(row.approvalMessage).toBe("Run command: make");
135
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
136
+ const stamped = row.approvalRequestedAt;
137
+ expect(stamped).toContain("T");
138
+ sb.apply({ kind: "tool_error", callId: "sh-1", message: "again" });
139
+ expect(rowOf(status, "sh-1").approvalRequestedAt, "stamped once").toBe(stamped);
140
+ });
141
+
142
+ it("an ungated row's error stamps no approvalRequestedAt", () => {
143
+ const { status } = feed(builder(), [...proposedCall(), { kind: "tool_error", callId: "call-1", message: "x" }]);
144
+ expect(rowOf(status, "call-1").approvalRequestedAt).toBe("");
145
+ });
146
+
147
+ it("a finish or an error the harness observed earlier stamps the observed instant, not the fold's", () => {
148
+ const { sb, status } = feed(builder(), [
149
+ ...proposedCall("obs-1"),
150
+ { kind: "tool_finished", callId: "obs-1", result: "", observedAt: "2026-01-01T00:00:07.000Z" },
151
+ ]);
152
+ expect(rowOf(status, "obs-1").completedAt, "the delta's own instant").toBe("2026-01-01T00:00:07.000Z");
153
+ sb.apply({ kind: "tool_started", callId: "obs-2", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } });
154
+ sb.apply({ kind: "tool_error", callId: "obs-2", message: "", observedAt: "2026-01-01T00:00:09.000Z" });
155
+ const gated = rowOf(status, "obs-2");
156
+ expect(gated.status).toBe(ToolCallStatus.TOOL_CALL_FAILED);
157
+ expect(gated.completedAt).toBe("2026-01-01T00:00:09.000Z");
158
+ expect(gated.approvalRequestedAt, "the gate's stamp shares the observed instant").toBe("2026-01-01T00:00:09.000Z");
159
+ // The stream's own error, arriving later with the text, fills the message
160
+ // and moves no stamp.
161
+ sb.apply({ kind: "tool_error", callId: "obs-2", message: "blocked" });
162
+ expect(rowOf(status, "obs-2").error).toBe("blocked");
163
+ expect(rowOf(status, "obs-2").completedAt).toBe("2026-01-01T00:00:09.000Z");
164
+ });
165
+
166
+ it("a finish or an error for an unknown call is ignored", () => {
167
+ const { status } = feed(builder(), [
168
+ { kind: "tool_finished", callId: "ghost", result: "x" },
169
+ { kind: "tool_error", callId: "ghost", message: "y" },
170
+ ]);
171
+ expect(status.messages).toHaveLength(0);
172
+ });
173
+ });
174
+
175
+ describe("TranscriptBuilder — every row with args carries an elided, redacted argsPreview", () => {
176
+ it("an ungated row's preview is its args, secret keys redacted", () => {
177
+ const { status } = feed(builder(), [
178
+ { kind: "tool_started", callId: "c-1", name: "connect", input: { host: "localhost", password: "super-secret" }, mcpServerSlug: "db" },
179
+ ]);
180
+ expect(JSON.parse(rowOf(status, "c-1").argsPreview)).toEqual({ host: "localhost", password: "[REDACTED]" });
181
+ });
182
+
183
+ it("a long non-salient value is left out of the preview; a salient one (the command) survives verbatim", () => {
184
+ const command = "echo " + "x".repeat(400);
185
+ const { status } = feed(builder(), [
186
+ { kind: "tool_started", callId: "sh-1", name: "execute", input: { command, notes: "y".repeat(400) }, mcpServerSlug: "" },
187
+ ]);
188
+ const preview = JSON.parse(rowOf(status, "sh-1").argsPreview);
189
+ expect(preview.command).toBe(command);
190
+ // Absent, never an in-band marker (stigmer#1107); the row's args carry it.
191
+ expect("notes" in preview).toBe(false);
192
+ expect(rowOf(status, "sh-1").args?.notes).toBe("y".repeat(400));
193
+ });
194
+
195
+ it("a row with no args carries no preview", () => {
196
+ const { status } = feed(builder(), [{ kind: "tool_started", callId: "n-1", name: "ls", input: {}, mcpServerSlug: "" }]);
197
+ expect(rowOf(status, "n-1").argsPreview).toBe("");
198
+ expect(rowOf(status, "n-1").args).toBeUndefined();
199
+ });
200
+
201
+ it("unserializable args leave the preview unset instead of failing the row", () => {
202
+ const cyclic: Record<string, unknown> = {};
203
+ cyclic.self = cyclic;
204
+ const { status } = feed(builder(), [{ kind: "tool_started", callId: "cy-1", name: "read", input: cyclic, mcpServerSlug: "" }]);
205
+ expect(rowOf(status, "cy-1").status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
206
+ expect(rowOf(status, "cy-1").argsPreview).toBe("");
207
+ });
208
+
209
+ it("a sub-agent's row carries a preview too — every row, every scope", () => {
210
+ const { status } = feed(builder(), [
211
+ { kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "s", input: "s" },
212
+ { kind: "tool_started", subAgentId: "task-1", callId: "g-1", name: "grep", input: { pattern: "x" }, mcpServerSlug: "" },
213
+ ]);
214
+ const row = status.subAgentExecutions[0].messages[0].toolCalls[0];
215
+ expect(row.argsPreview).toBe(JSON.stringify({ pattern: "x" }));
216
+ });
217
+ });
218
+
219
+ describe("TranscriptBuilder — approval_proposed is the one place a row is ever WAITING", () => {
220
+ const proposal = (callId: string, extra: Partial<Extract<TranscriptEvent, { kind: "approval_proposed" }>> = {}) =>
221
+ ({
222
+ kind: "approval_proposed",
223
+ callId,
224
+ name: "execute",
225
+ mcpServerSlug: "",
226
+ message: "Run command: rm -rf build",
227
+ ...extra,
228
+ }) as const satisfies TranscriptEvent;
229
+
230
+ it("an unknown call gets a WAITING row on the message whose text proposed it — never a new empty message", () => {
231
+ const { sb, status } = feed(builder(), [
232
+ { kind: "message_start", runId: "run-1" },
233
+ { kind: "text_delta", runId: "run-1", text: "I will run the command." },
234
+ { kind: "message_finish", runId: "run-1" },
235
+ proposal("exec-1", { args: { command: "rm -rf build" }, provenance: "builtin_category" }),
236
+ ]);
237
+ expect(status.messages.map((m) => m.content)).toEqual(["I will run the command."]);
238
+ const row = rowOf(status, "exec-1");
239
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
240
+ expect(row.requiresApproval).toBe(true);
241
+ expect(row.approvalMessage).toBe("Run command: rm -rf build");
242
+ expect(row.approvalRequestedAt).toContain("T");
243
+ expect(row.startedAt).toContain("T");
244
+ expect(row.args).toEqual({ command: "rm -rf build" });
245
+ expect(row.argsPreview).toBe(JSON.stringify({ command: "rm -rf build" }));
246
+ expect(row.approvalPolicySource).toBe(ApprovalPolicySource.BUILTIN_CATEGORY);
247
+ expect(row.policyEngineVersion).toBe("phase-7");
248
+ expect(row.toolKind).toBe(ToolKind.SHELL);
249
+ expect(sb.awaitingApproval, "the fact the caller reads").toBe(true);
250
+ });
251
+
252
+ it("with no message in the scope yet, the row gets an empty AI message (the boundary's fallback)", () => {
253
+ const { status } = feed(builder(), [proposal("exec-1")]);
254
+ expect(status.messages.map((m) => [m.type, m.content])).toEqual([[MessageType.MESSAGE_AI, ""]]);
255
+ expect(rowOf(status, "exec-1").status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
256
+ });
257
+
258
+ it("carries the MCP server, and the digest when the harness has one", () => {
259
+ const { status } = feed(builder(), [
260
+ proposal("mcp-1", { name: "create_issue", mcpServerSlug: "github", provenance: "agent_override", contentDigest: "sha-1" }),
261
+ ]);
262
+ const row = rowOf(status, "mcp-1");
263
+ expect(row.mcpServerSlug).toBe("github");
264
+ expect(row.toolKind).toBe(ToolKind.MCP);
265
+ expect(row.approvalContentDigest).toBe("sha-1");
266
+ expect(row.approvalPolicySource).toBe(ApprovalPolicySource.AGENT_OVERRIDE);
267
+ });
268
+
269
+ it("a known row REOPENS: WAITING, the outcome fields cleared, approvalRequestedAt stamped once (Cursor's denial overlay)", () => {
270
+ const { sb, status } = feed(builder(), [
271
+ { kind: "message_start", runId: "run-1" },
272
+ { kind: "text_delta", runId: "run-1", text: "Running the build." },
273
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: { command: "make" }, mcpServerSlug: "", gate: { message: "Run command: make" } },
274
+ { kind: "tool_error", callId: "sh-1", message: "Blocked by hook" },
275
+ ]);
276
+ const stamped = rowOf(status, "sh-1").approvalRequestedAt;
277
+ sb.apply(proposal("sh-1", { name: "Shell", message: "Run command: make", contentDigest: "d-1" }));
278
+ const row = rowOf(status, "sh-1");
279
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
280
+ expect(row.error).toBe("");
281
+ expect(row.result).toBe("");
282
+ expect(row.completedAt).toBe("");
283
+ expect(row.approvalRequestedAt, "the first stamp stands").toBe(stamped);
284
+ expect(row.approvalContentDigest).toBe("d-1");
285
+ expect(status.messages.flatMap((m) => m.toolCalls), "no second row").toHaveLength(1);
286
+ expect(sb.awaitingApproval).toBe(true);
287
+ });
288
+
289
+ it("a known row's reopen takes the proposal's args and re-derives the preview when carried; keeps its own when not", () => {
290
+ const { sb, status } = feed(builder(), [
291
+ { kind: "message_start", runId: "run-1" },
292
+ { kind: "text_delta", runId: "run-1", text: "Editing notes." },
293
+ { kind: "tool_started", callId: "ed-1", name: "edit", input: { path: "notes.md", content: "partial" }, mcpServerSlug: "", gate: { message: "Write file: notes.md" } },
294
+ { kind: "tool_error", callId: "ed-1", message: "Blocked by hook" },
295
+ ]);
296
+ // The boundary's overlay carries the hook's captured input — the whole
297
+ // proposed change, where the stream had only what streamed before the cancel.
298
+ sb.apply(proposal("ed-1", { name: "edit", message: "Write file: notes.md", args: { path: "notes.md", content: "the whole file" } }));
299
+ const row = rowOf(status, "ed-1");
300
+ expect(row.args).toEqual({ path: "notes.md", content: "the whole file" });
301
+ expect(row.argsPreview).toBe(JSON.stringify({ path: "notes.md", content: "the whole file" }));
302
+ // A proposal with no args leaves the row's own in place.
303
+ sb.apply(proposal("ed-1", { name: "edit", message: "Write file: notes.md" }));
304
+ expect(rowOf(status, "ed-1").args).toEqual({ path: "notes.md", content: "the whole file" });
305
+ });
306
+
307
+ it("proposing a call the stream already showed never duplicates it", () => {
308
+ const { status } = feed(builder(), [...proposedCall("exec-1", "execute"), proposal("exec-1")]);
309
+ expect(status.messages.flatMap((m) => m.toolCalls).map((tc) => tc.id)).toEqual(["exec-1"]);
310
+ });
311
+
312
+ it("no tool_started ever sets awaitingApproval; only a proposal does", () => {
313
+ const { sb, status } = feed(builder(), [
314
+ { kind: "tool_started", callId: "sh-1", name: "Shell", input: {}, mcpServerSlug: "", gate: { message: "Run command" } },
315
+ ]);
316
+ expect(sb.awaitingApproval).toBe(false);
317
+ expect(rowOf(status, "sh-1").status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
318
+ });
319
+ });
320
+
321
+ describe("TranscriptBuilder — a completed TODO call is projected into status.todos", () => {
322
+ const todoCall = (callId: string, name: string, args: Record<string, unknown>): TranscriptEvent[] => [
323
+ { kind: "tool_started", callId, name, input: args, mcpServerSlug: "" },
324
+ { kind: "tool_finished", callId, result: "ok" },
325
+ ];
326
+
327
+ it("a full write replaces the list, keyed todo-<i>; the row stays in the transcript", () => {
328
+ const { status } = feed(builder(), todoCall("t-1", "write_todos", { todos: [{ content: "a", status: "pending" }, { content: "b", status: "in_progress" }] }));
329
+ expect(Object.keys(status.todos).sort()).toEqual(["todo-0", "todo-1"]);
330
+ expect(status.todos["todo-1"].status).toBe(TodoStatus.TODO_IN_PROGRESS);
331
+ expect(rowOf(status, "t-1").toolKind).toBe(ToolKind.TODO);
332
+ });
333
+
334
+ it("a second full write replaces; a write with merge: true keeps what it does not name (Cursor's updateTodos)", () => {
335
+ const { sb, status } = feed(builder(), [
336
+ ...todoCall("t-1", "updateTodos", { todos: [{ id: "x", content: "first", status: "pending" }, { id: "y", content: "second", status: "pending" }] }),
337
+ ...todoCall("t-2", "updateTodos", { todos: [{ id: "y", content: "second", status: "completed" }], merge: true }),
338
+ ]);
339
+ expect(Object.keys(status.todos).sort()).toEqual(["x", "y"]);
340
+ expect(status.todos["y"].status).toBe(TodoStatus.TODO_COMPLETED);
341
+ sb.apply({ kind: "tool_started", callId: "t-3", name: "updateTodos", input: { todos: [{ id: "z", content: "only", status: "pending" }] }, mcpServerSlug: "" });
342
+ sb.apply({ kind: "tool_finished", callId: "t-3", result: "ok" });
343
+ expect(Object.keys(status.todos), "no merge flag: a full replace").toEqual(["z"]);
344
+ });
345
+
346
+ it("projects nothing before the call completes, and nothing for a sub-agent's write", () => {
347
+ const { sb, status } = feed(builder(), [
348
+ { kind: "tool_started", callId: "t-1", name: "write_todos", input: { todos: [{ content: "a", status: "pending" }] }, mcpServerSlug: "" },
349
+ ]);
350
+ expect(Object.keys(status.todos)).toEqual([]);
351
+ feed({ sb, status }, [
352
+ { kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "s", input: "s" },
353
+ ...todoCall("s-1", "write_todos", { todos: [{ content: "sub", status: "pending" }] }).map((e) => ({ ...e, subAgentId: "task-1" })),
354
+ ]);
355
+ expect(Object.keys(status.todos), "a sub-agent's list is its own").toEqual([]);
356
+ });
357
+ });
358
+
359
+ describe("TranscriptBuilder — system_note is the harness's line in its own voice", () => {
360
+ it("appends a SYSTEM message to the scope; it hosts no rows and moves no boundary", () => {
361
+ const { status } = feed(builder(), [
362
+ { kind: "message_start", runId: "run-1" },
363
+ { kind: "text_delta", runId: "run-1", text: "Working." },
364
+ { kind: "system_note", text: "The agent's changes could not be reviewed." },
365
+ { kind: "tool_started", callId: "t-1", name: "read", input: {}, mcpServerSlug: "" },
366
+ ]);
367
+ expect(status.messages.map((m) => [m.type, m.content])).toEqual([
368
+ [MessageType.MESSAGE_AI, "Working."],
369
+ [MessageType.MESSAGE_SYSTEM, "The agent's changes could not be reviewed."],
370
+ ]);
371
+ expect(status.messages[0].toolCalls.map((tc) => tc.id), "the row still joins the AI message").toEqual(["t-1"]);
372
+ expect(status.messages[1].toolCalls).toHaveLength(0);
373
+ });
374
+
375
+ it("lands in a sub-agent's transcript when scoped there", () => {
376
+ const { status } = feed(builder(), [
377
+ { kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "s", input: "s" },
378
+ { kind: "system_note", subAgentId: "task-1", text: "note" },
379
+ ]);
380
+ expect(status.messages).toHaveLength(0);
381
+ expect(status.subAgentExecutions[0].messages.map((m) => m.content)).toEqual(["note"]);
382
+ });
383
+ });
384
+
385
+ describe("TranscriptBuilder — tool_output_delta streams the row's output", () => {
386
+ it("appends the delta and marks the row streaming its OUTPUT", () => {
387
+ const { status } = feed(builder(), [
388
+ ...proposedCall("sh-1", "execute"),
389
+ { kind: "tool_output_delta", callId: "sh-1", delta: "line 1\n" },
390
+ { kind: "tool_output_delta", callId: "sh-1", delta: "line 2\n" },
391
+ ]);
392
+ const row = rowOf(status, "sh-1");
393
+ expect(row.result).toBe("line 1\nline 2\n");
394
+ expect(row.isStreaming).toBe(true);
395
+ expect(row.streamingSource).toBe(ToolCallStreamingSource.OUTPUT);
396
+ expect(row.status).toBe(ToolCallStatus.TOOL_CALL_RUNNING);
397
+ });
398
+
399
+ it("the finish closes the streaming row and clears the source; a non-empty final result replaces the streamed text", () => {
400
+ const { status } = feed(builder(), [
401
+ ...proposedCall("sh-1", "execute"),
402
+ { kind: "tool_output_delta", callId: "sh-1", delta: "partial" },
403
+ { kind: "tool_finished", callId: "sh-1", result: "partial and complete" },
404
+ ]);
405
+ const row = rowOf(status, "sh-1");
406
+ expect(row.isStreaming).toBe(false);
407
+ expect(row.streamingSource).toBe(ToolCallStreamingSource.UNSPECIFIED);
408
+ expect(row.result).toBe("partial and complete");
409
+ });
410
+
411
+ it("a chunk for a settled row is dropped — the completion's result is the whole output, and a late chunk would double it", () => {
412
+ const { status } = feed(builder(), [
413
+ ...proposedCall("sh-1", "shell"),
414
+ { kind: "tool_output_delta", callId: "sh-1", delta: "> test\n" },
415
+ { kind: "tool_finished", callId: "sh-1", result: "> test\n12 passing\n" },
416
+ { kind: "tool_output_delta", callId: "sh-1", delta: "12 passing\n" },
417
+ ]);
418
+ const row = rowOf(status, "sh-1");
419
+ expect(row.result).toBe("> test\n12 passing\n");
420
+ expect(row.isStreaming).toBe(false);
421
+ });
422
+
423
+ it("finalize() closes a row still streaming when the stream ends, and every message with it", () => {
424
+ const { sb, status } = feed(builder(), [
425
+ ...proposedCall("sh-1", "execute"),
426
+ { kind: "tool_output_delta", callId: "sh-1", delta: "never finished" },
427
+ { kind: "message_start", runId: "run-2" },
428
+ { kind: "text_delta", runId: "run-2", text: "still streaming" },
429
+ ]);
430
+ sb.finalize();
431
+ const row = rowOf(status, "sh-1");
432
+ expect(row.isStreaming).toBe(false);
433
+ expect(row.streamingSource).toBe(ToolCallStreamingSource.UNSPECIFIED);
434
+ expect(row.result, "the streamed output is kept").toBe("never finished");
435
+ expect(status.messages.every((m) => !m.isStreaming)).toBe(true);
436
+ expect(status.messages.at(-1)?.type).toBe(MessageType.MESSAGE_AI);
437
+ });
438
+ });
439
+
440
+ // ═══════════════════════════════════════════════════════════════════
441
+ // The header's rules, in its order
442
+ // ═══════════════════════════════════════════════════════════════════
443
+
444
+ describe("TranscriptBuilder — a row per callId, reconciled in place, never duplicated", () => {
445
+ it("a re-emitted start for a known row is a no-op on the row's identity; a WAITING row flips to RUNNING and takes the args it lacked", () => {
446
+ const status = create(AgentExecutionStatusSchema, {
447
+ messages: [
448
+ create(AgentMessageSchema, {
449
+ type: MessageType.MESSAGE_AI,
450
+ content: "I'll call the gated tool.",
451
+ toolCalls: [
452
+ create(ToolCallSchema, {
453
+ id: "gated-1",
454
+ name: "dangerous_tool",
455
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
456
+ requiresApproval: true,
457
+ approvalAction: ApprovalAction.APPROVE,
458
+ mcpServerSlug: "my-server",
459
+ }),
460
+ ],
461
+ }),
462
+ ],
463
+ });
464
+ const sb = new TranscriptBuilder("exec-resume", status);
465
+ // The durable checkpoint re-emits the SAME id now that approval is granted.
466
+ feed({ sb, status }, [
467
+ { kind: "tool_started", callId: "gated-1", name: "dangerous_tool", input: { target: "prod" }, mcpServerSlug: "my-server" },
468
+ { kind: "tool_finished", callId: "gated-1", result: "tool executed ok" },
469
+ { kind: "message_start", runId: "after" },
470
+ { kind: "text_delta", runId: "after", text: "All done." },
471
+ ]);
472
+ const rows = status.messages.flatMap((m) => m.toolCalls).filter((tc) => tc.id === "gated-1");
473
+ expect(rows, "exactly one copy").toHaveLength(1);
474
+ expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
475
+ expect(rows[0].args, "the resumed start fills the args the seed lacked").toEqual({ target: "prod" });
476
+ expect(rows[0].approvalAction, "the decision is kept for audit").toBe(ApprovalAction.APPROVE);
477
+ expect(status.messages.map((m) => m.content)).toEqual(["I'll call the gated tool.", "All done."]);
478
+ expect(sb.awaitingApproval, "a resumed tool never re-gates").toBe(false);
479
+ });
480
+
481
+ it("a re-emitted start advances a seeded INTERRUPTED row to RUNNING — the recovery replay's supersede — and previews the args it fills", () => {
482
+ const status = create(AgentExecutionStatusSchema, {
483
+ messages: [
484
+ create(AgentMessageSchema, {
485
+ type: MessageType.MESSAGE_AI,
486
+ content: "Running the build.",
487
+ toolCalls: [
488
+ create(ToolCallSchema, { id: "sh-1", name: "shell", status: ToolCallStatus.TOOL_CALL_INTERRUPTED, error: "interrupted by the server" }),
489
+ create(ToolCallSchema, { id: "done-1", name: "read", status: ToolCallStatus.TOOL_CALL_COMPLETED, result: "kept" }),
490
+ ],
491
+ }),
492
+ ],
493
+ });
494
+ const sb = new TranscriptBuilder("exec-recover", status);
495
+ feed({ sb, status }, [
496
+ { kind: "tool_started", callId: "sh-1", name: "shell", input: { command: "make" }, mcpServerSlug: "" },
497
+ { kind: "tool_started", callId: "done-1", name: "read", input: { path: "/x" }, mcpServerSlug: "" },
498
+ ]);
499
+ const replayed = rowOf(status, "sh-1");
500
+ expect(replayed.status, "live execution evidence outranks the interruption marker").toBe(ToolCallStatus.TOOL_CALL_RUNNING);
501
+ expect(replayed.args).toEqual({ command: "make" });
502
+ expect(replayed.argsPreview, "the filled args are previewed").toBe(JSON.stringify({ command: "make" }));
503
+ const settled = rowOf(status, "done-1");
504
+ expect(settled.status, "a settled row keeps its outcome").toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
505
+ expect(settled.result).toBe("kept");
506
+ sb.apply({ kind: "tool_finished", callId: "sh-1", result: "built" });
507
+ expect(rowOf(status, "sh-1").status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
508
+ });
509
+
510
+ it("a duplicate start for a row created this turn adds nothing", () => {
511
+ const { status } = feed(builder(), [...proposedCall(), { kind: "tool_started", callId: "call-1", name: "read_file", input: { path: "/x" }, mcpServerSlug: "" }]);
512
+ expect(status.messages.flatMap((m) => m.toolCalls)).toHaveLength(1);
513
+ });
514
+
515
+ describe("seed(): a reinvocation's prior rows, appended and indexed after the builder is born", () => {
516
+ /** Last turn's transcript as the server holds it: a text, a WAITING row, a sub-agent with its own row, an artifact, a write-back, a todo. */
517
+ function persisted() {
518
+ return create(AgentExecutionStatusSchema, {
519
+ messages: [
520
+ create(AgentMessageSchema, {
521
+ type: MessageType.MESSAGE_AI,
522
+ content: "I'll call the gated tool.",
523
+ toolCalls: [
524
+ create(ToolCallSchema, {
525
+ id: "gated-1",
526
+ name: "dangerous_tool",
527
+ status: ToolCallStatus.TOOL_CALL_WAITING_APPROVAL,
528
+ requiresApproval: true,
529
+ approvalAction: ApprovalAction.APPROVE,
530
+ }),
531
+ ],
532
+ }),
533
+ ],
534
+ subAgentExecutions: [
535
+ create(SubAgentExecutionSchema, {
536
+ id: "sub-1",
537
+ name: "researcher",
538
+ status: SubAgentStatus.SUB_AGENT_IN_PROGRESS,
539
+ messages: [
540
+ create(AgentMessageSchema, {
541
+ type: MessageType.MESSAGE_AI,
542
+ content: "Reading.",
543
+ toolCalls: [create(ToolCallSchema, { id: "sub-read", name: "read_file", status: ToolCallStatus.TOOL_CALL_RUNNING })],
544
+ }),
545
+ ],
546
+ }),
547
+ ],
548
+ artifacts: [create(ExecutionArtifactSchema, { sandboxPath: "out/a.md", contentHash: "h1" })],
549
+ workspaceWriteBacks: [create(WorkspaceWriteBackSchema, { workspaceEntryName: "repo" })],
550
+ todos: { "todo-1": { id: "todo-1", content: "first", status: TodoStatus.TODO_PENDING } },
551
+ });
552
+ }
553
+
554
+ it("appends every collection into the status's own arrays and sets no flag", () => {
555
+ const status = create(AgentExecutionStatusSchema, {});
556
+ const messages = status.messages;
557
+ const sb = new TranscriptBuilder("exec-seed", status);
558
+ sb.seed(persisted());
559
+ expect(status.messages, "pushed into, never reassigned").toBe(messages);
560
+ expect(status.messages.map((m) => m.content)).toEqual(["I'll call the gated tool."]);
561
+ expect(status.subAgentExecutions.map((s) => s.id)).toEqual(["sub-1"]);
562
+ expect(status.artifacts.map((a) => a.sandboxPath)).toEqual(["out/a.md"]);
563
+ expect(status.workspaceWriteBacks.map((w) => w.workspaceEntryName)).toEqual(["repo"]);
564
+ expect(Object.keys(status.todos)).toEqual(["todo-1"]);
565
+ expect(sb.dirty, "seeded rows are last turn's facts, already persisted").toBe(false);
566
+ expect(sb.awaitingApproval, "a seeded WAITING row is last turn's proposal, not this turn's").toBe(false);
567
+ });
568
+
569
+ it("indexes the seeded rows exactly as a builder born over the seeded status does: a re-issued call reconciles onto the seed, in the root and in a sub-agent", () => {
570
+ const lateStatus = create(AgentExecutionStatusSchema, {});
571
+ const seededLate: Fold = { sb: new TranscriptBuilder("exec-late", lateStatus), status: lateStatus };
572
+ seededLate.sb.seed(persisted());
573
+ const earlyStatus = persisted();
574
+ const seededEarly: Fold = { sb: new TranscriptBuilder("exec-early", earlyStatus), status: earlyStatus };
575
+ const replay: TranscriptEvent[] = [
576
+ { kind: "tool_started", callId: "gated-1", name: "dangerous_tool", input: { target: "prod" }, mcpServerSlug: "" },
577
+ { kind: "tool_finished", callId: "gated-1", result: "ok" },
578
+ { kind: "tool_finished", callId: "sub-read", result: "contents", subAgentId: "sub-1" },
579
+ { kind: "message_start", runId: "after" },
580
+ { kind: "text_delta", runId: "after", text: "All done." },
581
+ ];
582
+ for (const fold of [seededLate, seededEarly]) {
583
+ const { sb, status } = feed(fold, replay);
584
+ const rows = status.messages.flatMap((m) => m.toolCalls).filter((tc) => tc.id === "gated-1");
585
+ expect(rows, `${sb.executionId}: exactly one copy of the re-issued call`).toHaveLength(1);
586
+ expect(rows[0].status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
587
+ expect(rows[0].args, "the resumed start fills the args the seed lacked").toEqual({ target: "prod" });
588
+ expect(status.subAgentExecutions[0].messages[0].toolCalls[0].status, `${sb.executionId}: the sub-agent's seeded row settles`).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
589
+ expect(status.messages.map((m) => m.content), `${sb.executionId}: the new text follows the seed`).toEqual(["I'll call the gated tool.", "All done."]);
590
+ }
591
+ });
592
+
593
+ it("a seeded sub-agent row re-announced by a replayed engine is not re-opened", () => {
594
+ const status = create(AgentExecutionStatusSchema, {});
595
+ const sb = new TranscriptBuilder("exec-seed", status);
596
+ sb.seed(persisted());
597
+ sb.apply({ kind: "sub_agent_started", subAgentId: "sub-1", name: "researcher", subject: "again", input: "x" });
598
+ expect(status.subAgentExecutions, "one row").toHaveLength(1);
599
+ expect(status.subAgentExecutions[0].messages.map((m) => m.content), "its transcript is the seeded one").toEqual(["Reading."]);
600
+ });
601
+ });
602
+
603
+ it("two parallel rows on one message settle independently, out of order", () => {
604
+ const { status } = feed(builder(), [
605
+ { kind: "message_start", runId: "r" },
606
+ { kind: "text_delta", runId: "r", text: "Both files." },
607
+ { kind: "tool_started", callId: "a", name: "read_file", input: { path: "/a" }, mcpServerSlug: "" },
608
+ { kind: "tool_started", callId: "b", name: "read_file", input: { path: "/b" }, mcpServerSlug: "" },
609
+ { kind: "tool_finished", callId: "b", result: "B" },
610
+ { kind: "tool_finished", callId: "a", result: "A" },
611
+ ]);
612
+ const [msg] = status.messages;
613
+ expect(msg.toolCalls.map((tc) => [tc.id, tc.result])).toEqual([["a", "A"], ["b", "B"]]);
614
+ });
615
+
616
+ it("tool_arg_delta accumulates JSON into args once it parses", () => {
617
+ const { status } = feed(builder(), [
618
+ { kind: "tool_started", callId: "t", name: "think", input: {}, mcpServerSlug: "" },
619
+ { kind: "tool_arg_delta", callId: "t", argsChunk: '{"thought":' },
620
+ { kind: "tool_arg_delta", callId: "t", argsChunk: '"test"}' },
621
+ ]);
622
+ expect(rowOf(status, "t").args).toEqual({ thought: "test" });
623
+ });
624
+
625
+ it("classifies the row by name and server: a built-in, an MCP tool, the todo tool, the delegation tool", () => {
626
+ const { status } = feed(builder(), [
627
+ { kind: "tool_started", callId: "a", name: "read_file", input: { path: "/x" }, mcpServerSlug: "" },
628
+ { kind: "tool_started", callId: "b", name: "create_issue", input: { title: "t" }, mcpServerSlug: "github" },
629
+ { kind: "tool_started", callId: "c", name: "write_todos", input: { todos: [] }, mcpServerSlug: "" },
630
+ { kind: "tool_started", callId: "d", name: "task", input: { description: "x" }, mcpServerSlug: "" },
631
+ ]);
632
+ expect(rowOf(status, "a").toolKind).toBe(ToolKind.FILE_READ);
633
+ expect(rowOf(status, "b").toolKind).toBe(ToolKind.MCP);
634
+ expect(rowOf(status, "b").mcpServerSlug).toBe("github");
635
+ expect(rowOf(status, "c").toolKind).toBe(ToolKind.TODO);
636
+ expect(rowOf(status, "d").toolKind).toBe(ToolKind.SUBAGENT);
637
+ });
638
+
639
+ it("stamps provenance and the engine version when the translator attributes the call; leaves them unset when it cannot", () => {
640
+ const { status } = feed(builder(), [
641
+ { kind: "tool_started", callId: "a", name: "execute", input: { command: "ls" }, mcpServerSlug: "", provenance: "approval_lease" },
642
+ { kind: "tool_started", callId: "b", name: "read_file", input: { path: "/x" }, mcpServerSlug: "" },
643
+ ]);
644
+ expect(rowOf(status, "a").approvalPolicySource).toBe(ApprovalPolicySource.APPROVAL_LEASE);
645
+ expect(rowOf(status, "a").policyEngineVersion).toBe("phase-7");
646
+ expect(rowOf(status, "b").approvalPolicySource).toBe(ApprovalPolicySource.UNSPECIFIED);
647
+ expect(rowOf(status, "b").policyEngineVersion).toBe("");
648
+ });
649
+ });
650
+
651
+ describe("TranscriptBuilder — the AI-message boundary: a row joins the text that proposed it", () => {
652
+ it("a tool row attaches to the scope's current AI message; the next run's text is a new message", () => {
653
+ const { status } = feed(builder(), [
654
+ ...proposedCall("c-1"),
655
+ { kind: "tool_finished", callId: "c-1", result: "ok" },
656
+ { kind: "message_start", runId: "run-2" },
657
+ { kind: "text_delta", runId: "run-2", text: "Here is what I found." },
658
+ { kind: "message_finish", runId: "run-2" },
659
+ ]);
660
+ expect(status.messages.map((m) => [m.content, m.toolCalls.map((tc) => tc.id)])).toEqual([
661
+ ["Let me look.", ["c-1"]],
662
+ ["Here is what I found.", []],
663
+ ]);
664
+ });
665
+
666
+ it("a text-less run's tool still attaches to the latest message WITH text — the message boundary is text, not runs", () => {
667
+ const { status } = feed(builder(), [
668
+ { kind: "message_start", runId: "run-1" },
669
+ { kind: "text_delta", runId: "run-1", text: "Reading two files." },
670
+ { kind: "message_finish", runId: "run-1" },
671
+ { kind: "tool_started", callId: "a", name: "read_file", input: { path: "/a" }, mcpServerSlug: "" },
672
+ { kind: "tool_finished", callId: "a", result: "A" },
673
+ { kind: "message_start", runId: "run-2" },
674
+ { kind: "message_finish", runId: "run-2" },
675
+ { kind: "tool_started", callId: "b", name: "read_file", input: { path: "/b" }, mcpServerSlug: "" },
676
+ ]);
677
+ expect(status.messages).toHaveLength(1);
678
+ expect(status.messages[0].toolCalls.map((tc) => tc.id)).toEqual(["a", "b"]);
679
+ });
680
+
681
+ it("a tool start with no AI message in the scope gets an empty one that later rows share", () => {
682
+ const { status } = feed(builder(), [
683
+ { kind: "tool_started", callId: "orphan", name: "read", input: {}, mcpServerSlug: "" },
684
+ { kind: "tool_started", callId: "orphan-2", name: "read", input: {}, mcpServerSlug: "" },
685
+ ]);
686
+ expect(status.messages.map((m) => [m.type, m.content, m.toolCalls.length])).toEqual([[MessageType.MESSAGE_AI, "", 2]]);
687
+ expect(status.messages[0].isStreaming).toBe(false);
688
+ });
689
+
690
+ it("over a seeded transcript the first fresh row joins the seed's last AI message", () => {
691
+ const status = create(AgentExecutionStatusSchema, {
692
+ messages: [
693
+ create(AgentMessageSchema, { type: MessageType.MESSAGE_THINKING, content: "hm" }),
694
+ create(AgentMessageSchema, { type: MessageType.MESSAGE_AI, content: "I will run the two commands." }),
695
+ ],
696
+ });
697
+ const sb = new TranscriptBuilder("exec-resume", status);
698
+ sb.apply({ kind: "tool_started", callId: "b", name: "execute", input: { command: "b" }, mcpServerSlug: "" });
699
+ expect(status.messages).toHaveLength(2);
700
+ expect(status.messages[1].toolCalls.map((tc) => tc.id)).toEqual(["b"]);
701
+ });
702
+
703
+ it("a thinking row never hosts a tool row", () => {
704
+ const { status } = feed(builder(), [
705
+ { kind: "message_start", runId: "run-1" },
706
+ { kind: "reasoning_delta", runId: "run-1", text: "Let me think." },
707
+ { kind: "tool_started", callId: "t", name: "read", input: {}, mcpServerSlug: "" },
708
+ ]);
709
+ expect(status.messages.map((m) => [m.type, m.toolCalls.length])).toEqual([
710
+ [MessageType.MESSAGE_THINKING, 0],
711
+ [MessageType.MESSAGE_AI, 1],
712
+ ]);
713
+ });
714
+ });
715
+
716
+ describe("TranscriptBuilder — a THINKING row and a text message per runId; a new run closes the previous message", () => {
717
+ it("thinking streams into its own row before the run's text, and the run's finish closes both", () => {
718
+ const { sb, status } = feed(builder(), [
719
+ { kind: "message_start", runId: "run-1" },
720
+ { kind: "reasoning_delta", runId: "run-1", text: "Let me analyze " },
721
+ { kind: "reasoning_delta", runId: "run-1", text: "this." },
722
+ ]);
723
+ expect(status.messages[0].isStreaming, "thinking streams while the run is open").toBe(true);
724
+ feed({ sb, status }, [
725
+ { kind: "text_delta", runId: "run-1", text: "Based on " },
726
+ { kind: "text_delta", runId: "run-1", text: "that." },
727
+ { kind: "message_finish", runId: "run-1" },
728
+ ]);
729
+ // A finished run's thinking is finished (before #1097 the THINKING row
730
+ // spun until finalize — a live spinner on a block the model had left).
731
+ expect(status.messages.map((m) => [m.type, m.content, m.isStreaming])).toEqual([
732
+ [MessageType.MESSAGE_THINKING, "Let me analyze this.", false],
733
+ [MessageType.MESSAGE_AI, "Based on that.", false],
734
+ ]);
735
+ });
736
+
737
+ it("a run with thinking and no text: its finish closes the THINKING row alone", () => {
738
+ const { status } = feed(builder(), [
739
+ { kind: "message_start", runId: "run-1" },
740
+ { kind: "reasoning_delta", runId: "run-1", text: "Hmm." },
741
+ { kind: "message_finish", runId: "run-1" },
742
+ ]);
743
+ expect(status.messages.map((m) => [m.type, m.isStreaming])).toEqual([[MessageType.MESSAGE_THINKING, false]]);
744
+ });
745
+
746
+ it("a second run's thinking is a second THINKING row, and its start closes the first run's text", () => {
747
+ const { status } = feed(builder(), [
748
+ { kind: "message_start", runId: "run-1" },
749
+ { kind: "reasoning_delta", runId: "run-1", text: "one" },
750
+ { kind: "text_delta", runId: "run-1", text: "First." },
751
+ { kind: "message_start", runId: "run-2" },
752
+ { kind: "reasoning_delta", runId: "run-2", text: "two" },
753
+ { kind: "text_delta", runId: "run-2", text: "Second." },
754
+ ]);
755
+ const m = status.messages;
756
+ expect(m.map((x) => [x.type, x.content])).toEqual([
757
+ [MessageType.MESSAGE_THINKING, "one"],
758
+ [MessageType.MESSAGE_AI, "First."],
759
+ [MessageType.MESSAGE_THINKING, "two"],
760
+ [MessageType.MESSAGE_AI, "Second."],
761
+ ]);
762
+ expect(m[1].isStreaming, "closed by the next run's start").toBe(false);
763
+ expect(m[3].isStreaming, "still streaming").toBe(true);
764
+ });
765
+
766
+ it("two runs' tokens never interleave: each streams into its own message", () => {
767
+ const { status } = feed(builder(), [
768
+ { kind: "message_start", runId: "a" },
769
+ { kind: "text_delta", runId: "a", text: "A1 " },
770
+ { kind: "message_start", runId: "b" },
771
+ { kind: "text_delta", runId: "b", text: "B1 " },
772
+ { kind: "text_delta", runId: "a", text: "A2" },
773
+ { kind: "text_delta", runId: "b", text: "B2" },
774
+ ]);
775
+ expect(status.messages.map((m) => m.content)).toEqual(["A1 A2", "B1 B2"]);
776
+ });
777
+ });
778
+
779
+ describe("TranscriptBuilder — a sub-agent row per subAgentId with a transcript of its own", () => {
780
+ const open: TranscriptEvent = { kind: "sub_agent_started", subAgentId: "task-1", name: "helper", subject: "Look it up.", input: "Look it up." };
781
+
782
+ it("opens IN_PROGRESS with the translator's name and subject, and folds scoped events into its own messages", () => {
783
+ const { status } = feed(builder(), [
784
+ { kind: "message_start", runId: "root-1" },
785
+ { kind: "text_delta", runId: "root-1", text: "Delegating." },
786
+ open,
787
+ { kind: "tool_started", callId: "task-1", name: "task", input: { subagent_type: "helper" }, mcpServerSlug: "" },
788
+ { kind: "message_start", runId: "sub-1", subAgentId: "task-1" },
789
+ { kind: "text_delta", runId: "sub-1", text: "Working.", subAgentId: "task-1" },
790
+ { kind: "tool_started", callId: "g", name: "grep", input: { pattern: "x" }, mcpServerSlug: "", subAgentId: "task-1" },
791
+ { kind: "tool_finished", callId: "g", result: "found", subAgentId: "task-1" },
792
+ ]);
793
+ const [row] = status.subAgentExecutions;
794
+ expect([row.id, row.name, row.subject, row.input, row.status]).toEqual(["task-1", "helper", "Look it up.", "Look it up.", SubAgentStatus.SUB_AGENT_IN_PROGRESS]);
795
+ expect(row.startedAt).toContain("T");
796
+ expect(row.messages.map((m) => [m.content, m.toolCalls.map((tc) => [tc.id, tc.result])])).toEqual([["Working.", [["g", "found"]]]]);
797
+ expect(status.messages.map((m) => [m.content, m.toolCalls.map((tc) => tc.id)]), "the root keeps only the task row").toEqual([["Delegating.", ["task-1"]]]);
798
+ });
799
+
800
+ it("closes COMPLETED with the output, its streaming flags cleared; FAILED with the error", () => {
801
+ const { status } = feed(builder(), [
802
+ open,
803
+ { kind: "text_delta", runId: "sub-1", text: "still streaming", subAgentId: "task-1" },
804
+ { kind: "sub_agent_finished", subAgentId: "task-1", output: "forty-two" },
805
+ { kind: "sub_agent_started", subAgentId: "task-2", name: "w", subject: "s", input: "s" },
806
+ { kind: "sub_agent_failed", subAgentId: "task-2", error: "boom" },
807
+ ]);
808
+ const [a, b] = status.subAgentExecutions;
809
+ expect([a.status, a.output, a.completedAt !== "", a.messages[0].isStreaming]).toEqual([SubAgentStatus.SUB_AGENT_COMPLETED, "forty-two", true, false]);
810
+ expect([b.status, b.error, b.completedAt !== ""]).toEqual([SubAgentStatus.SUB_AGENT_FAILED, "boom", true]);
811
+ });
812
+
813
+ it("a known id is never re-opened — a seeded row re-announced by a replayed engine is reconciled onto, not duplicated", () => {
814
+ const status = create(AgentExecutionStatusSchema, {});
815
+ status.subAgentExecutions.push(create(SubAgentExecutionSchema, { id: "task-1", name: "helper", status: SubAgentStatus.SUB_AGENT_IN_PROGRESS }));
816
+ const sb = new TranscriptBuilder("exec-resume", status);
817
+ feed({ sb, status }, [open, { kind: "text_delta", runId: "sub-1", text: "resumed", subAgentId: "task-1" }]);
818
+ expect(status.subAgentExecutions).toHaveLength(1);
819
+ expect(status.subAgentExecutions[0].messages.map((m) => m.content)).toEqual(["resumed"]);
820
+ });
821
+
822
+ it("two sub-agents keep separate transcripts; the status's own array is pushed into, never replaced", () => {
823
+ const status = create(AgentExecutionStatusSchema, {});
824
+ const arrayBefore = status.subAgentExecutions;
825
+ const sb = new TranscriptBuilder("exec", status);
826
+ feed({ sb, status }, [
827
+ open,
828
+ { kind: "sub_agent_started", subAgentId: "task-2", name: "other", subject: "s", input: "s" },
829
+ { kind: "text_delta", runId: "s1", text: "one", subAgentId: "task-1" },
830
+ { kind: "text_delta", runId: "s2", text: "two", subAgentId: "task-2" },
831
+ ]);
832
+ expect(status.subAgentExecutions).toBe(arrayBefore);
833
+ expect(status.subAgentExecutions.map((r) => r.messages.map((m) => m.content))).toEqual([["one"], ["two"]]);
834
+ });
835
+
836
+ it("an event for an unknown sub-agent is dropped and logged, never folded into the root", () => {
837
+ const errors: string[] = [];
838
+ const original = console.error;
839
+ console.error = (msg: unknown) => { errors.push(String(msg)); };
840
+ try {
841
+ const { status } = feed(builder(), [{ kind: "text_delta", runId: "r", text: "lost", subAgentId: "nobody" }]);
842
+ expect(status.messages).toHaveLength(0);
843
+ } finally {
844
+ console.error = original;
845
+ }
846
+ expect(errors).toHaveLength(1);
847
+ expect(errors[0]).toContain("unknown sub-agent nobody");
848
+ });
849
+
850
+ it("CANCELLED is not the builder's: a stopped turn's rows are left IN_PROGRESS for the shared cancel act", () => {
851
+ const { sb, status } = feed(builder(), [open, { kind: "text_delta", runId: "s", text: "mid", subAgentId: "task-1" }]);
852
+ sb.finalize();
853
+ expect(status.subAgentExecutions[0].status).toBe(SubAgentStatus.SUB_AGENT_IN_PROGRESS);
854
+ expect(status.subAgentExecutions[0].messages[0].isStreaming, "but its streaming flags are cleared").toBe(false);
855
+ });
856
+ });
857
+
858
+ describe("TranscriptBuilder — finalize() clears every streaming flag in every scope", () => {
859
+ it("promotes nothing: a RUNNING row with streamed output but no completion stays RUNNING for the harness's boundary to settle", () => {
860
+ const { sb, status } = feed(builder(), [...proposedCall("sh-1", "shell"), { kind: "tool_output_delta", callId: "sh-1", delta: "partial output" }]);
861
+ sb.finalize();
862
+ const row = rowOf(status, "sh-1");
863
+ expect(row.status, "output is not completion").toBe(ToolCallStatus.TOOL_CALL_RUNNING);
864
+ expect(row.result).toBe("partial output");
865
+ expect(row.isStreaming).toBe(false);
866
+ expect(row.completedAt).toBe("");
867
+ });
868
+
869
+ it("messages and rows, root and sub-agents", () => {
870
+ const { sb, status } = feed(builder(), [
871
+ { kind: "message_start", runId: "r" },
872
+ { kind: "text_delta", runId: "r", text: "root streaming" },
873
+ { kind: "reasoning_delta", runId: "r2", text: "thinking" },
874
+ { kind: "sub_agent_started", subAgentId: "task-1", name: "h", subject: "s", input: "s" },
875
+ { kind: "text_delta", runId: "s", text: "sub streaming", subAgentId: "task-1" },
876
+ { kind: "tool_started", callId: "o", name: "execute", input: { command: "tail" }, mcpServerSlug: "", subAgentId: "task-1" },
877
+ { kind: "tool_output_delta", callId: "o", delta: "…", subAgentId: "task-1" },
878
+ ]);
879
+ const streaming = () => [
880
+ ...status.messages.map((m) => m.isStreaming),
881
+ ...status.subAgentExecutions[0].messages.flatMap((m) => [m.isStreaming, ...m.toolCalls.map((tc) => tc.isStreaming)]),
882
+ ];
883
+ expect(streaming().some(Boolean)).toBe(true);
884
+ sb.finalize();
885
+ expect(streaming().some(Boolean)).toBe(false);
886
+ });
887
+ });
888
+
889
+ describe("TranscriptBuilder — artifacts and write-backs", () => {
890
+ const artifact = (sandboxPath: string, contentHash: string) =>
891
+ create(ExecutionArtifactSchema, { sandboxPath, contentHash, storageKey: `k/${contentHash}` });
892
+
893
+ it("appends a new artifact and forces the next persist; the same path with the same hash is a no-op", () => {
894
+ const { sb, status } = builder();
895
+ sb.addArtifact(artifact("out/a.txt", "h1"));
896
+ expect(status.artifacts.map((a) => a.contentHash)).toEqual(["h1"]);
897
+ expect(sb.dirty).toBe(true);
898
+ sb.markPersisted();
899
+ sb.addArtifact(artifact("out/a.txt", "h1"));
900
+ expect(status.artifacts).toHaveLength(1);
901
+ expect(sb.dirty, "nothing changed, nothing to flush").toBe(false);
902
+ });
903
+
904
+ it("the same path with a new hash replaces in place", () => {
905
+ const { sb, status } = builder();
906
+ sb.addArtifact(artifact("out/a.txt", "h1"));
907
+ sb.addArtifact(artifact("out/a.txt", "h2"));
908
+ expect(status.artifacts.map((a) => [a.sandboxPath, a.contentHash])).toEqual([["out/a.txt", "h2"]]);
909
+ });
910
+
911
+ it("write-backs upsert by workspace entry name", () => {
912
+ const { sb, status } = builder();
913
+ sb.addWriteBack(create(WorkspaceWriteBackSchema, { workspaceEntryName: "repo", branchName: "b1" }));
914
+ sb.addWriteBack(create(WorkspaceWriteBackSchema, { workspaceEntryName: "repo", branchName: "b2" }));
915
+ sb.addWriteBack(create(WorkspaceWriteBackSchema, { workspaceEntryName: "docs", branchName: "d" }));
916
+ expect(status.workspaceWriteBacks.map((w) => [w.workspaceEntryName, w.branchName])).toEqual([["repo", "b2"], ["docs", "d"]]);
917
+ expect(sb.dirty).toBe(true);
918
+ });
919
+ });
920
+
921
+ describe("TranscriptBuilder — the dirty flag and the error guard", () => {
922
+ it("a discrete change (a row's start or finish, a sub-agent opening) forces the next persist; tokens do not", () => {
923
+ const { sb } = feed(builder(), [{ kind: "message_start", runId: "r" }, { kind: "text_delta", runId: "r", text: "tok" }]);
924
+ expect(sb.dirty, "text rides the cadence").toBe(false);
925
+ sb.apply({ kind: "tool_started", callId: "t", name: "read", input: {}, mcpServerSlug: "" });
926
+ expect(sb.dirty).toBe(true);
927
+ sb.markPersisted();
928
+ sb.apply({ kind: "tool_finished", callId: "t", result: "ok" });
929
+ expect(sb.dirty).toBe(true);
930
+ sb.markPersisted();
931
+ sb.apply({ kind: "sub_agent_started", subAgentId: "s", name: "n", subject: "s", input: "s" });
932
+ expect(sb.dirty).toBe(true);
933
+ });
934
+
935
+ it("a settled re-emit that changes nothing forces no persist; one that carries a new result or message does", () => {
936
+ const { sb, status } = feed(builder(), [...proposedCall(), { kind: "tool_finished", callId: "call-1", result: "" }]);
937
+ sb.markPersisted();
938
+ // Cursor's shape: the timing delta completed the row; the stream's own
939
+ // completion, carrying the result, follows and must flush.
940
+ sb.apply({ kind: "tool_finished", callId: "call-1", result: "contents" });
941
+ expect(sb.dirty, "the result is new").toBe(true);
942
+ sb.markPersisted();
943
+ sb.apply({ kind: "tool_finished", callId: "call-1", result: "contents" });
944
+ expect(sb.dirty, "a redundant terminal re-emit is noise").toBe(false);
945
+ sb.apply({ kind: "tool_finished", callId: "call-1", result: "" });
946
+ expect(sb.dirty).toBe(false);
947
+ sb.apply({ kind: "tool_error", callId: "call-1", message: "" });
948
+ expect(sb.dirty, "an empty error on a settled row changes nothing").toBe(false);
949
+ expect(rowOf(status, "call-1").status).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
950
+ });
951
+
952
+ it("writes neither the phase nor startedAt nor streamingUsage — the turn runtime owns them", () => {
953
+ const { status } = feed(builder(), proposedCall());
954
+ expect(status.phase).toBe(0);
955
+ expect(status.startedAt).toBe("");
956
+ expect(status.streamingUsage).toBeUndefined();
957
+ });
958
+
959
+ it("a handler that throws is logged with the execution and the event kind, and never escapes apply", () => {
960
+ const errors: string[] = [];
961
+ const original = console.error;
962
+ console.error = (msg: unknown) => { errors.push(String(msg)); };
963
+ try {
964
+ const { sb } = feed(builder(), proposedCall());
965
+ const poisoned = { kind: "tool_finished", callId: "call-1", get result(): string { throw new Error("property access failed"); } } as TranscriptEvent;
966
+ expect(() => sb.apply(poisoned)).not.toThrow();
967
+ } finally {
968
+ console.error = original;
969
+ }
970
+ expect(errors).toHaveLength(1);
971
+ expect(errors[0]).toContain("exec-test");
972
+ expect(errors[0]).toContain("kind=tool_finished");
973
+ expect(errors[0]).toContain("property access failed");
974
+ });
975
+ });