@stigmer/runner 3.40.0 → 3.41.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 (278) hide show
  1. package/README.md +3 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
  4. package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
  5. package/dist/activities/call-agent.js +1 -1
  6. package/dist/activities/discover-mcp-server.d.ts +7 -1
  7. package/dist/activities/discover-mcp-server.js +13 -29
  8. package/dist/activities/discover-mcp-server.js.map +1 -1
  9. package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
  10. package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
  11. package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
  12. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
  13. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
  14. package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
  15. package/dist/activities/execute-cursor/approval-policy.js +11 -35
  16. package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
  17. package/dist/activities/execute-cursor/boundary-rows.js +4 -4
  18. package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
  19. package/dist/activities/execute-cursor/translator.d.ts +12 -17
  20. package/dist/activities/execute-cursor/translator.js +13 -30
  21. package/dist/activities/execute-cursor/translator.js.map +1 -1
  22. package/dist/activities/execute-cursor/turn-stream.js +4 -2
  23. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  24. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
  25. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
  26. package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
  27. package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
  28. package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
  29. package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
  30. package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
  31. package/dist/activities/execute-deep-agent/platform-route.js +87 -19
  32. package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
  33. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
  34. package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
  35. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  36. package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
  37. package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
  38. package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
  39. package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
  40. package/dist/activities/execute-deep-agent/translator.js +5 -4
  41. package/dist/activities/execute-deep-agent/translator.js.map +1 -1
  42. package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
  43. package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
  44. package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
  45. package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
  46. package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
  47. package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
  48. package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
  49. package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
  50. package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
  51. package/dist/activities/execute-deep-agent/turn.js +15 -6
  52. package/dist/activities/execute-deep-agent/turn.js.map +1 -1
  53. package/dist/activities/workflow-event-activities.js +2 -0
  54. package/dist/activities/workflow-event-activities.js.map +1 -1
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +2 -0
  57. package/dist/config.js.map +1 -1
  58. package/dist/exit-backstop.d.ts +64 -0
  59. package/dist/exit-backstop.js +79 -0
  60. package/dist/exit-backstop.js.map +1 -0
  61. package/dist/harness/terminal-table.d.ts +12 -5
  62. package/dist/harness/terminal-table.js +13 -13
  63. package/dist/harness/terminal-table.js.map +1 -1
  64. package/dist/harness/transcript/builder.d.ts +21 -0
  65. package/dist/harness/transcript/builder.js +53 -31
  66. package/dist/harness/transcript/builder.js.map +1 -1
  67. package/dist/harness/transcript/events.d.ts +22 -26
  68. package/dist/harness/transcript/events.js +6 -5
  69. package/dist/harness/transcript/events.js.map +1 -1
  70. package/dist/harness/turn-context.d.ts +3 -1
  71. package/dist/harness/turn-context.js +5 -3
  72. package/dist/harness/turn-context.js.map +1 -1
  73. package/dist/main.js +10 -4
  74. package/dist/main.js.map +1 -1
  75. package/dist/middleware/cost-advisory.d.ts +6 -3
  76. package/dist/middleware/cost-advisory.js +6 -3
  77. package/dist/middleware/cost-advisory.js.map +1 -1
  78. package/dist/middleware/execution-budget.d.ts +22 -7
  79. package/dist/middleware/execution-budget.js +57 -35
  80. package/dist/middleware/execution-budget.js.map +1 -1
  81. package/dist/middleware/index.d.ts +10 -7
  82. package/dist/middleware/index.js +10 -7
  83. package/dist/middleware/index.js.map +1 -1
  84. package/dist/middleware/types.d.ts +6 -1
  85. package/dist/runner-manager.js +3 -2
  86. package/dist/runner-manager.js.map +1 -1
  87. package/dist/runner.d.ts +25 -0
  88. package/dist/runner.js +41 -14
  89. package/dist/runner.js.map +1 -1
  90. package/dist/shared/approval-policy.d.ts +23 -13
  91. package/dist/shared/approval-policy.js +47 -14
  92. package/dist/shared/approval-policy.js.map +1 -1
  93. package/dist/shared/caller-identity.d.ts +4 -5
  94. package/dist/shared/caller-identity.js +4 -5
  95. package/dist/shared/caller-identity.js.map +1 -1
  96. package/dist/shared/channel-attachment.js +3 -2
  97. package/dist/shared/channel-attachment.js.map +1 -1
  98. package/dist/shared/connect-backfill.d.ts +2 -1
  99. package/dist/shared/connect-backfill.js +2 -2
  100. package/dist/shared/connect-backfill.js.map +1 -1
  101. package/dist/shared/mcp-resolver.d.ts +10 -1
  102. package/dist/shared/mcp-resolver.js +11 -2
  103. package/dist/shared/mcp-resolver.js.map +1 -1
  104. package/dist/shared/memory-attachment.js +3 -2
  105. package/dist/shared/memory-attachment.js.map +1 -1
  106. package/dist/shared/model-error.d.ts +25 -3
  107. package/dist/shared/model-error.js +78 -10
  108. package/dist/shared/model-error.js.map +1 -1
  109. package/dist/shared/platform-server-address.d.ts +71 -0
  110. package/dist/shared/platform-server-address.js +103 -0
  111. package/dist/shared/platform-server-address.js.map +1 -0
  112. package/dist/shared/prompt-sections.d.ts +6 -28
  113. package/dist/shared/prompt-sections.js +6 -35
  114. package/dist/shared/prompt-sections.js.map +1 -1
  115. package/dist/shared/skill-resolver.d.ts +2 -3
  116. package/dist/shared/skill-resolver.js +2 -3
  117. package/dist/shared/skill-resolver.js.map +1 -1
  118. package/dist/shared/synthesized-attachment.d.ts +0 -5
  119. package/dist/shared/synthesized-attachment.js +0 -12
  120. package/dist/shared/synthesized-attachment.js.map +1 -1
  121. package/dist/shared/tool-rounds.d.ts +51 -23
  122. package/dist/shared/tool-rounds.js +67 -24
  123. package/dist/shared/tool-rounds.js.map +1 -1
  124. package/dist/shared/workspace/platform-dir.d.ts +15 -0
  125. package/dist/shared/workspace/platform-dir.js +18 -1
  126. package/dist/shared/workspace/platform-dir.js.map +1 -1
  127. package/dist/shared/workspace/sources/git.js +13 -6
  128. package/dist/shared/workspace/sources/git.js.map +1 -1
  129. package/dist/shared/workspace/stigmer-link.d.ts +21 -14
  130. package/dist/shared/workspace/stigmer-link.js +91 -29
  131. package/dist/shared/workspace/stigmer-link.js.map +1 -1
  132. package/dist/tools/url-guard.d.ts +5 -1
  133. package/dist/tools/url-guard.js +9 -2
  134. package/dist/tools/url-guard.js.map +1 -1
  135. package/dist/worker.d.ts +11 -2
  136. package/dist/worker.js +1 -1
  137. package/dist/worker.js.map +1 -1
  138. package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
  139. package/dist/workflow-engine/tasks/human-input.js +79 -37
  140. package/dist/workflow-engine/tasks/human-input.js.map +1 -1
  141. package/dist/workflow-engine/types.d.ts +14 -4
  142. package/dist/workflow-engine/types.js.map +1 -1
  143. package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
  144. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  145. package/dist/workflows/engine-core.d.ts +25 -0
  146. package/dist/workflows/engine-core.js.map +1 -1
  147. package/dist/workflows/execute-from-execution.d.ts +3 -0
  148. package/dist/workflows/execute-from-execution.js.map +1 -1
  149. package/dist/workflows/human-input-orchestrator.d.ts +9 -12
  150. package/dist/workflows/human-input-orchestrator.js +12 -44
  151. package/dist/workflows/human-input-orchestrator.js.map +1 -1
  152. package/package.json +5 -5
  153. package/src/__test-utils__/config-fixture.ts +1 -0
  154. package/src/__test-utils__/execution-record-fixture.ts +2 -1
  155. package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
  156. package/src/__test-utils__/test-gate.ts +8 -0
  157. package/src/__tests__/exit-backstop.test.ts +91 -0
  158. package/src/__tests__/golden-e2e.test.ts +80 -62
  159. package/src/__tests__/history-encryption-e2e.test.ts +51 -27
  160. package/src/__tests__/runner.test.ts +60 -2
  161. package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
  162. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
  163. package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
  164. package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
  165. package/src/activities/call-agent.ts +1 -1
  166. package/src/activities/discover-mcp-server.ts +27 -37
  167. package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
  168. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
  169. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
  170. package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
  171. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
  172. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
  173. package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  174. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
  175. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
  176. package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
  177. package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
  178. package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
  179. package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
  180. package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
  181. package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
  182. package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
  183. package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
  184. package/src/activities/execute-cursor/approval-policy.ts +11 -36
  185. package/src/activities/execute-cursor/boundary-rows.ts +3 -3
  186. package/src/activities/execute-cursor/translator.ts +15 -32
  187. package/src/activities/execute-cursor/turn-stream.ts +4 -1
  188. package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
  189. package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
  190. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
  191. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
  192. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
  193. package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
  194. package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
  195. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
  196. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
  197. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
  198. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
  199. package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
  200. package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
  201. package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
  202. package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
  203. package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
  204. package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
  205. package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
  206. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
  207. package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
  208. package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
  209. package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
  210. package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
  211. package/src/activities/execute-deep-agent/platform-route.ts +109 -20
  212. package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
  213. package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
  214. package/src/activities/execute-deep-agent/translator.ts +5 -4
  215. package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
  216. package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
  217. package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
  218. package/src/activities/execute-deep-agent/turn.ts +16 -7
  219. package/src/activities/workflow-event-activities.ts +2 -0
  220. package/src/config.ts +11 -0
  221. package/src/exit-backstop.ts +97 -0
  222. package/src/harness/README.md +5 -5
  223. package/src/harness/terminal-table.ts +13 -13
  224. package/src/harness/transcript/__tests__/builder.test.ts +93 -21
  225. package/src/harness/transcript/builder.ts +53 -30
  226. package/src/harness/transcript/events.ts +22 -27
  227. package/src/harness/turn-context.ts +5 -3
  228. package/src/main.ts +9 -5
  229. package/src/middleware/__tests__/execution-budget.test.ts +181 -103
  230. package/src/middleware/cost-advisory.ts +6 -3
  231. package/src/middleware/execution-budget.ts +60 -34
  232. package/src/middleware/index.ts +10 -7
  233. package/src/middleware/types.ts +6 -1
  234. package/src/runner-manager.ts +3 -2
  235. package/src/runner.ts +57 -15
  236. package/src/shared/__tests__/approval-policy.test.ts +41 -0
  237. package/src/shared/__tests__/artifact-storage.test.ts +1 -0
  238. package/src/shared/__tests__/connect-backfill.test.ts +15 -11
  239. package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
  240. package/src/shared/__tests__/model-error.test.ts +71 -3
  241. package/src/shared/__tests__/platform-server-address.test.ts +122 -0
  242. package/src/shared/__tests__/prompt-sections.test.ts +2 -54
  243. package/src/shared/__tests__/tool-rounds.test.ts +64 -14
  244. package/src/shared/approval-policy.ts +57 -14
  245. package/src/shared/caller-identity.ts +4 -5
  246. package/src/shared/channel-attachment.ts +2 -2
  247. package/src/shared/connect-backfill.ts +5 -1
  248. package/src/shared/mcp-resolver.ts +11 -1
  249. package/src/shared/memory-attachment.ts +2 -2
  250. package/src/shared/model-error.ts +87 -9
  251. package/src/shared/platform-server-address.ts +123 -0
  252. package/src/shared/prompt-sections.ts +6 -55
  253. package/src/shared/skill-resolver.ts +2 -3
  254. package/src/shared/synthesized-attachment.ts +0 -12
  255. package/src/shared/tool-rounds.ts +70 -24
  256. package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
  257. package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
  258. package/src/shared/workspace/platform-dir.ts +19 -1
  259. package/src/shared/workspace/sources/git.ts +14 -8
  260. package/src/shared/workspace/stigmer-link.ts +99 -29
  261. package/src/tools/__tests__/url-guard.test.ts +19 -1
  262. package/src/tools/url-guard.ts +9 -1
  263. package/src/worker.ts +12 -2
  264. package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
  265. package/src/workflow-engine/__tests__/loader.test.ts +1 -1
  266. package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
  267. package/src/workflow-engine/tasks/human-input.ts +92 -42
  268. package/src/workflow-engine/types.ts +16 -4
  269. package/src/workflows/call-agent-orchestrator.ts +7 -0
  270. package/src/workflows/engine-core.ts +18 -0
  271. package/src/workflows/execute-from-execution.ts +2 -1
  272. package/src/workflows/human-input-orchestrator.ts +15 -56
  273. package/dist/shared/skill-relevance.d.ts +0 -65
  274. package/dist/shared/skill-relevance.js +0 -175
  275. package/dist/shared/skill-relevance.js.map +0 -1
  276. package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
  277. package/src/shared/__tests__/skill-relevance.test.ts +0 -292
  278. package/src/shared/skill-relevance.ts +0 -222
