@vellumai/assistant 0.8.1 → 0.8.3

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 (630) hide show
  1. package/ARCHITECTURE.md +13 -19
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +17 -0
  5. package/docker-init-apt-root.sh +167 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +642 -5
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  21. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  23. package/src/__tests__/anthropic-provider.test.ts +45 -0
  24. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  25. package/src/__tests__/app-executors.test.ts +220 -4
  26. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  27. package/src/__tests__/bundled-asset.test.ts +6 -6
  28. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  29. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  30. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  31. package/src/__tests__/clawhub.test.ts +75 -16
  32. package/src/__tests__/compactor-tail-resolution.test.ts +147 -0
  33. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  34. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  35. package/src/__tests__/config-schema.test.ts +21 -0
  36. package/src/__tests__/config-set-route.test.ts +80 -0
  37. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  38. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  39. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  40. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  41. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  42. package/src/__tests__/context-token-estimator.test.ts +31 -65
  43. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  44. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  45. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  46. package/src/__tests__/conversation-agent-loop.test.ts +59 -1
  47. package/src/__tests__/conversation-error.test.ts +42 -3
  48. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  49. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  50. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  51. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  52. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  53. package/src/__tests__/conversation-pairing.test.ts +54 -0
  54. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  55. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  56. package/src/__tests__/conversation-queue.test.ts +4 -1
  57. package/src/__tests__/conversation-runtime-assembly.test.ts +102 -13
  58. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  59. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  60. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  61. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  62. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  63. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  64. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  65. package/src/__tests__/date-context.test.ts +45 -0
  66. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  67. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  68. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  69. package/src/__tests__/dm-backfill.test.ts +121 -10
  70. package/src/__tests__/document-tool-security.test.ts +258 -0
  71. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  72. package/src/__tests__/edit-propagation.test.ts +33 -0
  73. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  74. package/src/__tests__/external-plugin-loader.test.ts +151 -55
  75. package/src/__tests__/filing-service.test.ts +140 -0
  76. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  77. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  78. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  79. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  80. package/src/__tests__/heartbeat-service.test.ts +24 -164
  81. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  82. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  83. package/src/__tests__/helpers/wait-for.ts +21 -0
  84. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  85. package/src/__tests__/history-repair.test.ts +73 -0
  86. package/src/__tests__/host-app-control-proxy.test.ts +507 -10
  87. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  88. package/src/__tests__/image-credentials.test.ts +1 -1
  89. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  90. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  91. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  92. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  93. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  94. package/src/__tests__/injector-background-turn.test.ts +153 -0
  95. package/src/__tests__/injector-chain.test.ts +15 -8
  96. package/src/__tests__/install-skill-routing.test.ts +155 -37
  97. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +99 -3
  98. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  99. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  100. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  101. package/src/__tests__/llm-catalog-parity.test.ts +58 -13
  102. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  103. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  104. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +36 -0
  105. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  106. package/src/__tests__/llm-resolver.test.ts +255 -2
  107. package/src/__tests__/llm-usage-store.test.ts +114 -0
  108. package/src/__tests__/managed-profile-guard.test.ts +41 -29
  109. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  110. package/src/__tests__/managed-store.test.ts +84 -192
  111. package/src/__tests__/media-generate-image.test.ts +1 -1
  112. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  113. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  114. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  115. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  116. package/src/__tests__/notification-deep-link.test.ts +15 -0
  117. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  118. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  119. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  120. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  121. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  122. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  123. package/src/__tests__/openai-provider.test.ts +242 -3
  124. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  125. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  126. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  127. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  128. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  129. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +7 -2
  130. package/src/__tests__/platform.test.ts +2 -0
  131. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  132. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  133. package/src/__tests__/plugin-external-api.test.ts +68 -0
  134. package/src/__tests__/plugin-registry.test.ts +0 -77
  135. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  136. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  137. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  138. package/src/__tests__/plugin-types.test.ts +3 -13
  139. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  140. package/src/__tests__/process-message-display-content.test.ts +421 -0
  141. package/src/__tests__/provider-catalog-visibility.test.ts +158 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  143. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +33 -31
  144. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  145. package/src/__tests__/schedule-routes.test.ts +50 -3
  146. package/src/__tests__/schedule-store.test.ts +94 -0
  147. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  148. package/src/__tests__/schema-transforms.test.ts +20 -0
  149. package/src/__tests__/search-skills-unified.test.ts +0 -5
  150. package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +1 -1
  151. package/src/__tests__/server-history-render.test.ts +43 -0
  152. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  153. package/src/__tests__/skill-load-tool.test.ts +27 -89
  154. package/src/__tests__/skill-memory.test.ts +23 -3
  155. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  156. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  157. package/src/__tests__/skills-install-extract.test.ts +49 -38
  158. package/src/__tests__/skills-install-staging.test.ts +159 -0
  159. package/src/__tests__/skills-uninstall.test.ts +9 -41
  160. package/src/__tests__/skills.test.ts +51 -58
  161. package/src/__tests__/slack-channel-config.test.ts +9 -0
  162. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  163. package/src/__tests__/system-prompt.test.ts +670 -63
  164. package/src/__tests__/terminal-tools.test.ts +28 -1
  165. package/src/__tests__/thread-backfill.test.ts +557 -27
  166. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  167. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  168. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  169. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  170. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  171. package/src/__tests__/tool-executor.test.ts +16 -4
  172. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  173. package/src/__tests__/turn-events-store.test.ts +256 -0
  174. package/src/__tests__/twilio-routes.test.ts +4 -0
  175. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  176. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  177. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  178. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  179. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  180. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  181. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  182. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  183. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  184. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  185. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  186. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  187. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  188. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  189. package/src/a2a/__tests__/task-store.test.ts +246 -0
  190. package/src/a2a/agent-card.ts +58 -0
  191. package/src/a2a/feature-gate.ts +8 -0
  192. package/src/a2a/protocol-constants.ts +21 -0
  193. package/src/a2a/protocol-errors.ts +50 -0
  194. package/src/a2a/protocol-types.ts +162 -0
  195. package/src/a2a/task-store.ts +168 -0
  196. package/src/acp/resolve-agent.ts +1 -1
  197. package/src/agent/image-optimize.ts +13 -5
  198. package/src/agent/loop.ts +167 -18
  199. package/src/calls/voice-session-bridge.ts +61 -42
  200. package/src/channels/config.ts +9 -0
  201. package/src/channels/types.ts +122 -0
  202. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  203. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  204. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  205. package/src/cli/commands/__tests__/schedules.test.ts +960 -0
  206. package/src/cli/commands/changelog.ts +106 -42
  207. package/src/cli/commands/conversations.ts +102 -17
  208. package/src/cli/commands/default-action.ts +10 -53
  209. package/src/cli/commands/notifications.ts +388 -346
  210. package/src/cli/commands/plugins.ts +252 -0
  211. package/src/cli/commands/schedules.ts +683 -0
  212. package/src/cli/commands/telemetry.ts +40 -0
  213. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  214. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  215. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  216. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  217. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  218. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  219. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  220. package/src/cli/lib/cli-colors.ts +12 -0
  221. package/src/cli/lib/confirm-prompt.ts +79 -0
  222. package/src/cli/lib/install-from-github.ts +303 -0
  223. package/src/cli/lib/list-installed-plugins.ts +137 -0
  224. package/src/cli/lib/search-plugins.ts +163 -0
  225. package/src/cli/lib/uninstall-plugin.ts +82 -0
  226. package/src/cli/lib/unknown-command.ts +111 -0
  227. package/src/cli/program.ts +52 -2
  228. package/src/config/assistant-feature-flags.ts +24 -54
  229. package/src/config/bundled-skills/app-builder/SKILL.md +140 -22
  230. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  231. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  232. package/src/config/bundled-skills/document/SKILL.md +23 -3
  233. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  234. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  235. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  236. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  237. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  238. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  239. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  240. package/src/config/bundled-tool-registry.ts +6 -0
  241. package/src/config/call-site-defaults.ts +105 -0
  242. package/src/config/feature-flag-registry.json +41 -9
  243. package/src/config/llm-resolver.ts +52 -1
  244. package/src/config/loader.ts +64 -38
  245. package/src/config/schema.ts +9 -10
  246. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  247. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  248. package/src/config/schemas/channels.ts +17 -0
  249. package/src/config/schemas/compaction.ts +28 -0
  250. package/src/config/schemas/conversations.ts +10 -0
  251. package/src/config/schemas/heartbeat.ts +23 -0
  252. package/src/config/schemas/llm-request-logs.ts +31 -7
  253. package/src/config/schemas/llm.ts +1 -0
  254. package/src/config/schemas/memory-retrieval.ts +18 -0
  255. package/src/config/schemas/memory-retrospective.ts +1 -1
  256. package/src/config/schemas/memory-v2.ts +4 -4
  257. package/src/config/schemas/memory.ts +3 -1
  258. package/src/config/schemas/tools.ts +14 -0
  259. package/src/config/seed-inference-profiles.ts +99 -29
  260. package/src/config/skills.ts +3 -96
  261. package/src/context/compactor.ts +1107 -0
  262. package/src/context/token-estimator.ts +34 -36
  263. package/src/context/window-manager.ts +197 -1520
  264. package/src/credential-execution/managed-catalog.ts +37 -0
  265. package/src/credential-health/credential-health-service.ts +280 -19
  266. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +33 -18
  267. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  268. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  269. package/src/daemon/approval-generators.ts +8 -6
  270. package/src/daemon/config-watcher.ts +94 -31
  271. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  272. package/src/daemon/conversation-agent-loop.ts +198 -11
  273. package/src/daemon/conversation-error.ts +171 -37
  274. package/src/daemon/conversation-lifecycle.ts +53 -40
  275. package/src/daemon/conversation-messaging.ts +25 -6
  276. package/src/daemon/conversation-process.ts +49 -12
  277. package/src/daemon/conversation-runtime-assembly.ts +25 -1
  278. package/src/daemon/conversation-slash.ts +12 -5
  279. package/src/daemon/conversation-store.ts +11 -4
  280. package/src/daemon/conversation-tool-setup.ts +39 -7
  281. package/src/daemon/conversation.ts +33 -8
  282. package/src/daemon/date-context.ts +40 -0
  283. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  284. package/src/daemon/first-greeting.ts +22 -2
  285. package/src/daemon/guardian-action-generators.ts +1 -125
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  287. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  288. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  289. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  290. package/src/daemon/handlers/config-a2a.ts +289 -0
  291. package/src/daemon/handlers/config-model.ts +6 -5
  292. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  293. package/src/daemon/handlers/conversations.ts +1 -0
  294. package/src/daemon/handlers/shared.ts +14 -5
  295. package/src/daemon/handlers/skills.ts +111 -108
  296. package/src/daemon/history-repair.ts +28 -1
  297. package/src/daemon/host-app-control-proxy.ts +153 -27
  298. package/src/daemon/host-proxy-preactivation.ts +85 -18
  299. package/src/daemon/lifecycle.ts +89 -91
  300. package/src/daemon/meet-host-supervisor.ts +5 -4
  301. package/src/daemon/memory-v2-startup.ts +85 -0
  302. package/src/daemon/message-protocol.ts +1 -0
  303. package/src/daemon/message-types/conversations.ts +25 -0
  304. package/src/daemon/message-types/messages.ts +61 -0
  305. package/src/daemon/message-types/notifications.ts +21 -0
  306. package/src/daemon/message-types/subagents.ts +1 -0
  307. package/src/daemon/message-types/sync.ts +1 -0
  308. package/src/daemon/pkb-reminder-builder.test.ts +11 -54
  309. package/src/daemon/pkb-reminder-builder.ts +5 -20
  310. package/src/daemon/plugin-source-watcher.ts +146 -0
  311. package/src/daemon/process-message.ts +24 -3
  312. package/src/daemon/server.ts +11 -2
  313. package/src/daemon/skill-memory-refresh.ts +33 -0
  314. package/src/daemon/wake-target-adapter.ts +2 -0
  315. package/src/documents/document-store.ts +221 -3
  316. package/src/embedded/plugin-api.ts +40 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  318. package/src/export/transcript-formatter.ts +54 -20
  319. package/src/filing/filing-service.ts +39 -0
  320. package/src/heartbeat/__tests__/heartbeat-service.test.ts +135 -6
  321. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  322. package/src/heartbeat/heartbeat-service.ts +73 -189
  323. package/src/home/__tests__/feed-types.test.ts +80 -0
  324. package/src/home/feed-types.ts +36 -2
  325. package/src/home/post-connect-feed.ts +1 -0
  326. package/src/index.ts +18 -1
  327. package/src/ipc/cli-client.ts +147 -45
  328. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  329. package/src/mcp/client.ts +20 -4
  330. package/src/media/image-credentials.ts +3 -3
  331. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  332. package/src/memory/__tests__/conversation-queries.test.ts +483 -0
  333. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  334. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  335. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  336. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  337. package/src/memory/__tests__/message-content.test.ts +35 -0
  338. package/src/memory/bookmark-crud.ts +42 -10
  339. package/src/memory/context-search/sources/conversations.ts +62 -2
  340. package/src/memory/context-search/sources/workspace.ts +4 -0
  341. package/src/memory/conversation-crud.ts +63 -19
  342. package/src/memory/conversation-queries.ts +197 -11
  343. package/src/memory/conversation-title-service.ts +26 -4
  344. package/src/memory/db-init.ts +12 -0
  345. package/src/memory/delivery-crud.ts +152 -5
  346. package/src/memory/embedding-backend.ts +4 -4
  347. package/src/memory/external-conversation-store.ts +66 -5
  348. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +150 -12
  349. package/src/memory/graph/conversation-graph-memory.ts +49 -21
  350. package/src/memory/graph/tools.ts +9 -40
  351. package/src/memory/indexer.ts +34 -29
  352. package/src/memory/invite-store.ts +53 -0
  353. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  354. package/src/memory/jobs/embed-concept-page.ts +20 -11
  355. package/src/memory/jobs-worker.ts +6 -1
  356. package/src/memory/llm-request-log-source-clickhouse.ts +24 -12
  357. package/src/memory/llm-request-log-source.ts +19 -52
  358. package/src/memory/llm-request-log-store.ts +92 -1
  359. package/src/memory/llm-usage-store.ts +125 -5
  360. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  361. package/src/memory/memory-retrospective-job.ts +33 -6
  362. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  363. package/src/memory/message-content.ts +1 -1
  364. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  365. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  366. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  367. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  368. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  369. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  370. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  371. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  372. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  373. package/src/memory/migrations/index.ts +9 -0
  374. package/src/memory/migrations/registry.ts +16 -0
  375. package/src/memory/onboarding-events-store.ts +106 -0
  376. package/src/memory/schema/a2a.ts +15 -0
  377. package/src/memory/schema/bookmarks.ts +0 -2
  378. package/src/memory/schema/calls.ts +1 -0
  379. package/src/memory/schema/index.ts +1 -0
  380. package/src/memory/schema/inference.ts +3 -3
  381. package/src/memory/schema/infrastructure.ts +13 -0
  382. package/src/memory/turn-events-store.ts +127 -2
  383. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  384. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  385. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  386. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  387. package/src/memory/v2/__tests__/injection.test.ts +288 -11
  388. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  389. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  390. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  391. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  392. package/src/memory/v2/__tests__/router.test.ts +15 -0
  393. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  394. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  395. package/src/memory/v2/activation-store.ts +14 -16
  396. package/src/memory/v2/cli-command-content.ts +19 -0
  397. package/src/memory/v2/cli-command-store.ts +304 -0
  398. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  399. package/src/memory/v2/injection.ts +81 -26
  400. package/src/memory/v2/migration.ts +49 -19
  401. package/src/memory/v2/page-index.ts +63 -8
  402. package/src/memory/v2/prompts/router.ts +11 -8
  403. package/src/memory/v2/prompts/sweep.ts +2 -2
  404. package/src/memory/v2/qdrant.ts +135 -7
  405. package/src/memory/v2/router.ts +9 -8
  406. package/src/memory/v2/skill-store.ts +120 -35
  407. package/src/memory/v2/static-context.ts +4 -4
  408. package/src/memory/v2/types.ts +23 -0
  409. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  410. package/src/messaging/providers/a2a/deliver.ts +156 -0
  411. package/src/messaging/providers/gmail/client.ts +9 -2
  412. package/src/messaging/providers/index.ts +11 -2
  413. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  414. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  415. package/src/messaging/providers/slack/adapter.ts +43 -5
  416. package/src/messaging/providers/slack/client.ts +27 -0
  417. package/src/messaging/providers/slack/deep-link.ts +65 -0
  418. package/src/messaging/providers/slack/download.ts +104 -0
  419. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  420. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  421. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  422. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  423. package/src/messaging/providers/slack/types.ts +20 -1
  424. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  425. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  426. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  427. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  428. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  429. package/src/notifications/adapters/macos.ts +12 -2
  430. package/src/notifications/broadcaster.ts +29 -4
  431. package/src/notifications/conversation-pairing.ts +2 -1
  432. package/src/notifications/copy-composer.ts +17 -64
  433. package/src/notifications/decision-engine.ts +113 -45
  434. package/src/notifications/deterministic-checks.ts +96 -0
  435. package/src/notifications/emit-signal.ts +21 -1
  436. package/src/notifications/home-feed-side-effect.ts +138 -5
  437. package/src/notifications/signal.ts +3 -5
  438. package/src/notifications/types.ts +8 -0
  439. package/src/oauth/connection-resolver.ts +8 -4
  440. package/src/oauth/platform-connection.test.ts +43 -3
  441. package/src/oauth/platform-connection.ts +19 -6
  442. package/src/oauth/seed-providers.ts +10 -1
  443. package/src/permissions/checker.ts +2 -0
  444. package/src/permissions/ipc-risk-types.ts +1 -0
  445. package/src/permissions/question-prompter.test.ts +416 -0
  446. package/src/permissions/question-prompter.ts +294 -0
  447. package/src/platform/client.test.ts +1 -1
  448. package/src/platform/client.ts +1 -1
  449. package/src/plugin-api/constants.ts +26 -0
  450. package/src/plugin-api/index.ts +34 -1
  451. package/src/plugin-api/types.ts +104 -22
  452. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  453. package/src/plugins/defaults/compaction.ts +0 -4
  454. package/src/plugins/defaults/empty-response.ts +0 -2
  455. package/src/plugins/defaults/history-repair.ts +0 -2
  456. package/src/plugins/defaults/injectors.ts +74 -22
  457. package/src/plugins/defaults/llm-call.ts +0 -2
  458. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  459. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  460. package/src/plugins/defaults/persistence.ts +0 -2
  461. package/src/plugins/defaults/title-generate.ts +0 -5
  462. package/src/plugins/defaults/token-estimate.ts +0 -2
  463. package/src/plugins/defaults/tool-error.ts +0 -7
  464. package/src/plugins/defaults/tool-execute.ts +0 -2
  465. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  466. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  467. package/src/plugins/external-api.ts +104 -0
  468. package/src/plugins/external-plugin-loader.ts +187 -42
  469. package/src/plugins/feature-gate.ts +22 -0
  470. package/src/plugins/pipeline.ts +37 -0
  471. package/src/plugins/registry.ts +48 -80
  472. package/src/plugins/types.ts +40 -26
  473. package/src/plugins/user-loader.ts +21 -2
  474. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  475. package/src/proactive-artifact/job.test.ts +37 -5
  476. package/src/prompts/__tests__/system-prompt.test.ts +10 -43
  477. package/src/prompts/__tests__/task-progress-hint-section.test.ts +95 -0
  478. package/src/prompts/normalize-onboarding.ts +27 -0
  479. package/src/prompts/sections.ts +302 -0
  480. package/src/prompts/system-prompt.ts +63 -174
  481. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  482. package/src/prompts/templates/system-sections.ts +164 -0
  483. package/src/providers/__tests__/inference.test.ts +24 -7
  484. package/src/providers/anthropic/client.ts +28 -28
  485. package/src/providers/call-site-routing.ts +24 -6
  486. package/src/providers/connection-resolution.ts +68 -11
  487. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  488. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  489. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  490. package/src/providers/inference/adapter-factory.ts +32 -6
  491. package/src/providers/inference/auth.ts +12 -0
  492. package/src/providers/inference/backfill.ts +14 -1
  493. package/src/providers/inference/connections.ts +159 -34
  494. package/src/providers/inference/resolve-auth.ts +14 -4
  495. package/src/providers/model-catalog.ts +249 -12
  496. package/src/providers/model-intents.ts +3 -3
  497. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  498. package/src/providers/openai/chat-completions-provider.ts +169 -8
  499. package/src/providers/openrouter/client.ts +49 -4
  500. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -2
  501. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  502. package/src/providers/provider-availability.ts +17 -2
  503. package/src/providers/provider-catalog-visibility.ts +38 -0
  504. package/src/providers/provider-send-message.ts +27 -12
  505. package/src/providers/registry.ts +52 -15
  506. package/src/providers/retry.ts +47 -1
  507. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  508. package/src/runtime/agent-wake.ts +103 -15
  509. package/src/runtime/auth/route-policy.ts +21 -1
  510. package/src/runtime/btw-sidechain.ts +2 -0
  511. package/src/runtime/http-server.ts +7 -16
  512. package/src/runtime/http-types.ts +19 -47
  513. package/src/runtime/migrations/origin-mode.ts +1 -1
  514. package/src/runtime/pending-interactions.ts +1 -0
  515. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  516. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  517. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  518. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +172 -23
  519. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  520. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  521. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  522. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  523. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  524. package/src/runtime/routes/acp-routes.ts +5 -3
  525. package/src/runtime/routes/auth-routes.ts +1 -1
  526. package/src/runtime/routes/bookmark-routes.ts +5 -3
  527. package/src/runtime/routes/btw-routes.ts +5 -1
  528. package/src/runtime/routes/channel-availability-routes.ts +126 -0
  529. package/src/runtime/routes/consolidation-routes.ts +100 -0
  530. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  531. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  532. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  533. package/src/runtime/routes/conversation-query-routes.ts +99 -35
  534. package/src/runtime/routes/conversation-routes.ts +97 -11
  535. package/src/runtime/routes/documents-routes.ts +25 -86
  536. package/src/runtime/routes/group-routes.ts +5 -0
  537. package/src/runtime/routes/inbound-conversation.ts +28 -8
  538. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  539. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  540. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  541. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  542. package/src/runtime/routes/index.ts +8 -0
  543. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  544. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  545. package/src/runtime/routes/inference-provider-connection-routes.ts +199 -22
  546. package/src/runtime/routes/integrations/a2a.ts +235 -0
  547. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  548. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  549. package/src/runtime/routes/integrations/twilio.ts +6 -13
  550. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  551. package/src/runtime/routes/notification-routes.ts +1 -1
  552. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  553. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  554. package/src/runtime/routes/question-routes.ts +259 -0
  555. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  556. package/src/runtime/routes/schedule-routes.ts +4 -7
  557. package/src/runtime/routes/subagents-routes.ts +98 -18
  558. package/src/runtime/routes/telemetry-routes.ts +27 -0
  559. package/src/runtime/routes/tts-routes.ts +27 -2
  560. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  561. package/src/runtime/routes/workspace-routes.ts +28 -0
  562. package/src/runtime/services/conversation-serializer.ts +39 -7
  563. package/src/runtime/sync/resource-sync-events.ts +93 -1
  564. package/src/schedule/schedule-store.ts +27 -2
  565. package/src/schedule/scheduler.ts +9 -1
  566. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  567. package/src/security/untrusted-content.ts +93 -8
  568. package/src/skills/catalog-files.ts +1 -1
  569. package/src/skills/catalog-install.ts +233 -116
  570. package/src/skills/clawhub.ts +70 -13
  571. package/src/skills/managed-store.ts +4 -119
  572. package/src/skills/skillssh-registry.ts +27 -48
  573. package/src/subagent/manager.ts +17 -7
  574. package/src/telemetry/types.ts +113 -1
  575. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  576. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  577. package/src/tools/apps/executors.ts +58 -7
  578. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  579. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  580. package/src/tools/browser/browser-execution.ts +15 -11
  581. package/src/tools/computer-use/definitions.ts +3 -3
  582. package/src/tools/credentials/vault.ts +1 -1
  583. package/src/tools/document/document-tool.ts +124 -1
  584. package/src/tools/filesystem/edit.ts +1 -1
  585. package/src/tools/filesystem/list.ts +1 -1
  586. package/src/tools/filesystem/read.ts +1 -1
  587. package/src/tools/filesystem/write.ts +5 -2
  588. package/src/tools/host-filesystem/transfer.ts +1 -1
  589. package/src/tools/host-terminal/host-shell.ts +1 -1
  590. package/src/tools/memory/register.ts +1 -9
  591. package/src/tools/permission-checker.ts +1 -1
  592. package/src/tools/registry.ts +17 -7
  593. package/src/tools/schedule/create.ts +2 -2
  594. package/src/tools/schema-transforms.ts +7 -2
  595. package/src/tools/side-effects.ts +1 -0
  596. package/src/tools/skills/delete-managed.ts +4 -4
  597. package/src/tools/skills/execute.ts +1 -1
  598. package/src/tools/skills/scaffold-managed.ts +3 -2
  599. package/src/tools/subagent/notify-parent.ts +1 -1
  600. package/src/tools/system/request-permission.ts +2 -2
  601. package/src/tools/terminal/safe-env.ts +60 -1
  602. package/src/tools/tool-manifest.ts +2 -0
  603. package/src/tools/types.ts +107 -21
  604. package/src/tools/ui-surface/definitions.ts +6 -5
  605. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  606. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  607. package/src/types/onboarding-context.ts +2 -0
  608. package/src/util/errors.ts +17 -0
  609. package/src/util/platform.ts +10 -0
  610. package/src/watcher/__tests__/engine.test.ts +22 -0
  611. package/src/watcher/engine.ts +6 -2
  612. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  613. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  614. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  615. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  616. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  617. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  618. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  619. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  620. package/src/workspace/migrations/registry.ts +10 -0
  621. package/src/workspace/migrations/runner.ts +39 -9
  622. package/src/workspace/migrations/types.ts +4 -0
  623. package/examples/plugins/echo/bun.lock +0 -25
  624. package/src/__tests__/context-window-manager.test.ts +0 -2481
  625. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  626. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  627. package/src/context/prompts/compact.md +0 -26
  628. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  629. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  630. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -51,6 +51,34 @@ mock.module("../runtime/gateway-client.js", () => ({
51
51
  deliverChannelReply: async () => {},
52
52
  }));
