@stigmer/runner 3.10.0 → 3.11.1-dev.20260812192248

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 (355) hide show
  1. package/README.md +14 -1
  2. package/dist/.build-fingerprint +1 -1
  3. package/dist/activities/call-llm.js +9 -10
  4. package/dist/activities/call-llm.js.map +1 -1
  5. package/dist/activities/classify-tool-approvals.d.ts +2 -1
  6. package/dist/activities/classify-tool-approvals.js +28 -2
  7. package/dist/activities/classify-tool-approvals.js.map +1 -1
  8. package/dist/activities/discover-mcp-server.d.ts +39 -0
  9. package/dist/activities/discover-mcp-server.js +165 -28
  10. package/dist/activities/discover-mcp-server.js.map +1 -1
  11. package/dist/activities/emit-event.d.ts +14 -2
  12. package/dist/activities/emit-event.js +52 -17
  13. package/dist/activities/emit-event.js.map +1 -1
  14. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +8 -0
  15. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +1 -1
  16. package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
  17. package/dist/activities/execute-cursor/approval-state.d.ts +28 -2
  18. package/dist/activities/execute-cursor/approval-state.js +7 -1
  19. package/dist/activities/execute-cursor/approval-state.js.map +1 -1
  20. package/dist/activities/execute-cursor/attachment-resolver.d.ts +14 -0
  21. package/dist/activities/execute-cursor/attachment-resolver.js +18 -4
  22. package/dist/activities/execute-cursor/attachment-resolver.js.map +1 -1
  23. package/dist/activities/execute-cursor/blueprint-resolver.d.ts +1 -9
  24. package/dist/activities/execute-cursor/blueprint-resolver.js +6 -22
  25. package/dist/activities/execute-cursor/blueprint-resolver.js.map +1 -1
  26. package/dist/activities/execute-cursor/env-resolver.js +3 -1
  27. package/dist/activities/execute-cursor/env-resolver.js.map +1 -1
  28. package/dist/activities/execute-cursor/error-classifier.d.ts +59 -3
  29. package/dist/activities/execute-cursor/error-classifier.js +115 -6
  30. package/dist/activities/execute-cursor/error-classifier.js.map +1 -1
  31. package/dist/activities/execute-cursor/extract-structured-output.d.ts +29 -0
  32. package/dist/activities/execute-cursor/extract-structured-output.js +58 -0
  33. package/dist/activities/execute-cursor/extract-structured-output.js.map +1 -0
  34. package/dist/activities/execute-cursor/hook-script.d.ts +14 -3
  35. package/dist/activities/execute-cursor/hook-script.js +72 -10
  36. package/dist/activities/execute-cursor/hook-script.js.map +1 -1
  37. package/dist/activities/execute-cursor/index.d.ts +51 -11
  38. package/dist/activities/execute-cursor/index.js +177 -93
  39. package/dist/activities/execute-cursor/index.js.map +1 -1
  40. package/dist/activities/execute-cursor/mcp-resolver.d.ts +24 -1
  41. package/dist/activities/execute-cursor/mcp-resolver.js +5 -2
  42. package/dist/activities/execute-cursor/mcp-resolver.js.map +1 -1
  43. package/dist/activities/execute-cursor/model-pricing-data.d.ts +2 -0
  44. package/dist/activities/execute-cursor/model-pricing-data.js +13 -3
  45. package/dist/activities/execute-cursor/model-pricing-data.js.map +1 -1
  46. package/dist/activities/execute-cursor/prompt-builder.d.ts +51 -4
  47. package/dist/activities/execute-cursor/prompt-builder.js +49 -7
  48. package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
  49. package/dist/activities/execute-cursor/turn-recovery.d.ts +52 -0
  50. package/dist/activities/execute-cursor/turn-recovery.js +193 -0
  51. package/dist/activities/execute-cursor/turn-recovery.js.map +1 -0
  52. package/dist/activities/execute-cursor/turn-stream.js +4 -1
  53. package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
  54. package/dist/activities/execute-deep-agent/attachment-injector.d.ts +18 -1
  55. package/dist/activities/execute-deep-agent/attachment-injector.js +68 -23
  56. package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
  57. package/dist/activities/execute-deep-agent/environment.js +3 -1
  58. package/dist/activities/execute-deep-agent/environment.js.map +1 -1
  59. package/dist/activities/execute-deep-agent/index.js +15 -0
  60. package/dist/activities/execute-deep-agent/index.js.map +1 -1
  61. package/dist/activities/execute-deep-agent/prompt-builder.d.ts +7 -7
  62. package/dist/activities/execute-deep-agent/prompt-builder.js +8 -2
  63. package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
  64. package/dist/activities/execute-deep-agent/setup.d.ts +10 -0
  65. package/dist/activities/execute-deep-agent/setup.js +65 -24
  66. package/dist/activities/execute-deep-agent/setup.js.map +1 -1
  67. package/dist/activities/execute-deep-agent/shell-env.d.ts +5 -1
  68. package/dist/activities/execute-deep-agent/shell-env.js +7 -9
  69. package/dist/activities/execute-deep-agent/shell-env.js.map +1 -1
  70. package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +18 -1
  71. package/dist/activities/execute-deep-agent/subagent-transformer.js +15 -2
  72. package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
  73. package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +27 -6
  74. package/dist/activities/execute-deep-agent/subagent-wiring.js +26 -5
  75. package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
  76. package/dist/activities/hydrate-workflow-execution.js +8 -3
  77. package/dist/activities/hydrate-workflow-execution.js.map +1 -1
  78. package/dist/activities/run-command.d.ts +5 -2
  79. package/dist/activities/run-command.js +22 -10
  80. package/dist/activities/run-command.js.map +1 -1
  81. package/dist/activities/run-env.d.ts +40 -0
  82. package/dist/activities/run-env.js +66 -0
  83. package/dist/activities/run-env.js.map +1 -0
  84. package/dist/activities/workflow-event-activities.d.ts +28 -10
  85. package/dist/activities/workflow-event-activities.js +87 -58
  86. package/dist/activities/workflow-event-activities.js.map +1 -1
  87. package/dist/bootstrap.js +5 -0
  88. package/dist/bootstrap.js.map +1 -1
  89. package/dist/claimcheck/payload-codec.js +21 -1
  90. package/dist/claimcheck/payload-codec.js.map +1 -1
  91. package/dist/client/stigmer-client.d.ts +83 -21
  92. package/dist/client/stigmer-client.js +106 -29
  93. package/dist/client/stigmer-client.js.map +1 -1
  94. package/dist/encryption/config.d.ts +64 -0
  95. package/dist/encryption/config.js +108 -0
  96. package/dist/encryption/config.js.map +1 -0
  97. package/dist/encryption/index.d.ts +3 -0
  98. package/dist/encryption/index.js +3 -0
  99. package/dist/encryption/index.js.map +1 -0
  100. package/dist/encryption/payload-codec.d.ts +41 -0
  101. package/dist/encryption/payload-codec.js +130 -0
  102. package/dist/encryption/payload-codec.js.map +1 -0
  103. package/dist/middleware/index.d.ts +3 -0
  104. package/dist/middleware/index.js +7 -0
  105. package/dist/middleware/index.js.map +1 -1
  106. package/dist/middleware/path-normalization.d.ts +57 -0
  107. package/dist/middleware/path-normalization.js +109 -0
  108. package/dist/middleware/path-normalization.js.map +1 -0
  109. package/dist/middleware/types.d.ts +8 -0
  110. package/dist/payload-codecs.d.ts +17 -0
  111. package/dist/payload-codecs.js +42 -0
  112. package/dist/payload-codecs.js.map +1 -0
  113. package/dist/preflight.d.ts +31 -0
  114. package/dist/preflight.js +43 -0
  115. package/dist/preflight.js.map +1 -1
  116. package/dist/runner-manager.js +21 -17
  117. package/dist/runner-manager.js.map +1 -1
  118. package/dist/runner.js +11 -18
  119. package/dist/runner.js.map +1 -1
  120. package/dist/shared/approval-policy.d.ts +9 -3
  121. package/dist/shared/approval-policy.js +15 -6
  122. package/dist/shared/approval-policy.js.map +1 -1
  123. package/dist/shared/artifact-storage.d.ts +23 -1
  124. package/dist/shared/artifact-storage.js +51 -10
  125. package/dist/shared/artifact-storage.js.map +1 -1
  126. package/dist/shared/attachment-naming.d.ts +53 -0
  127. package/dist/shared/attachment-naming.js +59 -0
  128. package/dist/shared/attachment-naming.js.map +1 -0
  129. package/dist/shared/caller-identity.d.ts +23 -2
  130. package/dist/shared/caller-identity.js +36 -5
  131. package/dist/shared/caller-identity.js.map +1 -1
  132. package/dist/shared/channel-attachment.js +1 -0
  133. package/dist/shared/channel-attachment.js.map +1 -1
  134. package/dist/shared/checkpointer/http-saver.d.ts +26 -1
  135. package/dist/shared/checkpointer/http-saver.js +54 -8
  136. package/dist/shared/checkpointer/http-saver.js.map +1 -1
  137. package/dist/shared/conversation-attachment.js +1 -0
  138. package/dist/shared/conversation-attachment.js.map +1 -1
  139. package/dist/shared/conversation-catchup.d.ts +9 -2
  140. package/dist/shared/conversation-catchup.js +39 -6
  141. package/dist/shared/conversation-catchup.js.map +1 -1
  142. package/dist/shared/datastore-attachment.d.ts +50 -7
  143. package/dist/shared/datastore-attachment.js +93 -11
  144. package/dist/shared/datastore-attachment.js.map +1 -1
  145. package/dist/shared/grpc-retry.d.ts +5 -1
  146. package/dist/shared/grpc-retry.js +5 -1
  147. package/dist/shared/grpc-retry.js.map +1 -1
  148. package/dist/shared/http-retry.d.ts +85 -0
  149. package/dist/shared/http-retry.js +101 -0
  150. package/dist/shared/http-retry.js.map +1 -0
  151. package/dist/shared/llm-backend.d.ts +275 -0
  152. package/dist/shared/llm-backend.js +425 -0
  153. package/dist/shared/llm-backend.js.map +1 -0
  154. package/dist/shared/llm-proxy.d.ts +8 -0
  155. package/dist/shared/llm-proxy.js +15 -0
  156. package/dist/shared/llm-proxy.js.map +1 -1
  157. package/dist/shared/mcp-enabled-tools.d.ts +57 -0
  158. package/dist/shared/mcp-enabled-tools.js +86 -0
  159. package/dist/shared/mcp-enabled-tools.js.map +1 -0
  160. package/dist/shared/mcp-manager.d.ts +11 -5
  161. package/dist/shared/mcp-manager.js +47 -8
  162. package/dist/shared/mcp-manager.js.map +1 -1
  163. package/dist/shared/mcp-resolver.d.ts +39 -2
  164. package/dist/shared/mcp-resolver.js +38 -2
  165. package/dist/shared/mcp-resolver.js.map +1 -1
  166. package/dist/shared/mcp-schema-sanitizer.d.ts +70 -0
  167. package/dist/shared/mcp-schema-sanitizer.js +197 -0
  168. package/dist/shared/mcp-schema-sanitizer.js.map +1 -0
  169. package/dist/shared/model-client.d.ts +18 -5
  170. package/dist/shared/model-client.js +149 -18
  171. package/dist/shared/model-client.js.map +1 -1
  172. package/dist/shared/model-error.js +198 -5
  173. package/dist/shared/model-error.js.map +1 -1
  174. package/dist/shared/model-pricing-data.d.ts +2 -0
  175. package/dist/shared/model-pricing-data.js +13 -3
  176. package/dist/shared/model-pricing-data.js.map +1 -1
  177. package/dist/shared/model-registry.js +3 -2
  178. package/dist/shared/model-registry.js.map +1 -1
  179. package/dist/shared/plan-mode-permissions.d.ts +33 -0
  180. package/dist/shared/plan-mode-permissions.js +35 -0
  181. package/dist/shared/plan-mode-permissions.js.map +1 -0
  182. package/dist/shared/registry-endpoint.d.ts +20 -0
  183. package/dist/shared/registry-endpoint.js +25 -0
  184. package/dist/shared/registry-endpoint.js.map +1 -1
  185. package/dist/shared/runner-credential-keys.d.ts +24 -0
  186. package/dist/shared/runner-credential-keys.js +47 -0
  187. package/dist/shared/runner-credential-keys.js.map +1 -0
  188. package/dist/worker.d.ts +2 -1
  189. package/dist/worker.js +2 -4
  190. package/dist/worker.js.map +1 -1
  191. package/dist/workflow-engine/resolve.d.ts +19 -1
  192. package/dist/workflow-engine/resolve.js +37 -2
  193. package/dist/workflow-engine/resolve.js.map +1 -1
  194. package/dist/workflow-engine/types.d.ts +18 -0
  195. package/dist/workflow-engine/types.js.map +1 -1
  196. package/dist/workflows/call-agent-orchestrator.d.ts +9 -0
  197. package/dist/workflows/call-agent-orchestrator.js +1 -0
  198. package/dist/workflows/call-agent-orchestrator.js.map +1 -1
  199. package/dist/workflows/connect-mcp-server.js +9 -0
  200. package/dist/workflows/connect-mcp-server.js.map +1 -1
  201. package/dist/workflows/engine-core.js +23 -2
  202. package/dist/workflows/engine-core.js.map +1 -1
  203. package/dist/workflows/execute-from-execution.d.ts +1 -1
  204. package/dist/workflows/execute-from-execution.js +11 -1
  205. package/dist/workflows/execute-from-execution.js.map +1 -1
  206. package/dist/workflows/types.d.ts +8 -0
  207. package/package.json +9 -4
  208. package/src/__test-utils__/__tests__/vitest-global-setup.test.ts +37 -0
  209. package/src/__test-utils__/vitest-global-setup.ts +50 -0
  210. package/src/__tests__/bootstrap.test.ts +43 -0
  211. package/src/__tests__/claimcheck-codec.test.ts +36 -0
  212. package/src/__tests__/encryption-codec.test.ts +287 -0
  213. package/src/__tests__/fixtures/encrypted-payload-fixture.json +15 -0
  214. package/src/__tests__/history-encryption-e2e.test.ts +243 -0
  215. package/src/__tests__/preflight.test.ts +65 -2
  216. package/src/activities/__tests__/call-llm.test.ts +75 -0
  217. package/src/activities/__tests__/classify-tool-approvals.test.ts +117 -1
  218. package/src/activities/__tests__/discover-mcp-server.hang.test.ts +103 -0
  219. package/src/activities/__tests__/discover-mcp-server.test.ts +242 -1
  220. package/src/activities/__tests__/error-classifier.test.ts +94 -2
  221. package/src/activities/__tests__/run-command.test.ts +221 -0
  222. package/src/activities/__tests__/workflow-event-activities.test.ts +107 -8
  223. package/src/activities/call-llm.ts +9 -16
  224. package/src/activities/classify-tool-approvals.ts +34 -4
  225. package/src/activities/discover-mcp-server.ts +203 -31
  226. package/src/activities/emit-event.ts +65 -21
  227. package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +9 -0
  228. package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +14 -0
  229. package/src/activities/execute-cursor/__tests__/attachment-resolver.test.ts +53 -0
  230. package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +151 -16
  231. package/src/activities/execute-cursor/__tests__/error-classifier-extraction.test.ts +208 -0
  232. package/src/activities/execute-cursor/__tests__/extract-structured-output.test.ts +120 -0
  233. package/src/activities/execute-cursor/__tests__/hook-script.test.ts +93 -0
  234. package/src/activities/execute-cursor/__tests__/mcp-resolver.test.ts +125 -0
  235. package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +62 -1
  236. package/src/activities/execute-cursor/__tests__/prompt-builder-delegation.test.ts +1 -1
  237. package/src/activities/execute-cursor/__tests__/turn-recovery.test.ts +160 -0
  238. package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +13 -0
  239. package/src/activities/execute-cursor/approval-state.ts +30 -1
  240. package/src/activities/execute-cursor/attachment-resolver.ts +31 -3
  241. package/src/activities/execute-cursor/blueprint-resolver.ts +7 -27
  242. package/src/activities/execute-cursor/env-resolver.ts +3 -1
  243. package/src/activities/execute-cursor/error-classifier.ts +131 -7
  244. package/src/activities/execute-cursor/extract-structured-output.ts +72 -0
  245. package/src/activities/execute-cursor/hook-script.ts +74 -10
  246. package/src/activities/execute-cursor/index.ts +209 -109
  247. package/src/activities/execute-cursor/mcp-resolver.ts +36 -2
  248. package/src/activities/execute-cursor/model-pricing-data.ts +22 -3
  249. package/src/activities/execute-cursor/prompt-builder.ts +85 -9
  250. package/src/activities/execute-cursor/turn-recovery.ts +208 -0
  251. package/src/activities/execute-cursor/turn-stream.ts +5 -2
  252. package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +110 -8
  253. package/src/activities/execute-deep-agent/__tests__/datastore-degradation.test.ts +104 -0
  254. package/src/activities/execute-deep-agent/__tests__/hitl-reject.test.ts +2 -0
  255. package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +1 -0
  256. package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +1 -0
  257. package/src/activities/execute-deep-agent/__tests__/plan-mode-path-normalization.test.ts +174 -0
  258. package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +34 -5
  259. package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +1 -0
  260. package/src/activities/execute-deep-agent/__tests__/shell-env.test.ts +24 -10
  261. package/src/activities/execute-deep-agent/__tests__/subagent-plan-mode-permissions.test.ts +178 -0
  262. package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +12 -7
  263. package/src/activities/execute-deep-agent/attachment-injector.ts +94 -30
  264. package/src/activities/execute-deep-agent/environment.ts +3 -1
  265. package/src/activities/execute-deep-agent/index.ts +20 -0
  266. package/src/activities/execute-deep-agent/prompt-builder.ts +20 -10
  267. package/src/activities/execute-deep-agent/setup.ts +85 -29
  268. package/src/activities/execute-deep-agent/shell-env.ts +8 -9
  269. package/src/activities/execute-deep-agent/subagent-transformer.ts +30 -2
  270. package/src/activities/execute-deep-agent/subagent-wiring.ts +39 -6
  271. package/src/activities/hydrate-workflow-execution.ts +8 -3
  272. package/src/activities/run-command.ts +25 -12
  273. package/src/activities/run-env.ts +79 -0
  274. package/src/activities/workflow-event-activities.ts +96 -69
  275. package/src/bootstrap.ts +5 -0
  276. package/src/claimcheck/payload-codec.ts +33 -1
  277. package/src/client/__tests__/stigmer-client.test.ts +110 -11
  278. package/src/client/stigmer-client.ts +150 -37
  279. package/src/encryption/config.ts +155 -0
  280. package/src/encryption/index.ts +3 -0
  281. package/src/encryption/payload-codec.ts +152 -0
  282. package/src/middleware/__tests__/path-normalization.test.ts +140 -0
  283. package/src/middleware/index.ts +8 -0
  284. package/src/middleware/path-normalization.ts +125 -0
  285. package/src/middleware/types.ts +11 -0
  286. package/src/payload-codecs.ts +62 -0
  287. package/src/preflight.ts +45 -0
  288. package/src/runner-manager.ts +26 -26
  289. package/src/runner.ts +12 -27
  290. package/src/shared/__tests__/approval-policy.test.ts +82 -39
  291. package/src/shared/__tests__/artifact-storage.test.ts +145 -2
  292. package/src/shared/__tests__/attachment-naming.test.ts +159 -0
  293. package/src/shared/__tests__/bedrock-adapter.test.ts +213 -0
  294. package/src/shared/__tests__/bedrock-seam.test.ts +390 -0
  295. package/src/shared/__tests__/caller-identity.test.ts +25 -0
  296. package/src/shared/__tests__/channel-attachment.test.ts +1 -1
  297. package/src/shared/__tests__/connect-backfill.test.ts +1 -0
  298. package/src/shared/__tests__/conversation-attachment.test.ts +1 -1
  299. package/src/shared/__tests__/conversation-catchup.test.ts +24 -0
  300. package/src/shared/__tests__/datastore-attachment.test.ts +129 -1
  301. package/src/shared/__tests__/foundry-adapter.test.ts +276 -0
  302. package/src/shared/__tests__/foundry-seam.test.ts +482 -0
  303. package/src/shared/__tests__/http-retry.test.ts +68 -0
  304. package/src/shared/__tests__/llm-backend.test.ts +616 -0
  305. package/src/shared/__tests__/mcp-enabled-tools.test.ts +86 -0
  306. package/src/shared/__tests__/mcp-manager.test.ts +137 -15
  307. package/src/shared/__tests__/mcp-resolver.test.ts +146 -3
  308. package/src/shared/__tests__/mcp-schema-sanitizer.test.ts +267 -0
  309. package/src/shared/__tests__/model-client.test.ts +200 -0
  310. package/src/shared/__tests__/model-error.test.ts +289 -1
  311. package/src/shared/__tests__/model-pricing.test.ts +62 -2
  312. package/src/shared/__tests__/model-registry.test.ts +22 -0
  313. package/src/shared/__tests__/synthesized-attachment.test.ts +1 -0
  314. package/src/shared/__tests__/vertex-adapter.test.ts +169 -0
  315. package/src/shared/__tests__/vertex-seam.test.ts +295 -0
  316. package/src/shared/approval-policy.ts +14 -7
  317. package/src/shared/artifact-storage.ts +75 -10
  318. package/src/shared/attachment-naming.ts +78 -0
  319. package/src/shared/caller-identity.ts +40 -5
  320. package/src/shared/channel-attachment.ts +1 -0
  321. package/src/shared/checkpointer/__tests__/http-saver.test.ts +196 -1
  322. package/src/shared/checkpointer/http-saver.ts +71 -8
  323. package/src/shared/conversation-attachment.ts +1 -0
  324. package/src/shared/conversation-catchup.ts +39 -6
  325. package/src/shared/datastore-attachment.ts +106 -11
  326. package/src/shared/grpc-retry.ts +5 -1
  327. package/src/shared/http-retry.ts +139 -0
  328. package/src/shared/llm-backend.ts +544 -0
  329. package/src/shared/llm-proxy.ts +15 -0
  330. package/src/shared/mcp-enabled-tools.ts +105 -0
  331. package/src/shared/mcp-manager.ts +56 -8
  332. package/src/shared/mcp-resolver.ts +73 -2
  333. package/src/shared/mcp-schema-sanitizer.ts +224 -0
  334. package/src/shared/model-client.ts +179 -19
  335. package/src/shared/model-error.ts +222 -4
  336. package/src/shared/model-pricing-data.ts +22 -3
  337. package/src/shared/model-registry.ts +7 -2
  338. package/src/shared/plan-mode-permissions.ts +37 -0
  339. package/src/shared/registry-endpoint.ts +27 -0
  340. package/src/shared/runner-credential-keys.ts +46 -0
  341. package/src/worker.ts +4 -5
  342. package/src/workflow-engine/__tests__/tasks/emit-event.test.ts +127 -49
  343. package/src/workflow-engine/resolve.ts +48 -2
  344. package/src/workflow-engine/types.ts +18 -0
  345. package/src/workflows/__tests__/connect-mcp-server.test.ts +25 -0
  346. package/src/workflows/__tests__/execute-serverless-workflow.test.ts +68 -2
  347. package/src/workflows/call-agent-orchestrator.ts +10 -0
  348. package/src/workflows/connect-mcp-server.ts +9 -0
  349. package/src/workflows/engine-core.ts +23 -2
  350. package/src/workflows/execute-from-execution.ts +12 -2
  351. package/src/workflows/types.ts +8 -0
  352. package/dist/activities/execute-cursor/mcp-config.d.ts +0 -30
  353. package/dist/activities/execute-cursor/mcp-config.js +0 -39
  354. package/dist/activities/execute-cursor/mcp-config.js.map +0 -1
  355. package/src/activities/execute-cursor/mcp-config.ts +0 -66