@@ -27,6 +27,10 @@
27
27
  * a status the host could reach), and `goldens/file-review-capture.status.json`
28
28
  * pins the whole ledger beside Cursor's.
29
29
  *
30
+ * Golden hunk since #1443: the inline artifact's `createdAt` is t=1, the
31
+ * edit's finish, where it was t=2, the moment the publisher's upload
32
+ * returned (it reads the clock before its first await now).
33
+ *
30
34
  * Parent phase rows exercised beyond the plain arms: a `local_path` workspace
31
35
  * entry (mounted as-is in local mode, the agent rooted at the entry per
32
36
  * `provisionWorkspace`); capture-mode derivation on a git tree; the baseline
@@ -40,7 +40,7 @@
40
40
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
41
41
  "sizeBytes": "22",
42
42
  "storageKey": "artifacts/aex_hermetic_0001/notes.md",
43
- "createdAt": "2026-01-01T00:00:02.000Z",
43
+ "createdAt": "2026-01-01T00:00:01.000Z",
44
44
  "contentHash": "e4cdd09e8ca79e376a6ec5766d190754bc8ee4a2d6c5c7f4d86a9dc7fdf25a0f"
45
45
  }
46
46
  ],
@@ -57,7 +57,7 @@
57
57
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
58
58
  "sizeBytes": "32",