53
53
 
54
+ type DownloadedSlackFile = {
55
+ filename: string;
56
+ mimeType: string;
57
+ data: string;
58
+ };
59
+
60
+ const downloadSlackFileMock = mock(
61
+ async (_file: unknown, _token: string): Promise<DownloadedSlackFile | null> =>
62
+ null,
63
+ );
64
+
65
+ mock.module("../messaging/providers/slack/download.js", () => ({
66
+ downloadSlackFile: (file: unknown, token: string) =>
67
+ downloadSlackFileMock(file, token),
68
+ }));
69
+
70
+ mock.module("../messaging/providers/slack/adapter.js", () => ({
71
+ slackProvider: {
72
+ resolveConnection: async () => ({}),
73
+ getThreadRepliesPage: async () => ({ messages: [], hasMore: false }),
74
+ getHistory: async () => [],
75
+ },
76
+ withSlackBotToken: async (
77
+ _account: string | undefined,
78
+ fn: (token: string) => Promise<unknown>,
79
+ ) => fn("test-slack-token"),
80
+ }));
81
+
54
82
  // ---------------------------------------------------------------------------
55
83
  // Imports (after mocks)
56
84
  // ---------------------------------------------------------------------------
@@ -65,6 +93,7 @@ import {
65
93
  import type { MessageRow } from "../memory/conversation-crud.js";
66
94
  import { getDb } from "../memory/db-connection.js";
67
95
  import { initializeDb } from "../memory/db-init.js";
96
+ import { recordInbound } from "../memory/delivery-crud.js";
68
97
  import type { Message as MessagingMessage } from "../messaging/provider-types.js";
69
98
  import * as slackBackfill from "../messaging/providers/slack/backfill.js";
70
99
  import {
@@ -122,6 +151,8 @@ const SLACK_CHANNEL_ID = "C0THREAD";
122
151
 
123
152
  function resetState(): void {
124
153
  const db = getDb();
154
+ db.$client.exec("DELETE FROM message_attachments");
155
+ db.$client.exec("DELETE FROM attachments");
125
156
  db.$client.exec("DELETE FROM messages");
126
157
  db.$client.exec("DELETE FROM conversations");
127
158
  _backfillTriggerCache.clear();
@@ -129,6 +160,8 @@ function resetState(): void {
129
160
  backfillThreadMock.mockImplementation(async () => []);
130
161
  backfillDmMock.mockReset();
131
162
  backfillDmMock.mockImplementation(async () => []);
163
+ downloadSlackFileMock.mockReset();
164
+ downloadSlackFileMock.mockResolvedValue(null);
132
165
  }
133
166
 
134
167
  let convCounter = 0;
@@ -224,11 +257,31 @@ function makeBackfillMessage(
224
257
 
225
258
  interface PersistedRow {
226
259
  role: string;
260
+ /**
261
+ * Inner content with any `<external_content …>` wrapper stripped. Most
262
+ * assertions want to match against the raw text the backfill source
263
+ * produced; the wrapping is verified separately via {@link rawContent}.
264
+ */
227
265
  content: string;
266
+ /** Raw persisted content, including any wrapping the daemon applied. */
267
+ rawContent: string;
228
268
  channelTs: string | undefined;
229
269
  threadTs: string | undefined;
230
270
  displayName: string | undefined;
271
+ actorExternalUserId: string | undefined;
231
272
  slackFiles: Array<{ name: string; mimetype?: string }> | undefined;
273
+ provenanceTrustClass: string | undefined;
274
+ provenanceSourceChannel: string | undefined;
275
+ provenanceGuardianExternalUserId: string | undefined;
276
+ provenanceRequesterIdentifier: string | undefined;
277
+ }
278
+
279
+ const EXTERNAL_CONTENT_WRAPPER =
280
+ /^<external_content[^>]*>\n([\s\S]*?)\n<\/external_content>$/;
281
+
282
+ function unwrapExternalContent(content: string): string {
283
+ const match = content.match(EXTERNAL_CONTENT_WRAPPER);
284
+ return match ? match[1] : content;
232
285
  }
233
286
 
234
287
  function readPersistedSlackRows(conversationId: string): PersistedRow[] {
@@ -237,11 +290,17 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
237
290
  for (const row of rows) {
238
291
  const blank: PersistedRow = {
239
292
  role: row.role,
240
- content: row.content,
293
+ content: unwrapExternalContent(row.content),
294
+ rawContent: row.content,
241
295
  channelTs: undefined,
242
296
  threadTs: undefined,
243
297
  displayName: undefined,
298
+ actorExternalUserId: undefined,
244
299
  slackFiles: undefined,
300
+ provenanceTrustClass: undefined,
301
+ provenanceSourceChannel: undefined,
302
+ provenanceGuardianExternalUserId: undefined,
303
+ provenanceRequesterIdentifier: undefined,
245
304
  };
246
305
  if (!row.metadata) {
247
306
  out.push(blank);
@@ -271,14 +330,32 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
271
330
  const slackMeta = readSlackMetadata(slackMetaRaw);
272
331
  out.push({
273
332
  role: row.role,
274
- content: row.content,
333
+ content: unwrapExternalContent(row.content),
334
+ rawContent: row.content,
275
335
  channelTs: slackMeta?.channelTs,
276
336
  threadTs: slackMeta?.threadTs,
277
337
  displayName: slackMeta?.displayName,
338
+ actorExternalUserId: slackMeta?.actorExternalUserId,
278
339
  slackFiles: slackMeta?.slackFiles?.map((file) => ({
279
340
  name: file.name,
280
341
  ...(file.mimetype ? { mimetype: file.mimetype } : {}),
281
342
  })),
343
+ provenanceTrustClass:
344
+ typeof envelope.provenanceTrustClass === "string"
345
+ ? envelope.provenanceTrustClass
346
+ : undefined,
347
+ provenanceSourceChannel:
348
+ typeof envelope.provenanceSourceChannel === "string"
349
+ ? envelope.provenanceSourceChannel
350
+ : undefined,
351
+ provenanceGuardianExternalUserId:
352
+ typeof envelope.provenanceGuardianExternalUserId === "string"
353
+ ? envelope.provenanceGuardianExternalUserId
354
+ : undefined,
355
+ provenanceRequesterIdentifier:
356
+ typeof envelope.provenanceRequesterIdentifier === "string"
357
+ ? envelope.provenanceRequesterIdentifier
358
+ : undefined,
282
359
  });
283
360
  }
284
361
  return out;
@@ -828,6 +905,274 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
828
905
  expect(rendered).not.toContain("F-DRAFT");
829
906
  });
830
907
 
908
+ test("backfilled Slack image files are persisted as image content blocks", async () => {
909
+ const conv = createTestConversation();
910
+
911
+ seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
912
+
913
+ const imageBase64 = Buffer.from("fake image bytes").toString("base64");
914
+ downloadSlackFileMock.mockResolvedValue({
915
+ filename: "diagram.png",
916
+ mimeType: "image/png",
917
+ data: imageBase64,
918
+ });
919
+ backfillThreadMock.mockImplementation(async () => [
920
+ makeBackfillMessage({
921
+ id: "1234.1",
922
+ text: "uploaded the diagram",
923
+ threadId: "1234.0",
924
+ sender: { id: "U_IMAGE", name: "Image Sharer" },
925
+ metadata: {
926
+ slackFiles: [
927
+ {
928
+ id: "F-DIAGRAM",
929
+ name: "diagram.png",
930
+ mimetype: "image/png",
931
+ urlPrivateDownload:
932
+ "https://files.slack.com/files-pri/T/F/download",
933
+ },
934
+ ],
935
+ },
936
+ }),
937
+ ]);
938
+
939
+ await triggerSlackThreadBackfillIfNeeded({
940
+ conversationId: conv.id,
941
+ channelId: SLACK_CHANNEL_ID,
942
+ threadTs: "1234.0",
943
+ excludeChannelTs: "1234.2",
944
+ });
945
+
946
+ expect(downloadSlackFileMock).toHaveBeenCalledTimes(1);
947
+ expect(downloadSlackFileMock.mock.calls[0]?.[1]).toBe("test-slack-token");
948
+
949
+ const imageRow = readMessagesByConversation(conv.id).find((row) =>
950
+ row.content.includes("uploaded the diagram"),
951
+ );
952
+ expect(imageRow).toBeDefined();
953
+ const blocks = JSON.parse(imageRow!.content) as Message["content"];
954
+ const textBlock = blocks.find(
955
+ (block): block is Extract<Message["content"][number], { type: "text" }> =>
956
+ block.type === "text",
957
+ );
958
+ const imageBlock = blocks.find(
959
+ (
960
+ block,
961
+ ): block is Extract<Message["content"][number], { type: "image" }> =>
962
+ block.type === "image",
963
+ );
964
+ expect(textBlock?.text).toBe("uploaded the diagram");
965
+ expect(textBlock?.text).not.toContain("<external_content");
966
+ expect(imageBlock?.source.media_type).toBe("image/png");
967
+ expect(imageBlock?.source.data).toBe(imageBase64);
968
+
969
+ const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
970
+ loader: readMessageRowsByConversation,
971
+ trustClass: "guardian",
972
+ });
973
+
974
+ expect(context).not.toBeNull();
975
+ const contextImage = context!.messages
976
+ .flatMap((message) => message.content)
977
+ .find((block) => block.type === "image");
978
+ expect(contextImage).toBeDefined();
979
+ });
980
+
981
+ test("backfilled bot Slack image files are persisted as user image history", async () => {
982
+ const conv = createTestConversation();
983
+
984
+ seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
985
+ downloadSlackFileMock.mockResolvedValue({
986
+ filename: "bot-diagram.png",
987
+ mimeType: "image/png",
988
+ data: Buffer.from("bot image bytes").toString("base64"),
989
+ });
990
+ backfillThreadMock.mockImplementation(async () => [
991
+ makeBackfillMessage({
992
+ id: "1234.1",
993
+ text: "bot posted a diagram",
994
+ threadId: "1234.0",
995
+ sender: { id: "B_IMAGE", name: "Build Bot" },
996
+ metadata: {
997
+ isBot: true,
998
+ slackFiles: [
999
+ {
1000
+ id: "F-BOT-DIAGRAM",
1001
+ name: "bot-diagram.png",
1002
+ mimetype: "image/png",
1003
+ urlPrivateDownload:
1004
+ "https://files.slack.com/files-pri/T/F/download",
1005
+ },
1006
+ ],
1007
+ },
1008
+ }),
1009
+ ]);
1010
+
1011
+ await triggerSlackThreadBackfillIfNeeded({
1012
+ conversationId: conv.id,
1013
+ channelId: SLACK_CHANNEL_ID,
1014
+ threadTs: "1234.0",
1015
+ excludeChannelTs: "1234.2",
1016
+ });
1017
+
1018
+ expect(downloadSlackFileMock).toHaveBeenCalledTimes(1);
1019
+ const botRow = readMessagesByConversation(conv.id).find((row) =>
1020
+ row.content.includes("bot posted a diagram"),
1021
+ );
1022
+ expect(botRow).toBeDefined();
1023
+ expect(botRow?.role).toBe("user");
1024
+ const blocks = JSON.parse(botRow!.content) as Message["content"];
1025
+ const textBlock = blocks.find(
1026
+ (block): block is Extract<Message["content"][number], { type: "text" }> =>
1027
+ block.type === "text",
1028
+ );
1029
+ const imageBlock = blocks.find(
1030
+ (
1031
+ block,
1032
+ ): block is Extract<Message["content"][number], { type: "image" }> =>
1033
+ block.type === "image",
1034
+ );
1035
+ expect(textBlock?.text).toBe("bot posted a diagram");
1036
+ expect(textBlock?.text).not.toContain("<external_content");
1037
+ expect(imageBlock?.source.media_type).toBe("image/png");
1038
+
1039
+ const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
1040
+ loader: readMessageRowsByConversation,
1041
+ trustClass: "guardian",
1042
+ });
1043
+
1044
+ expect(context).not.toBeNull();
1045
+ const contextImage = context!.messages
1046
+ .flatMap((message) => message.content)
1047
+ .find((block) => block.type === "image");
1048
+ expect(contextImage).toBeDefined();
1049
+ });
1050
+
1051
+ test("backfilled non-guardian text is persisted raw with actor provenance", async () => {
1052
+ const conv = createTestConversation();
1053
+
1054
+ backfillThreadMock.mockImplementation(async () => [
1055
+ makeBackfillMessage({
1056
+ id: "1234.0",
1057
+ text: "i can't find the credential field",
1058
+ threadId: undefined,
1059
+ sender: { id: "U_ANITA", name: "Anita" },
1060
+ }),
1061
+ ]);
1062
+
1063
+ await triggerSlackThreadBackfillIfNeeded({
1064
+ conversationId: conv.id,
1065
+ channelId: SLACK_CHANNEL_ID,
1066
+ threadTs: "1234.0",
1067
+ guardianExternalUserId: "U_GUARDIAN",
1068
+ });
1069
+
1070
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1071
+ (p) => p.channelTs === "1234.0",
1072
+ );
1073
+ expect(persisted).toBeDefined();
1074
+ expect(persisted.role).toBe("user");
1075
+ expect(persisted.rawContent).toBe("i can't find the credential field");
1076
+ expect(persisted.rawContent).not.toContain("<external_content");
1077
+ expect(persisted.content).toBe("i can't find the credential field");
1078
+ expect(persisted.actorExternalUserId).toBe("U_ANITA");
1079
+ expect(persisted.provenanceTrustClass).toBe("unknown");
1080
+ expect(persisted.provenanceSourceChannel).toBe("slack");
1081
+ expect(persisted.provenanceGuardianExternalUserId).toBe("U_GUARDIAN");
1082
+ expect(persisted.provenanceRequesterIdentifier).toBe("U_ANITA");
1083
+ });
1084
+
1085
+ test("backfilled guardian-authored text is persisted raw with guardian provenance", async () => {
1086
+ const conv = createTestConversation();
1087
+
1088
+ backfillThreadMock.mockImplementation(async () => [
1089
+ makeBackfillMessage({
1090
+ id: "1234.0",
1091
+ text: "here is the trusted context",
1092
+ threadId: undefined,
1093
+ sender: { id: "U_GUARDIAN", name: "Guardian User" },
1094
+ }),
1095
+ ]);
1096
+
1097
+ await triggerSlackThreadBackfillIfNeeded({
1098
+ conversationId: conv.id,
1099
+ channelId: SLACK_CHANNEL_ID,
1100
+ threadTs: "1234.0",
1101
+ guardianExternalUserId: "U_GUARDIAN",
1102
+ });
1103
+
1104
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1105
+ (p) => p.channelTs === "1234.0",
1106
+ );
1107
+ expect(persisted).toBeDefined();
1108
+ expect(persisted.role).toBe("user");
1109
+ expect(persisted.rawContent).toBe("here is the trusted context");
1110
+ expect(persisted.rawContent).not.toContain("<external_content");
1111
+ expect(persisted.actorExternalUserId).toBe("U_GUARDIAN");
1112
+ expect(persisted.provenanceTrustClass).toBe("guardian");
1113
+ expect(persisted.provenanceSourceChannel).toBe("slack");
1114
+ expect(persisted.provenanceGuardianExternalUserId).toBe("U_GUARDIAN");
1115
+ expect(persisted.provenanceRequesterIdentifier).toBe("U_GUARDIAN");
1116
+ });
1117
+
1118
+ test("backfilled bot-authored text is persisted raw as user history", async () => {
1119
+ const conv = createTestConversation();
1120
+
1121
+ backfillThreadMock.mockImplementation(async () => [
1122
+ makeBackfillMessage({
1123
+ id: "1234.0",
1124
+ text: "earlier assistant reply",
1125
+ threadId: undefined,
1126
+ sender: { id: "U_BOT", name: "Douglas" },
1127
+ metadata: { isBot: true },
1128
+ }),
1129
+ ]);
1130
+
1131
+ await triggerSlackThreadBackfillIfNeeded({
1132
+ conversationId: conv.id,
1133
+ channelId: SLACK_CHANNEL_ID,
1134
+ threadTs: "1234.0",
1135
+ });
1136
+
1137
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1138
+ (p) => p.channelTs === "1234.0",
1139
+ );
1140
+ expect(persisted).toBeDefined();
1141
+ expect(persisted.role).toBe("user");
1142
+ expect(persisted.rawContent).toBe("earlier assistant reply");
1143
+ expect(persisted.rawContent).not.toContain("<external_content");
1144
+ expect(persisted.actorExternalUserId).toBe("U_BOT");
1145
+ expect(persisted.provenanceTrustClass).toBe("unknown");
1146
+ expect(persisted.provenanceSourceChannel).toBe("slack");
1147
+ expect(persisted.provenanceRequesterIdentifier).toBe("U_BOT");
1148
+ });
1149
+
1150
+ test("backfilled non-bot message with empty text is persisted unwrapped", async () => {
1151
+ const conv = createTestConversation();
1152
+
1153
+ backfillThreadMock.mockImplementation(async () => [
1154
+ makeBackfillMessage({
1155
+ id: "1234.0",
1156
+ text: "",
1157
+ threadId: undefined,
1158
+ sender: { id: "U_QUIET", name: "Quiet User" },
1159
+ }),
1160
+ ]);
1161
+
1162
+ await triggerSlackThreadBackfillIfNeeded({
1163
+ conversationId: conv.id,
1164
+ channelId: SLACK_CHANNEL_ID,
1165
+ threadTs: "1234.0",
1166
+ });
1167
+
1168
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1169
+ (p) => p.channelTs === "1234.0",
1170
+ );
1171
+ expect(persisted).toBeDefined();
1172
+ expect(persisted.rawContent).toBe("");
1173
+ expect(persisted.rawContent).not.toContain("<external_content");
1174
+ });
1175
+
831
1176
  test("latest stored thread message at or after inbound ts skips backfill using parsed Slack timestamps", async () => {
832
1177
  const conv = createTestConversation();
833
1178
 
@@ -1313,8 +1658,54 @@ function seedHttpActiveMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
1313
1658
  });