@@ -36,6 +36,7 @@ import {
36
36
  visionDisclosureLines,
37
37
  type NotViewableEntry,
38
38
  } from "../../shared/attachment-vision.js";
39
+ import { formatTurnRecoveryText } from "./turn-recovery.js";
39
40
  import { PLAN_MODE_DIRECTIVE } from "../../shared/plan-mode-prompt.js";
40
41
  import {
41
42
  buildImplementPlanDirective,
@@ -71,6 +72,18 @@ export interface VisionPromptInfo {
71
72
  notViewable: NotViewableEntry[];
72
73
  }
73
74
 
75
+ /**
76
+ * One resolved attachment as the `<input_files>` section renders it —
77
+ * the final workspace-relative path plus the duplicate-rename disclosure
78
+ * (attachment-resolver.ts / shared/attachment-naming.ts).
79
+ */
80
+ export interface AttachmentPromptEntry {
81
+ /** Workspace-relative path the agent reads (`.stigmer/inputs/{filename}`). */
82
+ path: string;
83
+ /** Original filename, present only when a duplicate name was renamed. */
84
+ renamedFrom?: string;
85
+ }
86
+
74
87
  export interface EnhancedPromptOptions {
75
88
  instructions: string;
76
89
  userMessage: string;
@@ -78,7 +91,10 @@ export interface EnhancedPromptOptions {
78
91
  /**
79
92
  * Datastores attached via `datastore_usages` — rendered as the
80
93
  * `<available_datastores>` section (DD-005 SD-5, skills precedent)
81
- * pointing the model at the synthesized record tools.
94
+ * pointing the model at the synthesized record tools. Always the healthy
95
+ * rendering here: the Cursor SDK connects MCP itself, so this harness can
96
+ * never observe the live roster — the section's standing
97
+ * failure-disclosure instruction is its whole coverage (issue #325).
82
98
  */
83
99
  datastoreUsages?: DatastoreUsage[];
84
100
  /**
@@ -90,7 +106,7 @@ export interface EnhancedPromptOptions {
90
106
  subAgents: SubAgent[];
91
107
  workspaceDirs: string[];
92
108
  workspaceFileRefs: string[];
93
- attachmentPaths: string[];
109
+ attachments: AttachmentPromptEntry[];
94
110
  /** Inline/degraded image facts for the input-files section (T04 vision). */
95
111
  vision?: VisionPromptInfo;
96
112
  interactionMode?: InteractionMode;
@@ -153,7 +169,7 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
153
169
 
154
170
  const implementPlan = formatImplementPlanSection(
155
171
  options.buildFromPlan,
156
- options.attachmentPaths,
172
+ options.attachments,
157
173
  );
158
174
  if (implementPlan) {
159
175
  sections.push(implementPlan);
@@ -195,8 +211,8 @@ export function buildEnhancedPrompt(options: EnhancedPromptOptions): string {
195
211
  sections.push(formatWorkspaceContext(safeDirs));
196
212
  }
197
213
 
198
- if (options.attachmentPaths.length > 0) {
199
- sections.push(formatInputFiles(options.attachmentPaths, options.vision));
214
+ if (options.attachments.length > 0) {
215
+ sections.push(formatInputFiles(options.attachments, options.vision));
200
216
  }
201
217
 
202
218
  if (options.workspaceFileRefs.length > 0) {
@@ -323,6 +339,56 @@ export function buildReinvocationPrompt(
323
339
  return parts.join("\n\n");
324
340
  }
325
341
 
342
+ /**
343
+ * Build the prompt for a fresh agent that replaced a lost one MID-HITL —
344
+ * the stored handle failed to resume when the approval landed, or the
345
+ * resumed handle proved poisoned mid-send (issue #366, both crossings).
346
+ *
347
+ * A resumed agent gets {@link buildReinvocationPrompt} alone because its
348
+ * native conversation carries everything else. A replacement agent's
349
+ * conversation is EMPTY, so the bare decisions prompt would strand it with
350
+ * instructions and no story — and, because recovery repoints the session at
351
+ * the new agent, every later turn would inherit that amnesia. This prompt
352
+ * rebuilds the whole story in chronological-narrative order:
353
+ *
354
+ * 1. the full enhanced prompt (blueprint, standing context, protocol, and
355
+ * the turn's ORIGINAL user message as `<user_request>`),
356
+ * 2. `<turn_recovery>` — the state-loss disclosure plus the recorded
357
+ * transcript of the turn so far (turn-recovery.ts; rendered even with no
358
+ * transcript, since without the disclosure the decisions below would read
359
+ * as reactions to proposals this agent never made),
360
+ * 3. the reinvocation decisions verbatim — already-applied / approved /
361
+ * skipped semantics byte-identical to the resumed path, ending on its
362
+ * "continue the task" directive.
363
+ *
364
+ * Appending past `<user_request>` is the chronology speaking (request →
365
+ * work done → decisions → continue), the same shape as the activity-level
366
+ * structured-output suffix.
367
+ */
368
+ export function buildHitlRecoveryPrompt(
369
+ options: EnhancedPromptOptions,
370
+ recovery: {
371
+ turnDigest: string | undefined;
372
+ pendingApprovals: PendingApproval[];
373
+ approvalDecisions: Map<string, ApprovalAction>;
374
+ appliedToolCallIds?: ReadonlySet<string>;
375
+ },
376
+ ): string {
377
+ return [
378
+ buildEnhancedPrompt(options),
379
+ formatTurnRecoverySection(recovery.turnDigest),
380
+ buildReinvocationPrompt(
381
+ recovery.pendingApprovals,
382
+ recovery.approvalDecisions,
383
+ recovery.appliedToolCallIds,
384
+ ),
385
+ ].join("\n\n---\n\n");
386
+ }
387
+
388
+ export function formatTurnRecoverySection(digest: string | undefined): string {
389
+ return `<turn_recovery>\n${formatTurnRecoveryText(digest)}\n</turn_recovery>`;
390
+ }
391
+
326
392
  /**
327
393
  * Render a pending approval as a short human-readable action description for the
328
394
  * reinvocation prompt. Prefers the already-resolved approval message; falls back
@@ -479,8 +545,18 @@ export function formatWorkspaceContext(dirs: string[]): string {
479
545
  ].join("\n");
480
546
  }
481
547
 
482
- export function formatInputFiles(paths: string[], vision?: VisionPromptInfo): string {
483
- const entries = paths.map((p) => `- \`${p}\``);
548
+ export function formatInputFiles(
549
+ attachments: readonly AttachmentPromptEntry[],
550
+ vision?: VisionPromptInfo,
551
+ ): string {
552
+ // A duplicate-renamed file (attachment-naming.ts) discloses its original
553
+ // name so the agent can connect "the two report.pdfs" in the user's
554
+ // message to distinct files on disk.
555
+ const entries = attachments.map((a) =>
556
+ a.renamedFrom !== undefined
557
+ ? `- \`${a.path}\` (renamed from duplicate '${a.renamedFrom}')`
558
+ : `- \`${a.path}\``,
559
+ );
484
560
  // The vision lines (shared wording, attachment-vision.ts) tell the model
485
561
  // which of these files it can already SEE inline versus which degraded to
486
562
  // path-only — without them an agent silently ignores a photo the user
@@ -541,14 +617,14 @@ export function formatInteractionModePrefix(
541
617
  */
542
618
  export function formatImplementPlanSection(
543
619
  buildFromPlan: boolean | undefined,
544
- attachmentPaths: string[],
620
+ attachments: readonly AttachmentPromptEntry[],
545
621
  ): string | undefined {
546
622
  if (!buildFromPlan) {
547
623
  return undefined;
548
624
  }
549
625
 
550
626
  const directive = buildImplementPlanDirective(
551
- findApprovedPlanPath(attachmentPaths),
627
+ findApprovedPlanPath(attachments.map((a) => a.path)),
552
628
  );
553
629
  return ["<implement_plan>", directive, "</implement_plan>"].join("\n");
554
630
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Turn-recovery digest: the platform's recorded transcript of the current
3
+ * turn, rendered as prompt context for a fresh agent that replaced one whose
4
+ * conversation state was lost mid-turn (issue #366).
5
+ *
6
+ * A HITL-paused turn can lose its agent two ways — the stored handle fails
7
+ * to resume when the approval lands, or the resumed handle turns out to be
8
+ * poisoned mid-send. Either way the replacement agent starts with an EMPTY
9
+ * conversation, so the approval decisions alone read as instructions with no
10
+ * story: the agent no longer knows what it was doing. This module rebuilds
11
+ * that story from `status.messages` — the persisted execution transcript
12
+ * seeded on every re-invocation — which is the platform's durable record of
13
+ * the turn AND exactly what the user watched happen (introspecting the dead
14
+ * agent's own conversation is best-effort at most; the handle is the thing
15
+ * that failed).
16
+ *
17
+ * Doctrine (mirrors `ChannelRolloverBridgeComposer`, cloud DD-013 — the
18
+ * platform's reference for conversation digests):
19
+ * - Content here, presentation framing separate: {@link composeTurnRecoveryDigest}
20
+ * emits bare `Assistant:` / `Tool:` / `System:` lines;
21
+ * {@link formatTurnRecoveryText} adds the behavioral preamble; the prompt
22
+ * builder wraps the section tag (the context-bridge / conversation-catchup
23
+ * split).
24
+ * - Oldest-first reading order; over budget, the OLDEST lines drop first
25
+ * (recency wins) and the drop is disclosed.
26
+ * - Best-effort by contract: composition never throws — a failed digest
27
+ * degrades to recovery-without-transcript, never a failed recovery.
28
+ *
29
+ * Scope: the CURRENT turn only. Prior turns live in other execution records
30
+ * and are not recoverable runner-side (that is the rollover bridge's domain,
31
+ * composed cloud-side); the enhanced prompt's standing-context sections carry
32
+ * whatever the session already had.
33
+ */
34
+
35
+ import type { AgentMessage, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
36
+ import { MessageType, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
37
+
38
+ /** Per-line text budget; longer texts are cut with an ellipsis (DD-013 twin). */
39
+ const MAX_LINE_CHARS = 400;
40
+
41
+ /**
42
+ * Args-preview budget inside a tool line — the `describeApproval` precedent
43
+ * (prompt-builder.ts), so tool renderings are bounded the same way on both
44
+ * surfaces that describe them.
45
+ */
46
+ const MAX_ARGS_CHARS = 200;
47
+
48
+ /**
49
+ * Whole-digest budget (DD-013 twin). Enforced by dropping the OLDEST lines
50
+ * first — recency wins, matching how a human skims their own recent work
51
+ * before continuing it.
52
+ */
53
+ const MAX_DIGEST_CHARS = 4000;
54
+
55
+ const ELLIPSIS = "\u2026";
56
+
57
+ /** Disclosed in place of the dropped prefix so the record never lies by omission. */
58
+ const OMISSION_NOTICE = `[${ELLIPSIS} earlier activity in this turn omitted for length]`;
59
+
60
+ /**
61
+ * How the transcript is introduced to the model. Establishes the three facts
62
+ * the replacement agent must hold: the work below is its OWN (continue, don't
63
+ * restart), completed actions are DONE (don't redo them), and the workspace —
64
+ * not this summary — is the source of truth for exact current state.
65
+ */
66
+ const TURN_RECOVERY_PREAMBLE =
67
+ "You had already started working on the user's request above, but the " +
68
+ "session holding that conversation was lost, so you do not remember it. " +
69
+ "Below is the platform's recorded transcript of your progress in this " +
70
+ "turn, oldest first. Treat it as work YOU already did: do not start the " +
71
+ "task over, do not redo actions shown as completed, and check the " +
72
+ "workspace's current state where exact details matter.";
73
+
74
+ /**
75
+ * Fallback framing when the turn produced no renderable transcript (or
76
+ * composition failed): the state-loss disclosure still matters — without it,
77
+ * the appended approval decisions read as reactions to proposals this agent
78
+ * never made.
79
+ */
80
+ const TURN_RECOVERY_NO_TRANSCRIPT =
81
+ "You had already started working on the user's request above, but the " +
82
+ "session holding that conversation was lost, so you do not remember it " +
83
+ "and no transcript of your progress is available. Check the workspace's " +
84
+ "current state to see what you already did before continuing.";
85
+
86
+ /**
87
+ * Render the turn's persisted transcript as bare digest lines, or undefined
88
+ * when there is nothing worth carrying. Never throws.
89
+ *
90
+ * Human messages are skipped — the turn's user message is already rendered
91
+ * in the enhanced prompt's `<user_request>`, and duplicating it here would
92
+ * dilute the budget. Thinking content is skipped as model-internal (the
93
+ * replacement agent forms its own). System notices are kept: platform
94
+ * messages like budget warnings or degradation notices are turn context the
95
+ * agent acted under.
96
+ */
97
+ export function composeTurnRecoveryDigest(
98
+ messages: readonly AgentMessage[],
99
+ ): string | undefined {
100
+ try {
101
+ const lines: string[] = [];
102
+ for (const message of messages) {
103
+ lines.push(...messageLines(message));
104
+ }
105
+ if (lines.length === 0) return undefined;
106
+
107
+ // Oldest-first; drop from the FRONT when over budget so the newest work
108
+ // always survives, and disclose the drop (DD-013 enforcement shape). The
109
+ // notice participates in the budget so the result never overshoots.
110
+ let first = 0;
111
+ while (first < lines.length && totalLength(lines, first) > MAX_DIGEST_CHARS) {
112
+ first++;
113
+ }
114
+ if (first >= lines.length) {
115
+ // Degenerate case: even the newest line alone busts the budget (it is
116
+ // already line-truncated, so this would take a pathological budget/line
117
+ // ratio) — keep that one line rather than rendering nothing.
118
+ first = lines.length - 1;
119
+ }
120
+ const kept = lines.slice(first);
121
+ if (first > 0) kept.unshift(OMISSION_NOTICE);
122
+ return kept.join("\n");
123
+ } catch (err) {
124
+ console.warn("Turn-recovery digest composition failed — recovering without a transcript:", err);
125
+ return undefined;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The framed recovery body (preamble + digest), ready for section wrapping.
131
+ * Accepts an absent digest: the state-loss disclosure is load-bearing even
132
+ * when there is no transcript to show (see {@link TURN_RECOVERY_NO_TRANSCRIPT}).
133
+ */
134
+ export function formatTurnRecoveryText(digest: string | undefined): string {
135
+ if (digest === undefined || digest.trim() === "") {
136
+ return TURN_RECOVERY_NO_TRANSCRIPT;
137
+ }
138
+ return `${TURN_RECOVERY_PREAMBLE}\n\n${digest.trim()}`;
139
+ }
140
+
141
+ /** The digest lines one transcript message contributes (possibly none). */
142
+ function messageLines(message: AgentMessage): string[] {
143
+ const lines: string[] = [];
144
+ const text = message.content.trim();
145
+ switch (message.type) {
146
+ case MessageType.MESSAGE_AI:
147
+ if (text) lines.push(`Assistant: ${truncate(text, MAX_LINE_CHARS)}`);
148
+ // Tool calls ride AI messages (message.proto contract); render them
149
+ // even when the surrounding text is blank.
150
+ for (const toolCall of message.toolCalls) {
151
+ lines.push(toolCallLine(toolCall));
152
+ }
153
+ return lines;
154
+ case MessageType.MESSAGE_SYSTEM:
155
+ if (text) lines.push(`System: ${truncate(text, MAX_LINE_CHARS)}`);
156
+ return lines;
157
+ default:
158
+ // Human (already in <user_request>), thinking (model-internal), tool
159
+ // results (each ToolCall line already carries its outcome), unknown.
160
+ return lines;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * One bounded line per tool call: what was attempted and how it ended.
166
+ * Prefers the resolved approval message where one exists — the same
167
+ * human-meaningful description the user approved against
168
+ * (the `describeApproval` convention).
169
+ */
170
+ function toolCallLine(toolCall: ToolCall): string {
171
+ const action = toolCall.approvalMessage
172
+ ? toolCall.approvalMessage
173
+ : toolCall.argsPreview
174
+ ? `${toolCall.name}(${truncate(toolCall.argsPreview, MAX_ARGS_CHARS)})`
175
+ : toolCall.name;
176
+ return `Tool: ${truncate(action, MAX_LINE_CHARS)} — ${outcome(toolCall)}`;
177
+ }
178
+
179
+ function outcome(toolCall: ToolCall): string {
180
+ switch (toolCall.status) {
181
+ case ToolCallStatus.TOOL_CALL_COMPLETED:
182
+ return "completed";
183
+ case ToolCallStatus.TOOL_CALL_FAILED:
184
+ return toolCall.error
185
+ ? `failed: ${truncate(toolCall.error, MAX_ARGS_CHARS)}`
186
+ : "failed";
187
+ case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
188
+ return "paused for user approval";
189
+ case ToolCallStatus.TOOL_CALL_SKIPPED:
190
+ return "skipped";
191
+ default:
192
+ // PENDING / RUNNING / unknown: in flight when the session was lost.
193
+ return "interrupted before it finished";
194
+ }
195
+ }
196
+
197
+ function truncate(text: string, max: number): string {
198
+ return text.length <= max ? text : `${text.slice(0, max)}${ELLIPSIS}`;
199
+ }
200
+
201
+ /** Joined length of lines[first..] plus the omission notice when one would render. */
202
+ function totalLength(lines: readonly string[], first: number): number {
203
+ let total = first > 0 ? OMISSION_NOTICE.length + 1 : 0;
204
+ for (let i = first; i < lines.length; i++) {
205
+ total += lines[i].length + 1;
206
+ }
207
+ return total;
208
+ }
@@ -387,8 +387,11 @@ export async function consumeCursorTurnStream(
387
387
  console.log(
388
388
  `ExecuteCursor stream status: execution=${executionId}, status=${JSON.stringify(event)}`,
389
389
  );
390
- const statusEvent = event as { status?: string; message?: string };
391
- if (statusEvent.status === "ERROR" && statusEvent.message) {
390
+ const statusEvent = event as { status?: string; message?: unknown };
391
+ // The cast is a claim, not a guarantee: message is untyped at runtime
392
+ // (the oss#299 class of bug). A structured value here would crash
393
+ // classification downstream, so only capture actual text.
394
+ if (statusEvent.status === "ERROR" && typeof statusEvent.message === "string" && statusEvent.message.length > 0) {
392
395
  state.streamErrorMessage = statusEvent.message;
393
396
  }
394
397
  }
@@ -600,24 +600,97 @@ describe("injectAttachments", () => {
600
600
  expect(Buffer.compare(writtenBuffer, binaryContent)).toBe(0);
601
601
  });
602
602
 
603
- it("detects mount path collision before any downloads", async () => {
603
+ it("uniquifies duplicate default-derived filenames instead of failing (issue #364)", async () => {
604
+ const contentA = Buffer.from("first");
605
+ const contentB = Buffer.from("second");
606
+ const storage = makeMockStorage();
607
+ await storage.upload("attachments/a/data.csv", contentA);
608
+ await storage.upload("attachments/b/data.csv", contentB);
609
+
610
+ const backend = mockWorkspaceBackend();
611
+
612
+ const result = await injectAttachments({
613
+ backend,
614
+ attachments: [
615
+ makeAttachment({ filename: "data.csv", storageKey: "attachments/a/data.csv" }),
616
+ makeAttachment({ filename: "data.csv", storageKey: "attachments/b/data.csv" }),
617
+ ],
618
+ storage,
619
+ isLocalMode: false,
620
+ });
621
+
622
+ // Both files land, both downloads run — the execution is never burned
623
+ // over a mechanically resolvable name.
624
+ expect(result).toHaveLength(2);
625
+ expect(result[0]).toMatchObject({
626
+ filename: "data.csv",
627
+ path: ".stigmer/inputs/data.csv",
628
+ });
629
+ expect(result[0].renamedFrom).toBeUndefined();
630
+ // The rename follows the platform's stem-2.ext semantics and carries the
631
+ // disclosure payload for the prompt.
632
+ expect(result[1]).toMatchObject({
633
+ filename: "data-2.csv",
634
+ path: ".stigmer/inputs/data-2.csv",
635
+ renamedFrom: "data.csv",
636
+ });
637
+ expect(backend.writeFileBuffer).toHaveBeenCalledWith(
638
+ ".stigmer/inputs/data.csv", contentA,
639
+ );
640
+ expect(backend.writeFileBuffer).toHaveBeenCalledWith(
641
+ ".stigmer/inputs/data-2.csv", contentB,
642
+ );
643
+ });
644
+
645
+ it("rejects two attachments explicitly pinning the same mountPath (a user contradiction)", async () => {
604
646
  const backend = mockWorkspaceBackend();
605
647
  const storage = makeMockStorage();
606
648
 
607
649
  await expect(injectAttachments({
608
650
  backend,
609
651
  attachments: [
610
- makeAttachment({ filename: "data.csv", storageKey: "key1" }),
611
- makeAttachment({ filename: "data.csv", storageKey: "key2" }),
652
+ makeAttachment({ filename: "a.csv", storageKey: "key1", mountPath: "inputs/data.csv" }),
653
+ makeAttachment({ filename: "b.csv", storageKey: "key2", mountPath: "inputs/data.csv" }),
612
654
  ],
613
655
  storage,
614
656
  isLocalMode: false,
615
657
  })).rejects.toThrow(/collides with/);
616
658
 
617
- // Verify no downloads were attempted
659
+ // The contradiction is detected before any downloads
618
660
  expect(storage.download).not.toHaveBeenCalled();
619
661
  });
620
662
 
663
+ it("default-derived names dodge an explicit mountPath already inside the inputs prefix", async () => {
664
+ const storage = makeMockStorage();
665
+ await storage.upload("attachments/a/pinned", Buffer.from("pinned"));
666
+ await storage.upload("attachments/b/report.pdf", Buffer.from("derived"));
667
+
668
+ const backend = mockWorkspaceBackend();
669
+
670
+ const result = await injectAttachments({
671
+ backend,
672
+ attachments: [
673
+ // Listed AFTER the default-derived attachment: explicit paths claim
674
+ // first regardless of order, so the default still dodges.
675
+ makeAttachment({ filename: "report.pdf", storageKey: "attachments/b/report.pdf" }),
676
+ makeAttachment({
677
+ filename: "report.pdf",
678
+ storageKey: "attachments/a/pinned",
679
+ mountPath: ".stigmer/inputs/report.pdf",
680
+ }),
681
+ ],
682
+ storage,
683
+ isLocalMode: false,
684
+ });
685
+
686
+ const byPath = new Map(result.map((f) => [f.path, f]));
687
+ expect(byPath.get(".stigmer/inputs/report.pdf")?.renamedFrom).toBeUndefined();
688
+ expect(byPath.get(".stigmer/inputs/report-2.pdf")).toMatchObject({
689
+ filename: "report-2.pdf",
690
+ renamedFrom: "report.pdf",
691
+ });
692
+ });
693
+
621
694
  it("derives filename from storageKey when filename is empty", async () => {
622
695
  const content = Buffer.from("fallback");
623
696
  const localFile = join(tempDir, "fallback.txt");
@@ -662,7 +735,7 @@ describe("injectAttachments", () => {
662
735
  expect(result[0].path).toBe("workspace/input.csv");
663
736
  });
664
737
 
665
- it("error message includes attachment filename and actionable suggestion", async () => {
738
+ it("explicit-collision error names both attachments and stays actionable", async () => {
666
739
  const backend = mockWorkspaceBackend();
667
740
  const storage = makeMockStorage();
668
741
 
@@ -670,8 +743,8 @@ describe("injectAttachments", () => {
670
743
  await injectAttachments({
671
744
  backend,
672
745
  attachments: [
673
- makeAttachment({ filename: "first.csv" }),
674
- makeAttachment({ filename: "first.csv", storageKey: "key2" }),
746
+ makeAttachment({ filename: "first.csv", mountPath: "inputs/shared.csv" }),
747
+ makeAttachment({ filename: "second.csv", storageKey: "key2", mountPath: "inputs/shared.csv" }),
675
748
  ],
676
749
  storage,
677
750
  isLocalMode: false,
@@ -680,7 +753,7 @@ describe("injectAttachments", () => {
680
753
  } catch (err) {
681
754
  expect(err).toBeInstanceOf(AttachmentInjectionError);
682
755
  const injErr = err as AttachmentInjectionError;
683
- expect(injErr.attachmentFilename).toBe("first.csv");
756
+ expect(injErr.attachmentFilename).toBe("second.csv");
684
757
  expect(injErr.message).toContain("first.csv");
685
758
  expect(injErr.message).toContain("mountPath");
686
759
  }
@@ -860,6 +933,35 @@ describe("injectAttachments — vision selection", () => {
860
933
  expect(injected.visionDegraded).toBe("type_mismatch");
861
934
  });
862
935
 
936
+ it("labels a duplicate-renamed image's vision payload with the on-disk name", async () => {
937
+ // Coherence pin: the prompt lists the renamed path and the vision
938
+ // disclosure lists image filenames — if the offer used the original
939
+ // attachment.filename, the agent would see two images with one
940
+ // indistinguishable name.
941
+ const backend = mockWorkspaceBackend();
942
+ const storage = makeMockStorage();
943
+ await storage.upload("attachments/a/photo.png", PNG_BYTES);
944
+ await storage.upload("attachments/b/photo.png", PNG_BYTES);
945
+
946
+ const injected = await injectAttachments({
947
+ backend,
948
+ attachments: [
949
+ makeAttachment({ filename: "photo.png", storageKey: "attachments/a/photo.png", contentType: "image/png" }),
950
+ makeAttachment({ filename: "photo.png", storageKey: "attachments/b/photo.png", contentType: "image/png" }),
951
+ ],
952
+ storage,
953
+ isLocalMode: false,
954
+ visionBudget: new VisionBudget(DEEP_AGENT_VISION_PROFILE),
955
+ });
956
+
957
+ expect(injected[0].vision?.filename).toBe("photo.png");
958
+ expect(injected[1]).toMatchObject({
959
+ filename: "photo-2.png",
960
+ renamedFrom: "photo.png",
961
+ });
962
+ expect(injected[1].vision?.filename).toBe("photo-2.png");
963
+ });
964
+
863
965
  it("never offers an extract archive to the budget — extracted files carry no vision fields", async () => {
864
966
  // A PNG inside a ZIP has no attachment-level bytes; the archive rides the
865
967
  // normal extraction story with no vision involvement or disclosure.
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Datastore degradation disclosure, activity half (issue #325).
3
+ *
4
+ * The activity appends the operator-facing MESSAGE_SYSTEM notice to
5
+ * `initialStatus.messages` BEFORE streaming (index.ts) — deliberately not
6
+ * through a status builder, because the v2 and v3 stream paths each
7
+ * construct their own builder around that same proto. These tests pin the
8
+ * property that push site depends on: BOTH builders preserve pre-seeded
9
+ * messages (ExecutionState only appends; index rebuilds touch runtime
10
+ * tool-call state, and a MESSAGE_SYSTEM row carries no tool calls), so
11
+ * the notice survives whichever stream version runs and rides every
12
+ * persist. The prompt half (section selection) is pinned in
13
+ * shared/__tests__/datastore-attachment.test.ts.
14
+ */
15
+
16
+ import { describe, it, expect, beforeEach } from "vitest";
17
+ import { create } from "@bufbuild/protobuf";
18
+ import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
19
+ import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
20
+ import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
21
+ import { MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
22
+ import {
23
+ EXPECTED_RECORD_TOOLS,
24
+ formatDatastoreDegradationNotice,
25
+ } from "../../../shared/datastore-attachment.js";
26
+ import { StatusBuilder } from "../status-builder.js";
27
+ import { V3StatusBuilder } from "../v3-status-builder.js";
28
+ import { normalize } from "../v3-protocol-normalizer.js";
29
+ import {
30
+ resetSeq,
31
+ makeMessageStart,
32
+ makeMessageFinish,
33
+ makeTextDelta,
34
+ } from "../__test-utils__/v3-event-fixtures.js";
35
+
36
+ beforeEach(() => resetSeq());
37
+
38
+ /** initialStatus as index.ts shapes it when setup reports missing tools. */
39
+ function statusWithDegradationNotice(): AgentExecutionStatus {
40
+ const status = create(AgentExecutionStatusSchema, {});
41
+ status.messages.push(create(AgentMessageSchema, {
42
+ type: MessageType.MESSAGE_SYSTEM,
43
+ content: formatDatastoreDegradationNotice(1, [...EXPECTED_RECORD_TOOLS]),
44
+ timestamp: "2026-08-11T00:00:00.000Z",
45
+ }));
46
+ return status;
47
+ }
48
+
49
+ describe("datastore degradation notice survives streaming (issue #325)", () => {
50
+ it("v2 StatusBuilder preserves the pre-seeded notice through a streamed turn", () => {
51
+ const initialStatus = statusWithDegradationNotice();
52
+ const sb = new StatusBuilder("exec-1", initialStatus);
53
+
54
+ sb.processEvent({
55
+ event: "on_chat_model_stream",
56
+ run_id: "run-1",
57
+ data: { chunk: { content: "I cannot reach the datastore right now." } },
58
+ });
59
+ sb.processEvent({ event: "on_chat_model_end", run_id: "run-1", data: {} });
60
+
61
+ const messages = sb.currentStatus.messages;
62
+ expect(messages.length).toBeGreaterThanOrEqual(2);
63
+ expect(messages[0].type).toBe(MessageType.MESSAGE_SYSTEM);
64
+ expect(messages[0].content).toContain("0/5 record tools connected");
65
+ expect(messages[0].content).toContain("declared 1 datastore(s)");
66
+ });
67
+
68
+ it("v3 V3StatusBuilder preserves the pre-seeded notice through a streamed turn", () => {
69
+ const initialStatus = statusWithDegradationNotice();
70
+ const sb = new V3StatusBuilder("exec-1", initialStatus);
71
+
72
+ const protocolEvents = [
73
+ makeMessageStart("run-1"),
74
+ makeTextDelta("run-1", "I cannot reach the datastore right now."),
75
+ makeMessageFinish("run-1"),
76
+ ];
77
+ for (const raw of protocolEvents) {
78
+ for (const e of normalize(raw)) {
79
+ sb.processEvent(e);
80
+ }
81
+ }
82
+
83
+ const messages = sb.currentStatus.messages;
84
+ expect(messages.length).toBeGreaterThanOrEqual(2);
85
+ expect(messages[0].type).toBe(MessageType.MESSAGE_SYSTEM);
86
+ expect(messages[0].content).toContain("0/5 record tools connected");
87
+ });
88
+
89
+ it("both builders share the initialStatus proto, so the append needs no builder", () => {
90
+ // The push-site contract itself: mutating initialStatus.messages is
91
+ // visible through whichever builder wraps it.
92
+ const initialStatus = create(AgentExecutionStatusSchema, {});
93
+ const sb = new StatusBuilder("exec-1", initialStatus);
94
+ initialStatus.messages.push(create(AgentMessageSchema, {
95
+ type: MessageType.MESSAGE_SYSTEM,
96
+ content: formatDatastoreDegradationNotice(2, ["delete_record"]),
97
+ timestamp: "2026-08-11T00:00:00.000Z",
98
+ }));
99
+ expect(sb.currentStatus.messages).toHaveLength(1);
100
+ expect(sb.currentStatus.messages[0].content).toContain(
101
+ "4/5 record tools connected",
102
+ );
103
+ });
104
+ });
@@ -184,6 +184,7 @@ function fakeHttpRejectSetup(): SetupResult {
184
184
  session: { spec: { workspaceEntries: [] } },
185
185
  workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-http" },
186
186
  mcpConnection: null,
187
+ datastoreToolsMissing: [],
187
188
  mergedEnvVars: {},
188
189
  secretKeys: new Set<string>(),
189
190
  modelName: "claude-sonnet",
@@ -219,6 +220,7 @@ function fakeMemoryRejectSetup(): SetupResult {
219
220
  session: { spec: { workspaceEntries: [] } },
220
221
  workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-reject-mem" },
221
222
  mcpConnection: null,
223
+ datastoreToolsMissing: [],
222
224
  mergedEnvVars: {},
223
225
  secretKeys: new Set<string>(),
224
226
  modelName: "claude-sonnet",
@@ -264,6 +264,7 @@ function fakeDurableResumeSetup(): SetupResult {
264
264
  // path shared across files would serialize parallel test workers for real.
265
265
  workspaceBackend: { rootDir: "/tmp/stigmer-test-ws-hitl-approve-all" },
266
266
  mcpConnection: null,
267
+ datastoreToolsMissing: [],
267
268
  mergedEnvVars: {},
268
269
  secretKeys: new Set<string>(),
269
270
  modelName: "claude-sonnet",