59
59
  "storageKey": "artifacts/aex_hermetic_0001/report.md",
60
- "createdAt": "2026-01-01T00:00:02.000Z",
60
+ "createdAt": "2026-01-01T00:00:01.000Z",
61
61
  "contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
62
62
  }
63
63
  ],
@@ -67,7 +67,7 @@
67
67
  "kind": "EXECUTION_ARTIFACT_KIND_FILE",
68
68
  "sizeBytes": "32",
69
69
  "storageKey": "artifacts/aex_hermetic_0001/report.md",
70
- "createdAt": "2026-01-01T00:00:02.000Z",
70
+ "createdAt": "2026-01-01T00:00:01.000Z",
71
71
  "contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
72
72
  }
73
73
  ],
@@ -200,89 +200,24 @@
200
200
  }
201
201
  ]
202
202
  },
203
- {
204
- "type": "MESSAGE_AI",
205
- "content": "Reading file 10.",
206
- "timestamp": "2026-01-01T00:00:11.000Z",
207
- "toolCalls": [
208
- {
209
- "id": "call-hermetic-loop-0010",
210
- "name": "read_file",
211
- "args": {
212
- "file_path": "/f10.md"
213
- },
214
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f10.md' not found\"}]",
215
- "status": "TOOL_CALL_COMPLETED",
216
- "startedAt": "2026-01-01T00:00:11.000Z",
217
- "completedAt": "2026-01-01T00:00:11.000Z",
218
- "argsPreview": "{\"file_path\":\"/f10.md\"}",
219
- "toolKind": "TOOL_KIND_FILE_READ"
220
- }
221
- ]
222
- },
223
- {
224
- "type": "MESSAGE_AI",
225
- "content": "Reading file 11.",
226
- "timestamp": "2026-01-01T00:00:12.000Z",
227
- "toolCalls": [
228
- {
229
- "id": "call-hermetic-loop-0011",
230
- "name": "read_file",
231
- "args": {
232
- "file_path": "/f11.md"
233
- },
234
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f11.md' not found\"}]",
235
- "status": "TOOL_CALL_COMPLETED",
236
- "startedAt": "2026-01-01T00:00:12.000Z",
237
- "completedAt": "2026-01-01T00:00:12.000Z",
238
- "argsPreview": "{\"file_path\":\"/f11.md\"}",
239
- "toolKind": "TOOL_KIND_FILE_READ"
240
- }
241
- ]
242
- },
243
- {
244
- "type": "MESSAGE_AI",
245
- "content": "Reading file 12.",
246
- "timestamp": "2026-01-01T00:00:13.000Z",
247
- "toolCalls": [
248
- {
249
- "id": "call-hermetic-loop-0012",
250
- "name": "read_file",
251
- "args": {
252
- "file_path": "/f12.md"
253
- },
254
- "result": "[{\"type\":\"text\",\"text\":\"Error: File '/f12.md' not found\"}]",
255
- "status": "TOOL_CALL_COMPLETED",
256
- "startedAt": "2026-01-01T00:00:13.000Z",
257
- "completedAt": "2026-01-01T00:00:13.000Z",
258
- "argsPreview": "{\"file_path\":\"/f12.md\"}",
259
- "toolKind": "TOOL_KIND_FILE_READ"
260
- }
261
- ]
262
- },
263
- {
264
- "type": "MESSAGE_AI",
265
- "content": "Reading file 13.",
266
- "timestamp": "2026-01-01T00:00:14.000Z"
267
- },
268
203
  {
269
204
  "type": "MESSAGE_SYSTEM",
270
205
  "content": "The agent reached the tool-call limit for this message. Work completed so far has been saved. Send another message to continue where the agent left off.",
271
- "timestamp": "2026-01-01T00:00:14.000Z"
206
+ "timestamp": "2026-01-01T00:00:10.000Z"
272
207
  }