1314
1659
  }
1315
1660
 
1661
+ function seedHttpGuardianMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
1662
+ upsertContactChannel({
1663
+ sourceChannel: "slack",
1664
+ externalUserId: HTTP_SLACK_USER_ID,
1665
+ externalChatId: chatId,
1666
+ status: "active",
1667
+ policy: "allow",
1668
+ displayName: HTTP_SLACK_DISPLAY_NAME,
1669
+ role: "guardian",
1670
+ verifiedAt: Date.now(),
1671
+ verifiedVia: "test",
1672
+ });
1673
+ }
1674
+
1316
1675
  let httpMsgCounter = 0;
1317
1676
 
1677
+ function buildSlackChannelRequest(
1678
+ messageId: string,
1679
+ overrides: Record<string, unknown> = {},
1680
+ ): Request {
1681
+ httpMsgCounter++;
1682
+ const body: Record<string, unknown> = {
1683
+ sourceChannel: "slack",
1684
+ interface: "slack",
1685
+ conversationExternalId: HTTP_SLACK_CHANNEL_ID,
1686
+ externalMessageId: `${HTTP_SLACK_CHANNEL_ID}:${messageId}:${httpMsgCounter}`,
1687
+ content: "channel text",
1688
+ actorExternalId: HTTP_SLACK_USER_ID,
1689
+ actorDisplayName: HTTP_SLACK_DISPLAY_NAME,
1690
+ actorUsername: "charlie",
1691
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
1692
+ sourceMetadata: {
1693
+ messageId,
1694
+ chatType: "channel",
1695
+ },
1696
+ ...overrides,
1697
+ };
1698
+
1699
+ return new Request("http://localhost:8080/channels/inbound", {
1700
+ method: "POST",
1701
+ headers: {
1702
+ "Content-Type": "application/json",
1703
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
1704
+ },
1705
+ body: JSON.stringify(body),
1706
+ });
1707
+ }
1708
+
1318
1709
  function buildThreadReplyRequest(
1319
1710
  threadId: string,
1320
1711
  messageId: string,
@@ -1383,12 +1774,14 @@ function buildSlackDmRequest(
1383
1774
  }
1384
1775
 
1385
1776
  interface SlackInboundProcessOptions {
1777
+ displayContent?: string;
1386
1778
  slackRuntimeContextNotice?: string;
1387
1779
  slackInbound?: {
1388
1780
  channelId: string;
1389
1781
  channelTs: string;
1390
1782
  threadTs?: string;
1391
1783
  displayName?: string;
1784
+ actorExternalUserId?: string;
1392
1785
  };
1393
1786
  }
1394
1787
 
@@ -1397,8 +1790,9 @@ function persistSlackInboundFromProcessMessage(
1397
1790
  content: string,
1398
1791
  options?: SlackInboundProcessOptions,
1399
1792
  ): string {
1793
+ const contentToPersist = options?.displayContent ?? content;
1400
1794
  const slackInbound = options?.slackInbound;
1401
- return insertMessage(conversationId, "user", content, {
1795
+ return insertMessage(conversationId, "user", contentToPersist, {
1402
1796
  ...(slackInbound
1403
1797
  ? {
1404
1798
  slackMeta: writeSlackMetadata({
@@ -1411,6 +1805,9 @@ function persistSlackInboundFromProcessMessage(
1411
1805
  ...(slackInbound.displayName
1412
1806
  ? { displayName: slackInbound.displayName }
1413
1807
  : {}),
1808
+ ...(slackInbound.actorExternalUserId
1809
+ ? { actorExternalUserId: slackInbound.actorExternalUserId }
1810
+ : {}),
1414
1811
  eventKind: "message",
1415
1812
  }),
1416
1813
  }
@@ -1436,6 +1833,77 @@ function flattenText(messages: Message[]): string {
1436
1833
  .join("\n");
1437
1834
  }
1438
1835
 
1836
+ interface CapturedSlackProcessMessage {
1837
+ conversationId: string;
1838
+ content: string;
1839
+ attachmentIds?: string[];
1840
+ options?: SlackInboundProcessOptions;
1841
+ }
1842
+
1843
+ function seedAttachment(id: string): void {
1844
+ getDb()
1845
+ .$client.prepare(
1846
+ `INSERT INTO attachments (
1847
+ id, original_filename, mime_type, size_bytes, kind, data_base64, created_at
1848
+ )
1849
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1850
+ )
1851
+ .run(
1852
+ id,
1853
+ "attachment.pdf",
1854
+ "application/pdf",
1855
+ Buffer.byteLength("pdf bytes"),
1856
+ "upload",
1857
+ Buffer.from("pdf bytes").toString("base64"),
1858
+ Date.now(),
1859
+ );
1860
+ }
1861
+
1862
+ async function handleAndCaptureLiveSlackProcessMessage(
1863
+ request: Request,
1864
+ ): Promise<CapturedSlackProcessMessage> {
1865
+ let captured: CapturedSlackProcessMessage | undefined;
1866
+ let resolveProcessed: (() => void) | undefined;
1867
+ const processed = new Promise<void>((resolve) => {
1868
+ resolveProcessed = resolve;
1869
+ });
1870
+
1871
+ const processMessage = async (
1872
+ conversationId: string,
1873
+ content: string,
1874
+ attachmentIds?: string[],
1875
+ options?: SlackInboundProcessOptions,
1876
+ ): Promise<{ messageId: string }> => {
1877
+ captured = { conversationId, content, attachmentIds, options };
1878
+ const messageId = persistSlackInboundFromProcessMessage(
1879
+ conversationId,
1880
+ content,
1881
+ options,
1882
+ );
1883
+ resolveProcessed?.();
1884
+ return { messageId };
1885
+ };
1886
+ setAdapterProcessMessage(processMessage);
1887
+
1888
+ const resp = await handleChannelInbound(
1889
+ request,
1890
+ processMessage,
1891
+ TEST_BEARER_TOKEN,
1892
+ );
1893
+ expect(resp.status).toBe(200);
1894
+ await Promise.race([
1895
+ processed,
1896
+ new Promise((_, reject) =>
1897
+ setTimeout(() => reject(new Error("processMessage not called")), 250),
1898
+ ),
1899
+ ]);
1900
+
1901
+ if (!captured) {
1902
+ throw new Error("processMessage not called");
1903
+ }
1904
+ return captured;
1905
+ }
1906
+
1439
1907
  describe("handleChannelInbound — Slack thread backfill wiring", () => {
1440
1908
  beforeEach(() => {
1441
1909
  resetHttpState();
@@ -1536,14 +2004,76 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
1536
2004
  ).toBe(false);
1537
2005
  });
1538
2006
 
2007
+ test("live Slack non-guardian passes raw displayContent while wrapping model content", async () => {
2008
+ const rawContent = "live Slack raw text";
2009
+ const captured = await handleAndCaptureLiveSlackProcessMessage(
2010
+ buildSlackChannelRequest("1700000000.000300", {
2011
+ content: rawContent,
2012
+ }),
2013
+ );
2014
+
2015
+ expect(captured.content.match(/<external_content/g)?.length).toBe(1);
2016
+ expect(captured.content).toContain('<external_content source="slack"');
2017
+ expect(captured.content).not.toContain('source="webhook"');
2018
+ expect(captured.content).toContain(`\n${rawContent}\n</external_content>`);
2019
+ expect(captured.options?.displayContent).toBe(rawContent);
2020
+ expect(captured.options?.slackInbound?.actorExternalUserId).toBe(
2021
+ HTTP_SLACK_USER_ID,
2022
+ );
2023
+
2024
+ const persisted = readMessagesByConversation(captured.conversationId);
2025
+ expect(persisted).toHaveLength(1);
2026
+ expect(persisted[0].content).toBe(rawContent);
2027
+ });
2028
+
2029
+ test("live Slack attachment-only passes empty raw displayContent for persistence", async () => {
2030
+ seedAttachment("att-slack-file");
2031
+
2032
+ const captured = await handleAndCaptureLiveSlackProcessMessage(
2033
+ buildSlackChannelRequest("1700000000.000350", {
2034
+ content: "",
2035
+ attachmentIds: ["att-slack-file"],
2036
+ }),
2037
+ );
2038
+
2039
+ expect(captured.content.match(/<external_content/g)?.length).toBe(1);
2040
+ expect(captured.content).toContain('<external_content source="slack"');
2041
+ expect(captured.content).toContain("\n\n</external_content>");
2042
+ expect(captured.attachmentIds).toEqual(["att-slack-file"]);
2043
+ expect(captured.options?.displayContent).toBe("");
2044
+
2045
+ const persisted = readMessagesByConversation(captured.conversationId);
2046
+ expect(persisted).toHaveLength(1);
2047
+ expect(persisted[0].content).toBe("");
2048
+ expect(persisted[0].content).not.toContain("<external_content");
2049
+ });
2050
+
2051
+ test("live Slack guardian keeps raw content without displayContent", async () => {
2052
+ seedHttpGuardianMember();
2053
+ const rawContent = "guardian live Slack text";
2054
+ const captured = await handleAndCaptureLiveSlackProcessMessage(
2055
+ buildSlackChannelRequest("1700000000.000400", {
2056
+ content: rawContent,
2057
+ }),
2058
+ );
2059
+
2060
+ expect(captured.content).toBe(rawContent);
2061
+ expect(captured.content).not.toContain("<external_content");
2062
+ expect(
2063
+ Object.prototype.hasOwnProperty.call(
2064
+ captured.options ?? {},
2065
+ "displayContent",
2066
+ ),
2067
+ ).toBe(false);
2068
+
2069
+ const persisted = readMessagesByConversation(captured.conversationId);
2070
+ expect(persisted).toHaveLength(1);
2071
+ expect(persisted[0].content).toBe(rawContent);
2072
+ });
2073
+
1539
2074
  test("late app mention sees unseen backfilled replies before the mention", async () => {
1540
2075
  let capturedTranscript = "";
1541
- let parentTurnSeen = false;
1542
- let resolveParentTurn: (() => void) | undefined;
1543
2076
  let secondTurnSeen = false;
1544
- const parentTurnProcessed = new Promise<void>((resolve) => {
1545
- resolveParentTurn = resolve;
1546
- });
1547
2077
  let resolveSecondTurn: (() => void) | undefined;
1548
2078
  const secondTurnProcessed = new Promise<void>((resolve) => {
1549
2079
  resolveSecondTurn = resolve;
@@ -1560,10 +2090,6 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
1560
2090
  content,
1561
2091
  options,
1562
2092
  );
1563
- if (options?.slackInbound?.channelTs === "1234.0") {
1564
- parentTurnSeen = true;
1565
- resolveParentTurn?.();
1566
- }
1567
2093
  if (options?.slackInbound?.channelTs === "1234.5") {
1568
2094
  const context = loadSlackChronologicalContext(
1569
2095
  conversationId,
@@ -1581,23 +2107,27 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
1581
2107
  };
1582
2108
  setAdapterProcessMessage(processMessage);
1583
2109
 
1584
- const parentResp = await handleChannelInbound(
1585
- buildThreadReplyRequest("1234.0", "1234.0", {
1586
- content: "parent already stored",
1587
- sourceMetadata: {
1588
- messageId: "1234.0",
1589
- chatType: "channel",
2110
+ const parentInbound = recordInbound(
2111
+ "slack",
2112
+ HTTP_SLACK_CHANNEL_ID,
2113
+ `${HTTP_SLACK_CHANNEL_ID}:1234.0:seed`,
2114
+ {
2115
+ sourceMessageId: "1234.0",
2116
+ sourceThreadId: "1234.0",
2117
+ },
2118
+ );
2119
+ persistSlackInboundFromProcessMessage(
2120
+ parentInbound.conversationId,
2121
+ "parent already stored",
2122
+ {
2123
+ slackInbound: {
2124
+ channelId: HTTP_SLACK_CHANNEL_ID,
2125
+ channelTs: "1234.0",
2126
+ threadTs: "1234.0",
2127
+ displayName: HTTP_SLACK_DISPLAY_NAME,
1590
2128
  },
1591
- }),
1592
- processMessage,
1593
- TEST_BEARER_TOKEN,
2129
+ },
1594
2130
  );
1595
- expect(parentResp.status).toBe(200);
1596
- await Promise.race([
1597
- parentTurnProcessed,
1598
- new Promise((resolve) => setTimeout(resolve, 250)),
1599
- ]);
1600
- expect(parentTurnSeen).toBe(true);
1601
2131
 
1602
2132
  backfillThreadMock.mockReset();
1603
2133
  backfillThreadMock.mockImplementation(async () => [