@vellumai/assistant 0.8.3 → 0.8.4

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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Verifies that every resolution path through
3
+ * `runtime/pending-interactions.ts` publishes an `interaction_resolved`
4
+ * envelope on the event hub with the right state.
5
+ *
6
+ * Each test registers an interaction directly and calls `resolve()` or
7
+ * `removeByConversation()` so we exercise the tracker in isolation
8
+ * without spinning up a Conversation, prompter, or proxy.
9
+ */
10
+
11
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import type { ServerMessage } from "../daemon/message-protocol.js";
14
+
15
+ // Capture every broadcast emitted by the tracker. The real hub is replaced
16
+ // with a thin recorder so we can assert payloads deterministically.
17
+ const publishedMessages: ServerMessage[] = [];
18
+
19
+ mock.module("../runtime/assistant-event-hub.js", () => ({
20
+ broadcastMessage: (msg: ServerMessage) => {
21
+ publishedMessages.push(msg);
22
+ },
23
+ capabilityForMessageType: () => undefined,
24
+ assistantEventHub: {
25
+ publish: async () => {},
26
+ subscribe: () => ({ dispose: () => {}, active: true }),
27
+ },
28
+ }));
29
+
30
+ const pendingInteractions = await import("../runtime/pending-interactions.js");
31
+
32
+ beforeEach(() => {
33
+ publishedMessages.length = 0;
34
+ pendingInteractions.clear();
35
+ });
36
+
37
+ afterEach(() => {
38
+ pendingInteractions.clear();
39
+ });
40
+
41
+ function lastResolvedEvent() {
42
+ const evt = publishedMessages.find((m) => m.type === "interaction_resolved");
43
+ expect(evt).toBeDefined();
44
+ return evt as Extract<ServerMessage, { type: "interaction_resolved" }>;
45
+ }
46
+
47
+ describe("pendingInteractions.resolve emits interaction_resolved", () => {
48
+ test("default state is 'cancelled'", () => {
49
+ pendingInteractions.register("req-1", {
50
+ conversationId: "conv-1",
51
+ kind: "confirmation",
52
+ });
53
+ const returned = pendingInteractions.resolve("req-1");
54
+ expect(returned).toBeDefined();
55
+ const evt = lastResolvedEvent();
56
+ expect(evt.requestId).toBe("req-1");
57
+ expect(evt.conversationKey).toBe("conv-1");
58
+ expect(evt.conversationId).toBe("conv-1");
59
+ expect(evt.state).toBe("cancelled");
60
+ expect(evt.kind).toBe("confirmation");
61
+ });
62
+
63
+ test("approved state propagates", () => {
64
+ pendingInteractions.register("req-approve", {
65
+ conversationId: "conv-a",
66
+ kind: "confirmation",
67
+ });
68
+ pendingInteractions.resolve("req-approve", "approved");
69
+ expect(lastResolvedEvent().state).toBe("approved");
70
+ });
71
+
72
+ test("rejected state propagates", () => {
73
+ pendingInteractions.register("req-reject", {
74
+ conversationId: "conv-b",
75
+ kind: "confirmation",
76
+ });
77
+ pendingInteractions.resolve("req-reject", "rejected");
78
+ expect(lastResolvedEvent().state).toBe("rejected");
79
+ });
80
+
81
+ test("answered state propagates (secret response)", () => {
82
+ pendingInteractions.register("req-secret", {
83
+ conversationId: "conv-c",
84
+ kind: "secret",
85
+ });
86
+ pendingInteractions.resolve("req-secret", "answered");
87
+ const evt = lastResolvedEvent();
88
+ expect(evt.state).toBe("answered");
89
+ expect(evt.kind).toBe("secret");
90
+ });
91
+
92
+ test("superseded state propagates", () => {
93
+ pendingInteractions.register("req-super", {
94
+ conversationId: "conv-d",
95
+ kind: "confirmation",
96
+ });
97
+ pendingInteractions.resolve("req-super", "superseded");
98
+ expect(lastResolvedEvent().state).toBe("superseded");
99
+ });
100
+
101
+ test("no event is emitted when the requestId is unknown", () => {
102
+ pendingInteractions.resolve("never-registered", "approved");
103
+ expect(publishedMessages).toHaveLength(0);
104
+ });
105
+
106
+ test("a single resolve emits exactly one event", () => {
107
+ pendingInteractions.register("req-once", {
108
+ conversationId: "conv-e",
109
+ kind: "host_bash",
110
+ });
111
+ pendingInteractions.resolve("req-once", "answered");
112
+ // Second resolve is a no-op because the entry was already consumed.
113
+ pendingInteractions.resolve("req-once", "answered");
114
+ const events = publishedMessages.filter(
115
+ (m) => m.type === "interaction_resolved",
116
+ );
117
+ expect(events).toHaveLength(1);
118
+ });
119
+
120
+ test("clears the registered timer on resolve", () => {
121
+ let fired = false;
122
+ const timer = setTimeout(() => {
123
+ fired = true;
124
+ }, 10_000);
125
+ pendingInteractions.register("req-timer", {
126
+ conversationId: "conv-f",
127
+ kind: "confirmation",
128
+ timer,
129
+ });
130
+ pendingInteractions.resolve("req-timer", "approved");
131
+ clearTimeout(timer);
132
+ expect(fired).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe("removeByConversation emits interaction_resolved per entry", () => {
137
+ test("emits superseded for every non-host interaction in the conversation", () => {
138
+ pendingInteractions.register("conf-1", {
139
+ conversationId: "conv-x",
140
+ kind: "confirmation",
141
+ });
142
+ pendingInteractions.register("secret-1", {
143
+ conversationId: "conv-x",
144
+ kind: "secret",
145
+ });
146
+ pendingInteractions.register("question-1", {
147
+ conversationId: "conv-x",
148
+ kind: "question",
149
+ });
150
+ pendingInteractions.register("host-bash-1", {
151
+ conversationId: "conv-x",
152
+ kind: "host_bash",
153
+ });
154
+ pendingInteractions.register("conf-other", {
155
+ conversationId: "conv-y",
156
+ kind: "confirmation",
157
+ });
158
+
159
+ pendingInteractions.removeByConversation("conv-x");
160
+
161
+ const events = publishedMessages.filter(
162
+ (m) => m.type === "interaction_resolved",
163
+ ) as Extract<ServerMessage, { type: "interaction_resolved" }>[];
164
+ expect(events).toHaveLength(3);
165
+ expect(events.every((e) => e.state === "superseded")).toBe(true);
166
+ const requestIds = new Set(events.map((e) => e.requestId));
167
+ expect(requestIds).toEqual(new Set(["conf-1", "secret-1", "question-1"]));
168
+
169
+ // host_bash entries survive auto-deny — no event for them.
170
+ expect(pendingInteractions.get("host-bash-1")).toBeDefined();
171
+ // Unrelated conversation is untouched.
172
+ expect(pendingInteractions.get("conf-other")).toBeDefined();
173
+ });
174
+
175
+ test("explicit state arg overrides the default 'superseded'", () => {
176
+ pendingInteractions.register("conf-2", {
177
+ conversationId: "conv-z",
178
+ kind: "confirmation",
179
+ });
180
+ pendingInteractions.removeByConversation("conv-z", "cancelled");
181
+ const events = publishedMessages.filter(
182
+ (m) => m.type === "interaction_resolved",
183
+ );
184
+ expect(events).toHaveLength(1);
185
+ expect(
186
+ (events[0] as Extract<ServerMessage, { type: "interaction_resolved" }>)
187
+ .state,
188
+ ).toBe("cancelled");
189
+ });
190
+ });
@@ -8,7 +8,6 @@ import {
8
8
  getDataDir,
9
9
  getDbPath,
10
10
  getHistoryPath,
11
- getInterfacesDir,
12
11
  getLogPath,
13
12
  getPidPath,
14
13
  getSandboxRootDir,
@@ -67,7 +66,6 @@ describe("path characterization", () => {
67
66
  expect(getDbPath()).toBe(join(data, "db", "assistant.db"));
68
67
  expect(getLogPath()).toBe(join(data, "logs", "vellum.log"));
69
68
  expect(getHistoryPath()).toBe(join(data, "history"));
70
- expect(getInterfacesDir()).toBe(join(data, "interfaces"));
71
69
  expect(getSandboxRootDir()).toBe(join(data, "sandbox"));
72
70
  expect(getSandboxWorkingDir()).toBe(ws);
73
71
 
@@ -117,7 +115,6 @@ describe("path characterization", () => {
117
115
  expect(existsSync(join(data, "memory"))).toBe(true);
118
116
  expect(existsSync(join(data, "memory", "knowledge"))).toBe(true);
119
117
  expect(existsSync(join(data, "apps"))).toBe(true);
120
- expect(existsSync(join(data, "interfaces"))).toBe(true);
121
118
 
122
119
  rmSync(wsDir, { recursive: true, force: true });
123
120
  });
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Tests for PluginSourceWatcher — filesystem watcher that detects plugin
3
+ * directory changes and triggers debounced reregistration.
4
+ *
5
+ * Key regression: the watcher must survive (and recover from) the Linux/Bun
6
+ * recursive-watch limitation where subdirectories created after watch starts
7
+ * are silently dropped. We test that close→reopen + rescan catches these.
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Mocks — must be set up before importing the module under test
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const TEST_PLUGINS_DIR = "/tmp/test-plugins";
17
+
18
+ let capturedWatchCallback:
19
+ | ((eventType: string, filename: string | null) => void)
20
+ | null = null;
21
+ let mockWatchShouldThrow = false;
22
+ const mockWatcher = { close: mock(() => {}) };
23
+
24
+ const mockWatch = mock(
25
+ (
26
+ _path: string,
27
+ _opts: Record<string, unknown>,
28
+ callback: (eventType: string, filename: string | null) => void,
29
+ ) => {
30
+ if (mockWatchShouldThrow) throw new Error("watch failed");
31
+ capturedWatchCallback = callback;
32
+ return mockWatcher;
33
+ },
34
+ );
35
+
36
+ const mockRereadirSync = mock((_path: string): string[] => []);
37
+
38
+ let mockGetRegisteredPluginImpl: (name: string) => unknown | undefined = (
39
+ _name,
40
+ ) => undefined;
41
+ const mockGetRegisteredPlugin = mock((name: string) =>
42
+ mockGetRegisteredPluginImpl(name),
43
+ );
44
+
45
+ let mockReregisterExternalPluginImpl = mock(async (_name: string) => {});
46
+ const mockReregisterExternalPlugin = mock(async (name: string) =>
47
+ mockReregisterExternalPluginImpl(name),
48
+ );
49
+
50
+ mock.module("node:fs", () => {
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const actualFs = require("node:fs");
53
+ return {
54
+ ...actualFs,
55
+ watch: mockWatch,
56
+ readdirSync: mockRereadirSync,
57
+ };
58
+ });
59
+
60
+ mock.module("../plugins/registry.js", () => ({
61
+ getRegisteredPlugin: mockGetRegisteredPlugin,
62
+ }));
63
+
64
+ mock.module("../util/platform.js", () => ({
65
+ getWorkspacePluginsDir: mock(() => TEST_PLUGINS_DIR),
66
+ }));
67
+
68
+ mock.module("../daemon/external-plugins-bootstrap.js", () => ({
69
+ reregisterExternalPlugin: mockReregisterExternalPlugin,
70
+ }));
71
+
72
+ mock.module("../util/logger.js", () => ({
73
+ getLogger: mock(() => ({
74
+ info: mock(() => {}),
75
+ warn: mock(() => {}),
76
+ })),
77
+ }));
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Import after mocks
81
+ // ---------------------------------------------------------------------------
82
+
83
+ import { PluginSourceWatcher } from "../daemon/plugin-source-watcher.js";
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Tests
87
+ // ---------------------------------------------------------------------------
88
+
89
+ describe("PluginSourceWatcher", () => {
90
+ beforeEach(() => {
91
+ PluginSourceWatcher.resetForTests();
92
+ capturedWatchCallback = null;
93
+ mockWatchShouldThrow = false;
94
+ mockWatcher.close.mockClear();
95
+ mockWatch.mockClear();
96
+ mockRereadirSync.mockClear();
97
+ mockGetRegisteredPlugin.mockClear();
98
+ mockReregisterExternalPlugin.mockClear();
99
+ mockGetRegisteredPluginImpl = (_name: string) => undefined;
100
+ mockReregisterExternalPluginImpl = mock(async (_name: string) => {});
101
+ });
102
+
103
+ afterEach(() => {
104
+ const watcher = PluginSourceWatcher.getInstance();
105
+ watcher.stop();
106
+ });
107
+
108
+ test("start() creates a recursive watcher on the plugins directory", () => {
109
+ const watcher = PluginSourceWatcher.getInstance();
110
+ watcher.start();
111
+ expect(capturedWatchCallback).not.toBeNull();
112
+ });
113
+
114
+ test("plugin directory creation triggers rebuild", async () => {
115
+ const watcher = PluginSourceWatcher.getInstance();
116
+ watcher.start();
117
+
118
+ capturedWatchCallback!("change", "my-plugin");
119
+
120
+ // Wait for debounce
121
+ await new Promise((r) => setTimeout(r, 600));
122
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
123
+ });
124
+
125
+ test("nested file change within plugin triggers rebuild", async () => {
126
+ const watcher = PluginSourceWatcher.getInstance();
127
+ watcher.start();
128
+
129
+ capturedWatchCallback!("change", "my-plugin/src/index.ts");
130
+
131
+ await new Promise((r) => setTimeout(r, 600));
132
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
133
+ });
134
+
135
+ test("deeply nested file change triggers rebuild", async () => {
136
+ const watcher = PluginSourceWatcher.getInstance();
137
+ watcher.start();
138
+
139
+ capturedWatchCallback!("change", "my-plugin/src/handlers/util/helper.ts");
140
+
141
+ await new Promise((r) => setTimeout(r, 600));
142
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
143
+ });
144
+
145
+ test("dotfiles in plugins root are ignored", async () => {
146
+ const watcher = PluginSourceWatcher.getInstance();
147
+ watcher.start();
148
+
149
+ capturedWatchCallback!("change", ".DS_Store");
150
+
151
+ await new Promise((r) => setTimeout(r, 600));
152
+ expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
153
+ });
154
+
155
+ test("null filename is ignored", async () => {
156
+ const watcher = PluginSourceWatcher.getInstance();
157
+ watcher.start();
158
+
159
+ capturedWatchCallback!("change", null);
160
+
161
+ await new Promise((r) => setTimeout(r, 600));
162
+ expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
163
+ });
164
+
165
+ test("rapid changes to same plugin are debounced into single rebuild", async () => {
166
+ const watcher = PluginSourceWatcher.getInstance();
167
+ watcher.start();
168
+
169
+ capturedWatchCallback!("change", "my-plugin/src/index.ts");
170
+ capturedWatchCallback!("change", "my-plugin/src/handlers.ts");
171
+ capturedWatchCallback!("change", "my-plugin/package.json");
172
+
173
+ await new Promise((r) => setTimeout(r, 600));
174
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledTimes(1);
175
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
176
+ });
177
+
178
+ test("changes to different plugins trigger separate rebuilds (debounced)", async () => {
179
+ const watcher = PluginSourceWatcher.getInstance();
180
+ watcher.start();
181
+
182
+ capturedWatchCallback!("change", "plugin-a/src/index.ts");
183
+ capturedWatchCallback!("change", "plugin-b/src/index.ts");
184
+
185
+ await new Promise((r) => setTimeout(r, 600));
186
+ expect(mockReregisterExternalPlugin).toHaveBeenCalledTimes(2);
187
+ expect(mockReregisterExternalPlugin).toHaveBeenNthCalledWith(1, "plugin-a");
188
+ expect(mockReregisterExternalPlugin).toHaveBeenNthCalledWith(2, "plugin-b");
189
+ });
190
+
191
+ test("stop() closes watcher and cancels pending rebuilds", () => {
192
+ const watcher = PluginSourceWatcher.getInstance();
193
+ watcher.start();
194
+
195
+ capturedWatchCallback!("change", "my-plugin/src/index.ts");
196
+ watcher.stop();
197
+
198
+ expect(mockWatcher.close).toHaveBeenCalledTimes(1);
199
+ // No rebuild should fire after stop
200
+ expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
201
+ });
202
+
203
+ test("ensureStarted() initializes watcher after start() if watch coverage was lost", () => {
204
+ const watcher = PluginSourceWatcher.getInstance();
205
+ watcher.start();
206
+ expect(capturedWatchCallback).not.toBeNull();
207
+
208
+ // Simulate lost coverage while the daemon lifecycle is still started
209
+ // (e.g. a previous watch attempt failed after startup).
210
+ (watcher as unknown as { watcher: unknown }).watcher = null;
211
+ capturedWatchCallback = null;
212
+
213
+ watcher.ensureStarted();
214
+ expect(capturedWatchCallback).not.toBeNull();
215
+ });
216
+
217
+ test("ensureStarted() is a no-op when watcher is already running", () => {
218
+ const watcher = PluginSourceWatcher.getInstance();
219
+ watcher.start();
220
+ const callCountAfterStart = mockWatch.mock.calls.length;
221
+
222
+ watcher.ensureStarted();
223
+ expect(mockWatch.mock.calls.length).toBe(callCountAfterStart);
224
+ });
225
+
226
+ test("watcher restart keeps the previous watcher active if replacement fails", async () => {
227
+ const watcher = PluginSourceWatcher.getInstance();
228
+ watcher.start();
229
+ const firstCallback = capturedWatchCallback;
230
+
231
+ mockWatchShouldThrow = true;
232
+ capturedWatchCallback!("change", "my-plugin/src/index.ts");
233
+
234
+ await new Promise((r) => setTimeout(r, 600));
235
+
236
+ expect(mockWatcher.close).not.toHaveBeenCalled();
237
+ expect((watcher as unknown as { watcher: unknown }).watcher).toBe(
238
+ mockWatcher,
239
+ );
240
+ expect(capturedWatchCallback).toBe(firstCallback);
241
+ });
242
+
243
+ test("singleton instance is shared across calls", () => {
244
+ const watcher1 = PluginSourceWatcher.getInstance();
245
+ const watcher2 = PluginSourceWatcher.getInstance();
246
+ expect(watcher1).toBe(watcher2);
247
+ });
248
+
249
+ test("resetForTests() clears the singleton", () => {
250
+ const watcher1 = PluginSourceWatcher.getInstance();
251
+ watcher1.start();
252
+
253
+ PluginSourceWatcher.resetForTests();
254
+ const watcher2 = PluginSourceWatcher.getInstance();
255
+
256
+ expect(watcher1).not.toBe(watcher2);
257
+ });
258
+
259
+ /**
260
+ * REGRESSION: When an event arrives during a close→reopen swap, rescan
261
+ * must catch any plugin not yet in the registry.
262
+ *
263
+ * Scenario:
264
+ * 1. Plugin "new-plugin" directory is created
265
+ * 2. Event fires, triggering a watcher restart (close + reopen)
266
+ * 3. Before the old watcher's callback is fully fired, another plugin
267
+ * "late-plugin" is created
268
+ * 4. The new watcher doesn't yet know about "late-plugin"
269
+ * 5. After the reopen, rescan walks the directory and finds "late-plugin"
270
+ * not in the registry, and schedules its rebuild
271
+ */
272
+ test("watcher restart + rescan catches plugins created during close→reopen", async () => {
273
+ const watcher = PluginSourceWatcher.getInstance();
274
+ watcher.start();
275
+
276
+ // Track which plugins are "registered" at each point
277
+ const registeredPlugins = new Set<string>();
278
+ mockGetRegisteredPluginImpl = (name: string) =>
279
+ registeredPlugins.has(name) ? { name } : undefined;
280
+
281
+ // Simulate multiple plugins on disk
282
+ mockRereadirSync.mockImplementation(() => [
283
+ "new-plugin",
284
+ "late-plugin",
285
+ ".DS_Store",
286
+ ]);
287
+
288
+ // Fire an event on new-plugin (this will trigger a watcher restart)
289
+ capturedWatchCallback!("change", "new-plugin/src/index.ts");
290
+
291
+ // Wait for the direct rebuild debounce, the watcher-restart debounce,
292
+ // and the rescan-triggered rebuild debounce.
293
+ await new Promise((r) => setTimeout(r, 1100));
294
+
295
+ // At this point, rescan should have run and discovered late-plugin,
296
+ // even though no direct fs.watch event was delivered for it.
297
+ const calls = mockReregisterExternalPlugin.mock.calls.map((c) => c[0]);
298
+ expect(calls).toContain("new-plugin");
299
+ expect(calls).toContain("late-plugin");
300
+ expect(calls).not.toContain(".DS_Store");
301
+ });
302
+ });
@@ -95,7 +95,6 @@ type PersistUserMessageMock = ReturnType<
95
95
  type RunAgentLoopMock = ReturnType<
96
96
  typeof mock<(...args: unknown[]) => Promise<void>>
97
97
  >;
98
- type NoticeMock = ReturnType<typeof mock<(notice: string | undefined) => void>>;
99
98
  interface TestConversation {
100
99
  conversationId: string;
101
100
  trustContext: unknown;
@@ -123,13 +122,10 @@ interface TestConversation {
123
122
  estimatedCost: number;
124
123
  };
125
124
  persistUserMessage: PersistUserMessageMock;
126
- setSlackRuntimeContextNotice: NoticeMock;
127
125
  runAgentLoop: RunAgentLoopMock;
128
126
  updateClient: (sender: (...args: unknown[]) => void) => void;
129
127
  getCurrentSender: () => ((...args: unknown[]) => void) | undefined;
130
128
  __loopDeferred: Deferred<void>;
131
- __noticeCalls: Array<string | undefined>;
132
- __loopNotices: Array<string | undefined>;
133
129
  __clientSenders: Array<((...args: unknown[]) => void) | undefined>;
134
130
  }
135
131
 
@@ -171,11 +167,8 @@ async function waitForRunAgentLoopCall(): Promise<void> {
171
167
  function makeConversation(): TestConversation {
172
168
  let turnChannelContext: TurnChannelContext | null = null;
173
169
  let turnInterfaceContext: TurnInterfaceContext | null = null;
174
- let slackNotice: string | undefined;
175
170
  let currentSender: ((...args: unknown[]) => void) | undefined;
176
- const noticeCalls: Array<string | undefined> = [];
177
171
  const loopDeferred = createDeferred<void>();
178
- const loopNotices: Array<string | undefined> = [];
179
172
  const clientSenders: Array<((...args: unknown[]) => void) | undefined> = [];
180
173
  const messages: unknown[] = [];
181
174
 
@@ -223,12 +216,7 @@ function makeConversation(): TestConversation {
223
216
  _metadata?: Record<string, unknown>,
224
217
  ) => "persisted-user-message-id",
225
218
  ),
226
- setSlackRuntimeContextNotice: mock((notice: string | undefined) => {
227
- slackNotice = notice;
228
- noticeCalls.push(notice);
229
- }),
230
219
  runAgentLoop: mock(async (..._args: unknown[]) => {
231
- loopNotices.push(slackNotice);
232
220
  await loopDeferred.promise;
233
221
  }),
234
222
  updateClient: (sender: (...args: unknown[]) => void) => {
@@ -237,8 +225,6 @@ function makeConversation(): TestConversation {
237
225
  },
238
226
  getCurrentSender: () => currentSender,
239
227
  __loopDeferred: loopDeferred,
240
- __noticeCalls: noticeCalls,
241
- __loopNotices: loopNotices,
242
228
  __clientSenders: clientSenders,
243
229
  };
244
230
 
@@ -252,15 +238,13 @@ describe("processMessageInBackground Slack option propagation", () => {
252
238
  broadcastMessages.length = 0;
253
239
  });
254
240
 
255
- test("passes Slack inbound metadata to persistence and exposes the runtime notice during the loop", async () => {
241
+ test("passes Slack inbound metadata to persistence during background processing", async () => {
256
242
  const slackInbound = {
257
243
  channelId: "C0123CHANNEL",
258
244
  channelTs: "1700000001.111111",
259
245
  threadTs: "1700000000.000001",
260
246
  displayName: "Alice",
261
247
  };
262
- const notice =
263
- "Slack context note: this turn joined an existing thread. 2 earlier messages were backfilled.";
264
248
 
265
249
  const result = await processMessageInBackground(
266
250
  "conv-background-slack",
@@ -268,7 +252,6 @@ describe("processMessageInBackground Slack option propagation", () => {
268
252
  undefined,
269
253
  {
270
254
  slackInbound,
271
- slackRuntimeContextNotice: notice,
272
255
  },
273
256
  "slack",
274
257
  "slack",
@@ -280,43 +263,10 @@ describe("processMessageInBackground Slack option propagation", () => {
280
263
  slackInbound,
281
264
  });
282
265
  expect(activeConversation.runAgentLoop).toHaveBeenCalledTimes(1);
283
- expect(activeConversation.__loopNotices).toEqual([notice]);
284
266
 
285
267
  activeConversation.__loopDeferred.resolve();
286
268
  await activeConversation.__loopDeferred.promise;
287
269
  await Promise.resolve();
288
-
289
- expect(activeConversation.__noticeCalls).toEqual([notice, undefined]);
290
- });
291
-
292
- test("clears the Slack runtime notice after normal message processing", async () => {
293
- const notice =
294
- "Slack context note: this turn joined an existing thread. 2 earlier messages were backfilled.";
295
-
296
- const processing = processMessage(
297
- "conv-background-slack",
298
- "Reply from Slack",
299
- undefined,
300
- {
301
- slackRuntimeContextNotice: notice,
302
- isInteractive: true,
303
- },
304
- "slack",
305
- "slack",
306
- );
307
-
308
- await waitForRunAgentLoopCall();
309
-
310
- expect(activeConversation.runAgentLoop).toHaveBeenCalledTimes(1);
311
- expect(activeConversation.__loopNotices).toEqual([notice]);
312
-
313
- activeConversation.__loopDeferred.resolve();
314
- await expect(processing).resolves.toEqual({
315
- messageId: "persisted-user-message-id",
316
- });
317
-
318
- expect(activeConversation.__noticeCalls).toEqual([notice, undefined]);
319
- expect(activeConversation.__clientSenders).toHaveLength(2);
320
270
  });
321
271
 
322
272
  test("observes live agent events without replacing the broadcast emitter", async () => {
@@ -196,7 +196,6 @@ function makeTestConversation() {
196
196
  metadata,
197
197
  displayContent,
198
198
  ),
199
- setSlackRuntimeContextNotice: () => {},
200
199
  runAgentLoop,
201
200
  updateClient: () => {},
202
201
  getCurrentSender: () => undefined,
@@ -361,6 +360,7 @@ describe("processMessage displayContent", () => {
361
360
  expect(persistedBlocks).toEqual([
362
361
  {
363
362
  type: "file",
363
+ _attachmentId: "att-1",
364
364
  source: {
365
365
  type: "base64",
366
366
  media_type: "application/pdf",
@@ -370,21 +370,26 @@ describe("processMessage displayContent", () => {
370
370
  },
371
371
  ]);
372
372
  expect(addMessageCalls[0]!.content).not.toContain("<external_content");
373
- expect(conversation.getMessages()[0]).toEqual({
374
- role: "user",
375
- content: [
376
- { type: "text", text: modelContent },
377
- {
378
- type: "file",
379
- source: {
380
- type: "base64",
381
- media_type: "application/pdf",
382
- data: Buffer.from("pdf bytes").toString("base64"),
383
- filename: "attachment.pdf",
384
- },
385
- extracted_text: undefined,
386
- },
387
- ],
373
+ const inMemoryMessage = conversation.getMessages()[0]!;
374
+ expect(inMemoryMessage.role).toBe("user");
375
+ expect(inMemoryMessage.content[0]).toEqual({
376
+ type: "text",
377
+ text: modelContent,
378
+ });
379
+ const inMemoryFileBlock = inMemoryMessage.content[1] as unknown as Record<
380
+ string,
381
+ unknown
382
+ >;
383
+ expect(inMemoryFileBlock._attachmentId).toBe("att-1");
384
+ expect(inMemoryFileBlock).toMatchObject({
385
+ type: "file",
386
+ source: {
387
+ type: "base64",
388
+ media_type: "application/pdf",
389
+ data: Buffer.from("pdf bytes").toString("base64"),
390
+ filename: "attachment.pdf",
391
+ },
392
+ extracted_text: undefined,
388
393
  });
389
394
  });
390
395