273
208
  ],
274
209
  "phase": "EXECUTION_TERMINATED",
275
210
  "error": "Agent reached the tool-call limit for this message. Send another message to continue.",
276
211
  "startedAt": "2026-01-01T00:00:00.000Z",
277
- "completedAt": "2026-01-01T00:00:14.000Z",
212
+ "completedAt": "2026-01-01T00:00:10.000Z",
278
213
  "streamingUsage": {
279
- "inputTokens": "14000",
280
- "outputTokens": "280",
281
- "totalTokens": "14280",
282
- "turnCount": 14,
283
- "estimatedCostUsd": 0.015400000000000002,
214
+ "inputTokens": "10000",
215
+ "outputTokens": "200",
216
+ "totalTokens": "10200",
217
+ "turnCount": 10,
218
+ "estimatedCostUsd": 0.011000000000000001,
284
219
  "model": "claude-haiku-4.5",
285
- "observedAt": "2026-01-01T00:00:14.000Z",
220
+ "observedAt": "2026-01-01T00:00:10.000Z",
286
221
  "requestedServiceTier": "SERVICE_TIER_STANDARD",
287
222
  "requestedThinkingMode": "THINKING_MODE_DISABLED"
288
223
  },
@@ -12,7 +12,7 @@
12
12
  }
13
13
  ],
14
14
  "phase": "EXECUTION_FAILED",
15
- "error": "Execution failed: [Error] hermetic fault injected at getAgent",
15
+ "error": "[Error] hermetic fault injected at getAgent",
16
16
  "startedAt": "2026-01-01T00:00:00.000Z",
17
17
  "completedAt": "2026-01-01T00:00:00.000Z"
18
18
  }
@@ -24,6 +24,11 @@
24
24
  * this pair of goldens (`inline-artifact.turn1`, `inline-artifact.turn2`) is
25
25
  * what proves it did.
26
26
  *
27
+ * Turn 2 completes, so its post-stream safety net walks the seeded history
28
+ * and asks for `report.md` again. The publisher re-reads it, finds the bytes
29
+ * the seeded row lists, and uploads nothing: the store sees turn 1's write
30
+ * only (stigmer/stigmer#1448).
31
+ *
27
32
  * Also recorded (a shape fact, as found): the write's tool row
28
33
  * shows `requiresApproval: true` with `approvalPolicySource` BUILTIN_CATEGORY
29
34
  * — the category policy's verdict — while the row is COMPLETED because
@@ -50,6 +55,12 @@
50
55
  * to persist "0 files" beside a one-file change set. The runtime now
51
56
  * refreshes the strip on every write, the terminal one included.
52
57
  *
58
+ * Golden hunk since #1443, in both goldens: the artifact's `createdAt` is
59
+ * t=1, the write's finish, where it was t=2. The publisher read the clock
60
+ * after its upload, which runs beside the stream, so the stamp landed on
61
+ * either side of the next model turn's tick and flaked under the full
62
+ * suite; it reads the clock before its first await now.
63
+ *
53
64
  * Regenerate ONLY after a deliberate behavior change:
54
65
  * npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
55
66
  */
@@ -88,6 +99,7 @@ import {
88
99
  runDeepAgentTurn,
89
100
  } from "../../__test-utils__/hermetic-deep-agent.js";
90
101
  import { CLOSING_TURN, EXECUTE_CALL_A } from "../../__test-utils__/hitl-script.js";
102
+ import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
91
103
 
92
104
  const REPORT = "report.md";
93
105
  const REPORT_BODY = "# Report\n\nAll fixtures nominal.\n";
@@ -148,6 +160,7 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
148
160
  expect(existsSync(join(env.artifactPath, run1.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
149
161
  const writeRow = record.toolCalls().find((tc) => tc.id === WRITE_CALL_ID)!;
150
162
  expect(writeRow.status, "capture mode let the write flow").toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
163
+ expect(run1.artifacts[0].createdAt, "the artifact is stamped at its write's finish").toBe(writeRow.completedAt);
151
164
  expect(record.waitingToolCalls().map((tc) => tc.id)).toEqual([EXECUTE_CALL_A.id]);
152
165
  await expect(statusJson(run1)).toMatchFileSnapshot("./goldens/inline-artifact.turn1.status.json");
153
166
 
@@ -155,13 +168,17 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
155
168
  expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, DECIDED_AT)).toBe(1);
156
169
 
157
170
  // ── Turn 2 ───────────────────────────────────────────────────────────────
171
+ const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload");
158
172
  const turn2 = await runDeepAgentTurn(scenario, { turnSeq: 1 });
173
+ const turn2Keys = uploads.mock.calls.map(([key]) => key);
174
+ uploads.mockRestore();
159
175
  expect(turn2.outcome.kind).toBe("returned");
160
176
  expect((turn2.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
161
177
  const final = record.lastFullStatus!;
162
178
  expect(final.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]), "the seed carried the artifact").toEqual(
163
179
  run1.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]),
164
180
  );
181
+ expect(turn2Keys, "turn 2 does not upload the unchanged report again").not.toContain(run1.artifacts[0].storageKey);
165
182
  expect(final.messages.flatMap((m) => m.toolCalls).map((tc) => [tc.id, tc.status])).toEqual([
166
183
  [WRITE_CALL_ID, ToolCallStatus.TOOL_CALL_COMPLETED],
167
184
  [EXECUTE_CALL_A.id, ToolCallStatus.TOOL_CALL_COMPLETED],
@@ -3,13 +3,15 @@
3
3
  * model that keeps calling tools.
4
4
  *
5
5
  * What the arm does: `max_tool_rounds = 10` (the clamp floor,
6
- * `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) becomes LangGraph's
7
- * `recursionLimit = 60` (SUPER_STEPS_PER_ROUND); the script proposes a
8
- * DISTINCT `read_file` every round (distinct ids and paths, so the
9
- * loop-detection middleware's "repetitive pattern" warning does not end the
10
- * run first); the graph throws `GraphRecursionError`; the adapter's turn
11
- * classifies it as the `tool_call_limit` outcome (`shared/tool-rounds.ts`
12
- * `isGraphRecursionError`), and the runtime's `toolCallLimitArm` writes
6
+ * `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) is the execution budget
7
+ * middleware's round limit; the script proposes a DISTINCT `read_file` every
8
+ * round (distinct ids and paths, so the loop-detection middleware's
9
+ * "repetitive pattern" warning does not end the run first); after round 10's
10
+ * results the middleware throws `ToolRoundLimitError` in place of round 11's
11
+ * model call; the adapter's turn classifies it, through LangChain's
12
+ * `MiddlewareError` wrapping, as the `tool_call_limit` outcome
13
+ * (`shared/tool-rounds.ts` `isToolCallBudgetStop`), and the runtime's
14
+ * `toolCallLimitArm` writes
13
15
  * TERMINATED with the cross-repo prefix `TOOL_CALL_LIMIT_ERROR_PREFIX`
14
16
  * (matched with `startsWith` by stigmer-cloud's `reply-extractor.ts`) and
15
17
  * its row, PERSISTS it, and returns the slim.
@@ -29,16 +31,26 @@
29
31
  * empty message once and its `currentAiMessage[""]` then caught every later
30
32
  * row.
31
33
  *
32
- * How many rounds fit is the middleware stack's shape, not the knob's
33
- * (ruled 2026-09-13, #1096): LangChain makes every
34
- * middleware hook its own graph node, and `max_tool_rounds` becomes a
35
- * super-step budget at the ×6 floor estimate, so a node fewer per round
36
- * means more rounds within the same 60 steps. When `graceful-stop.ts` (an
37
- * `afterModel` node) was deleted in #1096, this golden went from 11 tool
38
- * calls to 13 before TERMINATED — the only hunk, the copy unchanged. That the
39
- * knob counts super-steps and not rounds is recorded as #1113
40
- * (a round counter in the budget middleware would make it mean what it
41
- * says); this file pins what the stack delivers today.
34
+ * How many rounds run is the knob's, exactly (#1113): TEN tool rows, on any
35
+ * middleware stack. Until #1113 the knob was LangGraph's recursion limit at
36
+ * 10 × 6 super-steps, and since LangChain makes every middleware hook its own
37
+ * graph node, the rows moved with the stack: 11 until #1096 deleted
38
+ * `graceful-stop.ts` (an `afterModel` node), 13 after it. #1113 moved this
39
+ * golden from 13 rows to 10, the copy unchanged. It also dropped the golden's
40
+ * last AI message, "Reading file 13.": the step limit had cut that fourteenth
41
+ * model call's proposal before its tool ran, so the transcript ended on a
42
+ * call that never executed. The round stop replaces the model call instead,
43
+ * so the transcript ends on round 10's results, and usage counts ten model
44
+ * calls, not fourteen.
45
+ *
46
+ * The second arm is the copy's promise, "send another message to continue":
47
+ * on the durable (`sqlite`) checkpointer, a follow-up message on the same
48
+ * thread starts from the checkpoint the stop left. The stop is a throw inside
49
+ * the model node rather than a limit between super-steps, so this is proven,
50
+ * not assumed: the follow-up's first model turn sees all ten rounds in its
51
+ * transcript (the scripted model indexes its script by the rounds the
52
+ * transcript holds), the run completes, and the budget is the new message's
53
+ * own.
42
54
  *
43
55
  * Regenerate ONLY after a deliberate behavior change:
44
56
  * npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
@@ -65,14 +77,25 @@ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fix
65
77
  import { MIN_TOOL_ROUNDS } from "../../../../shared/tool-rounds.js";
66
78
  import {
67
79
  beginDeepAgentScenario,
80
+ continueDeepAgentScenario,
68
81
  deepAgentExecutionRecord,
69
82
  runDeepAgentTurn,
70
83
  } from "../../__test-utils__/hermetic-deep-agent.js";
84
+ import type { ScriptedTurn } from "../../__test-utils__/scripted-model.js";
71
85
  import { TOOL_CALL_LIMIT_ERROR_PREFIX } from "../../../../shared/tool-rounds.js";
72
86
 
73
87
  /** More rounds than the budget allows; the graph stops the script, not the other way round. */
74
88
  const SCRIPTED_ROUNDS = 40;
75
89
 
90
+ /** The model that keeps calling tools: a distinct `read_file` per round. */
91
+ function readingRounds(): ScriptedTurn[] {
92
+ return Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
93
+ text: `Reading file ${i}.`,
94
+ toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
95
+ usage: { inputTokens: 1_000, outputTokens: 20 },
96
+ }));
97
+ }
98
+
76
99
  describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
77
100
  let env: HermeticEnvironment;
78
101
  let registry: ReturnType<typeof stubRegistryFetch>;
@@ -97,13 +120,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
97
120
  env,
98
121
  clock,
99
122
  record,
100
- script: () => ({
101
- turns: Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
102
- text: `Reading file ${i}.`,
103
- toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
104
- usage: { inputTokens: 1_000, outputTokens: 20 },
105
- })),
106
- }),
123
+ script: () => ({ turns: readingRounds() }),
107
124
  });
108
125
 
109
126
  // ── Act ──────────────────────────────────────────────────────────────────
@@ -125,8 +142,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
125
142
  expect(persisted.error, "the event count left the copy").not.toMatch(/\d+ events?/);
126
143
  expect(persisted.completedAt).not.toBe("");
127
144
  const rows = record.toolCalls();
128
- expect(rows.length, "fewer rounds ran than were scripted: the budget stopped the graph").toBeLessThan(SCRIPTED_ROUNDS);
129
- expect(rows.length).toBeGreaterThanOrEqual(MIN_TOOL_ROUNDS);
145
+ expect(rows.length, "exactly max_tool_rounds rounds ran: the budget counts rounds, not graph steps").toBe(MIN_TOOL_ROUNDS);
130
146
  // The message boundary: every row sits on the message whose text proposed it
131
147
  // — one "Reading file N." message per row, no empty host (before #1097 all
132
148
  // thirteen rows sat on ONE empty message).
@@ -144,4 +160,48 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
144
160
  const json = JSON.stringify(toJson(AgentExecutionStatusSchema, persisted), null, 2) + "\n";
145
161
  await expect(json).toMatchFileSnapshot("./goldens/recursion-limit.persisted.status.json");
146
162
  });
163
+
164
+ it("continues on the next message from the checkpoint the stop left (sqlite)", async () => {
165
+ // ── Arrange: the first message exhausts its budget ───────────────────────
166
+ const first = beginDeepAgentScenario({
167
+ env,
168
+ clock,
169
+ record: deepAgentExecutionRecord({ message: "Keep reading files.", maxToolRounds: MIN_TOOL_ROUNDS }),
170
+ checkpointer: "sqlite",
171
+ script: () => ({ turns: readingRounds() }),
172
+ });
173
+ await runDeepAgentTurn(first);
174
+ expect(first.record.lastFullStatus!.phase).toBe(ExecutionPhase.EXECUTION_TERMINATED);
175
+ expect(first.record.toolCalls()).toHaveLength(MIN_TOOL_ROUNDS);
176
+
177
+ // The follow-up's script is indexed by the rounds its transcript holds: a
178
+ // model that saw the ten rounds answers in text; one that lost them would
179
+ // play turn 0 and read a file.
180
+ const LOST_THREAD: ScriptedTurn = {
181
+ text: "Starting over.",
182
+ toolCalls: [{ id: "call-hermetic-lost-thread", name: "read_file", args: { file_path: "/lost.md" } }],
183
+ };
184
+ const PICKED_UP: ScriptedTurn = { text: "Picking up where I left off: all ten files are read." };
185
+ const roundsSeen: number[] = [];
186
+ const record = deepAgentExecutionRecord({ message: "Continue.", maxToolRounds: MIN_TOOL_ROUNDS });
187
+ const next = continueDeepAgentScenario(first, {
188
+ record,
189
+ script: () => ({ turns: [...Array.from({ length: MIN_TOOL_ROUNDS }, () => LOST_THREAD), PICKED_UP] }),
190
+ onTurn: (info) => {
191
+ roundsSeen.push(info.round);
192
+ },
193
+ });
194
+
195
+ // ── Act ──────────────────────────────────────────────────────────────────
196
+ const invocation = await runDeepAgentTurn(next);
197
+
198
+ // ── Assert ───────────────────────────────────────────────────────────────
199
+ expect(invocation.outcome.kind).toBe("returned");
200
+ expect(roundsSeen, "the follow-up's one model turn saw the ten rounds the stop checkpointed").toEqual([MIN_TOOL_ROUNDS]);
201
+ expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_COMPLETED]);
202
+ const persisted = record.lastFullStatus!;
203
+ expect(persisted.error).toBe("");
204
+ const answer = persisted.messages.filter((m) => m.type === MessageType.MESSAGE_AI).at(-1);
205
+ expect(answer?.content).toBe(PICKED_UP.text);
206
+ });
147
207
  });
@@ -5,7 +5,7 @@
5
5
  * Invariant pinned: the runtime's blueprint phase throws, the runtime's
6
6
  * catch takes its generic-error arm (`terminal-table.ts` `unexpectedErrorArm`),
7
7
  * persists ONE EXECUTION_FAILED status carrying
8
- * `status.error = "Execution failed: [Error] <message>"` and two system rows
8
+ * `status.error = "[Error] <message>"` and two system rows
9
9
  * — the boilerplate "Internal system error occurred. Please contact support
10
10
  * if this issue persists." and `"Error details: [Error] <message>"` — and
11
11
  * RETURNS the slim status (the workflow reads the phase; a throw would make
@@ -14,10 +14,14 @@
14
14
  *
15
15
  * The row shape is one of the alignments #1096 made on purpose: the
16
16
  * orchestrator wrote one row `"Error: [Error] <message>"`; the runtime's arm
17
- * writes the boilerplate plus the details row. `status.error` itself did
18
- * not move (the conformance suite pins it). The setup labels are the
17
+ * writes the boilerplate plus the details row. The setup labels are the
19
18
  * runtime's resolution phases'.
20
19
  *
20
+ * Golden hunk since stigmer#1122: `status.error` is `[Error] <message>`, the
21
+ * details row's text, where it was `Execution failed: [Error] <message>`.
22
+ * This arm alone framed the error; every other FAILED arm, and this
23
+ * harness's own thrown-turn classification, writes the failure unframed.
24
+ *
21
25
  * Carried from `index.test.ts` ("returns EXECUTION_FAILED status when setup
22
26
  * fails", "includes error message in failed status", "always returns a
23
27
  * serializable result").
@@ -70,7 +74,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
70
74
  env.dispose();
71
75
  });
72
76
 
73
- it("fails with the fault's message in status.error and one system row, and returns", async () => {
77
+ it("fails with the fault's message in status.error and two system rows, and returns", async () => {
74
78
  // ── Arrange ──────────────────────────────────────────────────────────────
75
79
  const record = deepAgentExecutionRecord({ message: "Never reaches a model." });
76
80
  const scenario = beginDeepAgentScenario({
@@ -101,7 +105,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
101
105
 
102
106
  // ── Assert: the copy ─────────────────────────────────────────────────────
103
107
  const final = record.lastFullStatus!;
104
- expect(final.error).toBe(`Execution failed: [Error] ${CONTROL_PLANE_FAULT}`);
108
+ expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
105
109
  expect(final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content)).toEqual([
106
110
  "Internal system error occurred. Please contact support if this issue persists.",
107
111
  `Error details: [Error] ${CONTROL_PLANE_FAULT}`,
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Hermetic: a THROWN TURN still carries every artifact it published — the
3
+ * native harness drains its in-flight inline publishes before the runtime
4
+ * writes the terminal status (stigmer#1116).
5
+ *
6
+ * The model writes a report in round one (`write_file`), and
7
+ * `streaming-side-effects.ts` hands the path to `InlinePublisher`, whose
8
+ * upload runs beside the stream. Round two's model call fails mid-stream
9
+ * (the scripted model's `fail` ending: a provider error), so the turn never
10
+ * reaches its settle — the one place that used to drain the publishes. The
11
+ * adapter classifies the throw as `failed`, and the runtime writes the
12
+ * turn's terminal status as soon as the adapter returns. The report is an
13
+ * edit on the tree, and the hermetic environment runs in capture mode (it
14
+ * carries artifact storage, as every hosted runner does), so that status is
15
+ * the file review's WAITING_FOR_APPROVAL with the failure's rows riding it
16
+ * and its phase deferred (`terminal-table.ts` `awaitingReviewArm`): the
17
+ * production shape of this fault.
18
+ *
19
+ * The order is made deterministic, not left to I/O timing: the upload is
20
+ * held until the turn asks for its publishes to drain, or until the
21
+ * activity has returned, whichever comes first. With the drain on the
22
+ * turn's every exit, the drain asks first and the artifact is on the
23
+ * persisted status. Without it the activity returns first, the upload
24
+ * lands afterwards on a status nobody persists again, and the assertion on
25
+ * the persisted artifacts fails — the defect this pins.
26
+ */
27
+
28
+ import { existsSync } from "node:fs";
29
+ import { join } from "node:path";
30
+ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
31
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
32
+ import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
33
+
34
+ vi.mock("../../../../shared/model-client.js", async () =>
35
+ (await import("../../__test-utils__/scripted-model-module.js")).scriptedModelClientModule(),
36
+ );
37
+ vi.mock("../../../../client/stigmer-client.js", async () =>
38
+ (await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
39
+ );
40
+
41
+ import {
42
+ ScriptedClock,
43
+ createHermeticEnvironment,
44
+ type HermeticEnvironment,
45
+ } from "../../../../__test-utils__/hermetic-activity.js";
46
+ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
47
+ import {
48
+ FIXTURE,
49
+ beginDeepAgentScenario,
50
+ deepAgentExecutionRecord,
51
+ runDeepAgentTurn,
52
+ } from "../../__test-utils__/hermetic-deep-agent.js";
53
+ import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
54
+ import { StreamingSideEffects } from "../../streaming-side-effects.js";
55
+ import { TERMINAL_COPY } from "../../../../harness/terminal-table.js";
56
+
57
+ const REPORT = "report.md";
58
+ const REPORT_BODY = "# Report\n\nWritten before the provider failed.\n";
59
+ const WRITE_CALL_ID = "call-hermetic-write-0001";
60
+ const PROVIDER_FAULT = "provider stream failed mid-turn";
61
+
62
+ function systemMessages(status: AgentExecutionStatus): string[] {
63
+ return status.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content);
64
+ }
65
+
66
+ describe("ExecuteDeepAgent hermetic — a thrown turn drains its inline publishes", () => {
67
+ let env: HermeticEnvironment;
68
+ let registry: ReturnType<typeof stubRegistryFetch>;
69
+ const clock = new ScriptedClock();
70
+
71
+ beforeAll(() => {
72
+ env = createHermeticEnvironment();
73
+ registry = stubRegistryFetch();
74
+ clock.install();
75
+ });
76
+
77
+ afterEach(() => {
78
+ vi.restoreAllMocks();
79
+ });
80
+
81
+ afterAll(() => {
82
+ clock.uninstall();
83
+ registry.restore();
84
+ env.dispose();
85
+ });
86
+
87
+ it("persists the turn's end with the artifact it published before the model call threw", async () => {
88
+ // ── Arrange ──────────────────────────────────────────────────────────────
89
+ const record = deepAgentExecutionRecord({ message: "Write the report." });
90
+ const scenario = beginDeepAgentScenario({
91
+ env,
92
+ clock,
93
+ record,
94
+ script: () => ({
95
+ turns: [
96
+ {
97
+ text: "Writing the report.",
98
+ toolCalls: [{ id: WRITE_CALL_ID, name: "write_file", args: { file_path: `/${REPORT}`, content: REPORT_BODY } }],
99
+ usage: { inputTokens: 1_300, outputTokens: 60 },
100
+ },
101
+ { text: "Now summarising", ends: { kind: "fail", error: new Error(PROVIDER_FAULT) } },
102
+ ],
103
+ }),
104
+ });
105
+
106
+ // The artifact's upload waits on `held`; the first of "the turn drains"
107
+ // and "the activity returned" releases it (the header's deterministic
108
+ // order). Capture mode's own blobs share the store and are never held:
109
+ // the runtime writes them after the adapter returns.
110
+ const artifactKey = `artifacts/${FIXTURE.executionId}/${REPORT}`;
111
+ let release = (): void => undefined;
112
+ const held = new Promise<void>((resolve) => {
113
+ release = resolve;
114
+ });
115
+ const upload = LocalArtifactStorage.prototype.upload;
116
+ const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload").mockImplementation(async function (
117
+ this: LocalArtifactStorage,
118
+ ...args: Parameters<LocalArtifactStorage["upload"]>
119
+ ) {
120
+ if (args[0] === artifactKey) await held;
121
+ return upload.apply(this, args);
122
+ });
123
+ const drain = StreamingSideEffects.prototype.drainPublishes;
124
+ const drains = vi.spyOn(StreamingSideEffects.prototype, "drainPublishes").mockImplementation(async function (
125
+ this: StreamingSideEffects,
126
+ ) {
127
+ release();
128
+ return drain.apply(this);
129
+ });
130
+
131
+ // ── Act ──────────────────────────────────────────────────────────────────
132
+ const invocation = await runDeepAgentTurn(scenario);
133
+ release();
134
+
135
+ // ── Assert: the turn failed, deferred behind the review, and returned ────
136
+ expect(invocation.outcome.kind, "a provider failure RETURNS the status").toBe("returned");
137
+ const final = record.lastFullStatus!;
138
+ expect(final.phase, "the edit reaches review before the failure settles").toBe(
139
+ ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
140
+ );
141
+ expect(final.error, "a deferred terminal writes no error yet").toBe("");
142
+ expect(systemMessages(final), "the failure's rows ride the review").toEqual([
143
+ TERMINAL_COPY.internalFailure.row,
144
+ `Error details: [Error] ${PROVIDER_FAULT}`,
145
+ ]);
146
+
147
+ // ── Assert: the artifact reached the terminal status ─────────────────────
148
+ expect(
149
+ uploads.mock.calls.map(([key]) => key).filter((key) => key === artifactKey),
150
+ "the write was published inline, once",
151
+ ).toHaveLength(1);
152
+ expect(final.artifacts.map((a) => [a.name, a.storageKey])).toEqual([[REPORT, artifactKey]]);
153
+ expect(drains, "the thrown turn drained its publishes").toHaveBeenCalled();
154
+ expect(existsSync(join(env.artifactPath, final.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
155
+
156
+ // ── Assert: hermeticity ──────────────────────────────────────────────────
157
+ expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
158
+ });
159
+ });