@vellumai/assistant 0.6.4 → 0.6.5

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 (717) hide show
  1. package/.prettierignore +5 -0
  2. package/ARCHITECTURE.md +32 -36
  3. package/Dockerfile +12 -0
  4. package/README.md +3 -4
  5. package/bun.lock +8 -3
  6. package/docs/architecture/integrations.md +1 -20
  7. package/docs/architecture/security.md +16 -16
  8. package/docs/error-handling.md +111 -0
  9. package/docs/skills.md +10 -10
  10. package/docs/stt-provider-onboarding.md +2 -1
  11. package/knip.json +9 -2
  12. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  13. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  14. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  15. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  16. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  17. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  18. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  19. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  20. package/openapi.yaml +123 -11
  21. package/package.json +6 -3
  22. package/scripts/generate-openapi.ts +50 -11
  23. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  24. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  25. package/src/__tests__/agent-loop.test.ts +112 -1
  26. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  27. package/src/__tests__/anthropic-provider.test.ts +171 -2
  28. package/src/__tests__/approval-cascade.test.ts +31 -10
  29. package/src/__tests__/approval-routes-http.test.ts +134 -10
  30. package/src/__tests__/assistant-attachments.test.ts +44 -0
  31. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  32. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  33. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  34. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  35. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  36. package/src/__tests__/btw-routes.test.ts +47 -1
  37. package/src/__tests__/call-controller.test.ts +1 -2
  38. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  39. package/src/__tests__/catalog-cache.test.ts +27 -4
  40. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  41. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  42. package/src/__tests__/checker.test.ts +428 -501
  43. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  44. package/src/__tests__/compaction-circuit-breaker.test.ts +336 -0
  45. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  46. package/src/__tests__/config-analysis.test.ts +11 -28
  47. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  48. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  49. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  50. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  51. package/src/__tests__/config-schema.test.ts +427 -114
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  54. package/src/__tests__/contacts-write.test.ts +4 -4
  55. package/src/__tests__/context-token-estimator.test.ts +191 -1
  56. package/src/__tests__/context-window-manager.test.ts +530 -2
  57. package/src/__tests__/conversation-abort-tool-results.test.ts +30 -16
  58. package/src/__tests__/conversation-agent-loop-overflow.test.ts +61 -17
  59. package/src/__tests__/conversation-agent-loop.test.ts +412 -82
  60. package/src/__tests__/conversation-attachments.test.ts +1 -1
  61. package/src/__tests__/conversation-confirmation-signals.test.ts +30 -9
  62. package/src/__tests__/conversation-error.test.ts +37 -6
  63. package/src/__tests__/conversation-history-web-search.test.ts +6 -0
  64. package/src/__tests__/conversation-init.benchmark.test.ts +36 -0
  65. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  66. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  67. package/src/__tests__/conversation-pre-run-repair.test.ts +30 -16
  68. package/src/__tests__/conversation-process-callsite.test.ts +306 -0
  69. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -16
  70. package/src/__tests__/conversation-queue.test.ts +41 -26
  71. package/src/__tests__/conversation-routes-disk-view.test.ts +29 -1
  72. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  73. package/src/__tests__/conversation-runtime-assembly.test.ts +2735 -55
  74. package/src/__tests__/conversation-runtime-workspace.test.ts +12 -12
  75. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  76. package/src/__tests__/conversation-slash-queue.test.ts +34 -19
  77. package/src/__tests__/conversation-slash-unknown.test.ts +30 -16
  78. package/src/__tests__/conversation-speed-override.test.ts +30 -11
  79. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  80. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  81. package/src/__tests__/conversation-title-service.test.ts +2 -2
  82. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  83. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  84. package/src/__tests__/conversation-usage.test.ts +3 -1
  85. package/src/__tests__/conversation-workspace-cache-state.test.ts +31 -10
  86. package/src/__tests__/conversation-workspace-injection.test.ts +43 -15
  87. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +44 -16
  88. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  89. package/src/__tests__/credential-security-invariants.test.ts +3 -0
  90. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  91. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  92. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  93. package/src/__tests__/credentials-cli.test.ts +1 -9
  94. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  95. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  96. package/src/__tests__/delete-propagation.test.ts +437 -0
  97. package/src/__tests__/dm-backfill.test.ts +417 -0
  98. package/src/__tests__/dm-persistence.test.ts +227 -0
  99. package/src/__tests__/edit-propagation.test.ts +280 -0
  100. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  101. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  102. package/src/__tests__/estimator-calibration.test.ts +213 -0
  103. package/src/__tests__/extension-id-sync-guard.test.ts +26 -7
  104. package/src/__tests__/file-write-tool.test.ts +151 -1
  105. package/src/__tests__/filing-service.test.ts +255 -0
  106. package/src/__tests__/gemini-provider.test.ts +0 -3
  107. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  108. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  109. package/src/__tests__/heartbeat-service.test.ts +96 -15
  110. package/src/__tests__/host-shell-tool.test.ts +124 -18
  111. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  112. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  113. package/src/__tests__/intent-routing.test.ts +1 -40
  114. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  115. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  116. package/src/__tests__/llm-resolver.test.ts +214 -0
  117. package/src/__tests__/llm-schema.test.ts +223 -0
  118. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  119. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  120. package/src/__tests__/migration-import-from-url.test.ts +684 -0
  121. package/src/__tests__/model-intents.test.ts +9 -83
  122. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  123. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  124. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  125. package/src/__tests__/oauth-store.test.ts +10 -7
  126. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  127. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  128. package/src/__tests__/openai-provider.test.ts +7 -0
  129. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  130. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  131. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  132. package/src/__tests__/permission-checker-host-gate.test.ts +1 -1
  133. package/src/__tests__/permission-mode.test.ts +16 -0
  134. package/src/__tests__/permission-types.test.ts +0 -1
  135. package/src/__tests__/persona-resolver.test.ts +13 -13
  136. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  137. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  138. package/src/__tests__/pricing.test.ts +50 -3
  139. package/src/__tests__/profiler-routes.test.ts +1 -1
  140. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  141. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  142. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  143. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  144. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  145. package/src/__tests__/proxy-approval-callback.test.ts +0 -1
  146. package/src/__tests__/reaction-persistence.test.ts +560 -0
  147. package/src/__tests__/relay-server.test.ts +1 -1
  148. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  149. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  150. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  151. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  152. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  153. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  154. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  155. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  156. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  157. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  158. package/src/__tests__/server-history-render.test.ts +31 -0
  159. package/src/__tests__/shell-parser-property.test.ts +13 -13
  160. package/src/__tests__/skill-cache-store.test.ts +182 -0
  161. package/src/__tests__/skills.test.ts +19 -33
  162. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  163. package/src/__tests__/slack-skill.test.ts +3 -8
  164. package/src/__tests__/starter-bundle.test.ts +35 -0
  165. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  166. package/src/__tests__/suggestion-routes.test.ts +160 -3
  167. package/src/__tests__/system-prompt.test.ts +22 -35
  168. package/src/__tests__/task-runner.test.ts +3 -1
  169. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  170. package/src/__tests__/terminal-tools.test.ts +8 -0
  171. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  172. package/src/__tests__/thread-backfill.test.ts +941 -0
  173. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -2
  174. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  175. package/src/__tests__/tool-executor.test.ts +60 -94
  176. package/src/__tests__/trust-store.test.ts +442 -109
  177. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  178. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  179. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  180. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  181. package/src/__tests__/volume-security-guard.test.ts +3 -2
  182. package/src/__tests__/web-search-history.test.ts +337 -0
  183. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  184. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  185. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  186. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  187. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  188. package/src/__tests__/workspace-policy.test.ts +1 -13
  189. package/src/acp/client-handler.ts +1 -2
  190. package/src/agent/loop.ts +209 -17
  191. package/src/avatar/resvg-lazy.test.ts +136 -0
  192. package/src/avatar/resvg-lazy.ts +82 -9
  193. package/src/avatar/traits-png-sync.ts +21 -1
  194. package/src/browser/__tests__/operations.test.ts +163 -0
  195. package/src/browser/identifiers.ts +51 -0
  196. package/src/browser/operations.ts +660 -0
  197. package/src/browser/types.ts +81 -0
  198. package/src/calls/guardian-question-copy.ts +2 -2
  199. package/src/calls/telephony-stt-routing.ts +1 -1
  200. package/src/calls/voice-session-bridge.ts +1 -0
  201. package/src/cli/AGENTS.md +1 -1
  202. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  203. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  204. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  205. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  206. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  207. package/src/cli/commands/__tests__/image-generation.test.ts +666 -0
  208. package/src/cli/commands/__tests__/inference-send.test.ts +451 -0
  209. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  210. package/src/cli/commands/__tests__/task.test.ts +913 -0
  211. package/src/cli/commands/__tests__/tts-synthesize.test.ts +594 -0
  212. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  213. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  214. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  215. package/src/cli/commands/attachment.ts +182 -0
  216. package/src/cli/commands/browser.ts +350 -0
  217. package/src/cli/commands/cache.ts +341 -0
  218. package/src/cli/commands/completions.ts +0 -3
  219. package/src/cli/commands/config.ts +6 -6
  220. package/src/cli/commands/conversations-import.ts +347 -0
  221. package/src/cli/commands/conversations.ts +14 -1
  222. package/src/cli/commands/email.ts +234 -194
  223. package/src/cli/commands/image-generation.ts +300 -0
  224. package/src/cli/commands/inference.ts +200 -0
  225. package/src/cli/commands/memory.ts +127 -17
  226. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  227. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  228. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  229. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  230. package/src/cli/commands/stt.ts +339 -0
  231. package/src/cli/commands/task.ts +795 -0
  232. package/src/cli/commands/trust.ts +50 -19
  233. package/src/cli/commands/tts.ts +273 -0
  234. package/src/cli/commands/ui.ts +670 -0
  235. package/src/cli/commands/watchers.ts +509 -0
  236. package/src/cli/lib/daemon-credential-client.ts +0 -19
  237. package/src/cli/program.ts +23 -4
  238. package/src/cli.ts +0 -37
  239. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +23 -1
  240. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  241. package/src/config/bundled-skills/messaging/SKILL.md +2 -2
  242. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  243. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +8 -1
  244. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  245. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  246. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +11 -12
  247. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  248. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  249. package/src/config/bundled-tool-registry.ts +0 -175
  250. package/src/config/env.ts +7 -2
  251. package/src/config/feature-flag-registry.json +25 -9
  252. package/src/config/llm-resolver.ts +128 -0
  253. package/src/config/loader.ts +194 -10
  254. package/src/config/raw-config-utils.ts +30 -2
  255. package/src/config/sanitize-for-transfer.ts +35 -0
  256. package/src/config/schema.ts +30 -41
  257. package/src/config/schemas/analysis.ts +3 -22
  258. package/src/config/schemas/calls.ts +0 -4
  259. package/src/config/schemas/filing.ts +2 -7
  260. package/src/config/schemas/heartbeat.ts +0 -5
  261. package/src/config/schemas/inference.ts +3 -23
  262. package/src/config/schemas/llm.ts +318 -0
  263. package/src/config/schemas/memory-processing.ts +1 -9
  264. package/src/config/schemas/notifications.ts +4 -11
  265. package/src/config/schemas/platform.ts +3 -9
  266. package/src/config/schemas/security.ts +33 -0
  267. package/src/config/schemas/services.ts +9 -4
  268. package/src/config/schemas/stt.ts +1 -0
  269. package/src/config/schemas/tts.ts +53 -0
  270. package/src/config/schemas/updates.ts +1 -1
  271. package/src/config/schemas/workspace-git.ts +3 -40
  272. package/src/config/skills.ts +2 -2
  273. package/src/context/__tests__/compact-prompt.test.ts +45 -0
  274. package/src/context/__tests__/microcompact.test.ts +805 -0
  275. package/src/context/estimator-calibration.ts +136 -0
  276. package/src/context/microcompact.ts +443 -0
  277. package/src/context/prompts/compact.md +12 -0
  278. package/src/context/token-estimator.ts +61 -3
  279. package/src/context/window-manager.ts +229 -25
  280. package/src/credential-execution/approval-bridge.ts +0 -1
  281. package/src/credential-execution/executable-discovery.ts +19 -8
  282. package/src/credential-execution/process-manager.test.ts +109 -0
  283. package/src/credential-execution/process-manager.ts +65 -2
  284. package/src/daemon/approval-generators.ts +29 -4
  285. package/src/daemon/assistant-attachments.ts +24 -13
  286. package/src/daemon/classifier.ts +2 -2
  287. package/src/daemon/config-watcher.ts +0 -1
  288. package/src/daemon/context-overflow-reducer.ts +4 -1
  289. package/src/daemon/conversation-agent-loop-handlers.ts +79 -12
  290. package/src/daemon/conversation-agent-loop.ts +462 -80
  291. package/src/daemon/conversation-attachments.ts +2 -6
  292. package/src/daemon/conversation-error.ts +36 -1
  293. package/src/daemon/conversation-lifecycle.ts +30 -6
  294. package/src/daemon/conversation-messaging.ts +73 -4
  295. package/src/daemon/conversation-process.ts +10 -4
  296. package/src/daemon/conversation-queue-manager.ts +3 -0
  297. package/src/daemon/conversation-runtime-assembly.ts +760 -29
  298. package/src/daemon/conversation-slash.ts +2 -2
  299. package/src/daemon/conversation-surfaces.ts +389 -1
  300. package/src/daemon/conversation-tool-setup.ts +10 -5
  301. package/src/daemon/conversation-usage.ts +1 -1
  302. package/src/daemon/conversation.ts +118 -30
  303. package/src/daemon/external-skills-bootstrap.ts +41 -0
  304. package/src/daemon/guardian-action-generators.ts +34 -14
  305. package/src/daemon/handlers/config-model.test.ts +86 -0
  306. package/src/daemon/handlers/config-model.ts +54 -12
  307. package/src/daemon/handlers/conversations.ts +9 -2
  308. package/src/daemon/handlers/shared.ts +39 -11
  309. package/src/daemon/handlers/skills.ts +2 -2
  310. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  311. package/src/daemon/lifecycle.ts +76 -14
  312. package/src/daemon/message-types/conversations.ts +14 -0
  313. package/src/daemon/message-types/messages.ts +9 -1
  314. package/src/daemon/message-types/trust.ts +0 -2
  315. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  316. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  317. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  318. package/src/daemon/pkb-context-tracker.ts +125 -0
  319. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  320. package/src/daemon/pkb-reminder-builder.ts +31 -0
  321. package/src/daemon/providers-setup.ts +6 -0
  322. package/src/daemon/server.ts +117 -9
  323. package/src/daemon/tool-side-effects.ts +0 -9
  324. package/src/daemon/watch-handler.ts +4 -4
  325. package/src/daemon/web-search-history.ts +126 -0
  326. package/src/events/domain-events.ts +0 -1
  327. package/src/filing/filing-service.ts +9 -10
  328. package/src/heartbeat/heartbeat-service.ts +76 -28
  329. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  330. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  331. package/src/home/assistant-feed-authoring.ts +4 -0
  332. package/src/home/emit-feed-event.ts +4 -0
  333. package/src/home/feed-scheduler.ts +20 -4
  334. package/src/home/feed-types.ts +56 -2
  335. package/src/home/relationship-state-writer.ts +2 -2
  336. package/src/home/rollup-producer.ts +34 -5
  337. package/src/home/suggested-prompts.ts +101 -0
  338. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  339. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  340. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  341. package/src/ipc/__tests__/socket-path.test.ts +73 -0
  342. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  343. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  344. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  345. package/src/ipc/cli-client.ts +2 -1
  346. package/src/ipc/cli-server.ts +26 -8
  347. package/src/ipc/gateway-client.ts +4 -4
  348. package/src/ipc/routes/attachment.ts +114 -0
  349. package/src/ipc/routes/browser-context.ts +61 -0
  350. package/src/ipc/routes/browser.ts +96 -0
  351. package/src/ipc/routes/cache.ts +96 -0
  352. package/src/ipc/routes/index.ts +17 -1
  353. package/src/ipc/routes/task-queue.ts +226 -0
  354. package/src/ipc/routes/task.ts +173 -0
  355. package/src/ipc/routes/ui-request.ts +50 -0
  356. package/src/ipc/routes/watcher.ts +203 -0
  357. package/src/ipc/socket-path.ts +100 -0
  358. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  359. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  360. package/src/memory/admin.ts +18 -0
  361. package/src/memory/conversation-analyze-job.ts +14 -13
  362. package/src/memory/conversation-attention-store.ts +13 -6
  363. package/src/memory/conversation-crud.ts +103 -3
  364. package/src/memory/conversation-group-migration.ts +38 -6
  365. package/src/memory/conversation-title-service.ts +7 -4
  366. package/src/memory/db-init.ts +2 -0
  367. package/src/memory/embedding-backend.ts +1 -1
  368. package/src/memory/graph/compaction.ts +299 -0
  369. package/src/memory/graph/consolidation.ts +4 -4
  370. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  371. package/src/memory/graph/extraction.test.ts +272 -2
  372. package/src/memory/graph/extraction.ts +173 -51
  373. package/src/memory/graph/graph-search.test.ts +92 -0
  374. package/src/memory/graph/graph-search.ts +4 -1
  375. package/src/memory/graph/narrative.ts +2 -2
  376. package/src/memory/graph/pattern-scan.ts +2 -2
  377. package/src/memory/graph/retriever.test.ts +459 -0
  378. package/src/memory/graph/retriever.ts +230 -48
  379. package/src/memory/graph/store.ts +41 -0
  380. package/src/memory/graph/tool-handlers.ts +27 -0
  381. package/src/memory/graph/tools.ts +6 -1
  382. package/src/memory/indexer.ts +5 -5
  383. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  384. package/src/memory/job-handlers/summarization.ts +2 -2
  385. package/src/memory/job-utils.ts +7 -1
  386. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  387. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  388. package/src/memory/jobs-store.ts +44 -3
  389. package/src/memory/jobs-worker.ts +4 -0
  390. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  391. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  392. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  393. package/src/memory/migrations/index.ts +1 -0
  394. package/src/memory/pkb/pkb-index.test.ts +368 -0
  395. package/src/memory/pkb/pkb-index.ts +255 -0
  396. package/src/memory/pkb/pkb-reconcile.test.ts +251 -0
  397. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  398. package/src/memory/pkb/pkb-search.test.ts +438 -0
  399. package/src/memory/pkb/pkb-search.ts +137 -0
  400. package/src/memory/pkb/types.ts +53 -0
  401. package/src/memory/qdrant-client.ts +122 -1
  402. package/src/memory/slack-thread-store.ts +37 -0
  403. package/src/messaging/providers/gmail/adapter.ts +6 -16
  404. package/src/messaging/providers/gmail/client.ts +22 -0
  405. package/src/messaging/providers/gmail/types.ts +7 -0
  406. package/src/messaging/providers/slack/adapter.ts +14 -2
  407. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  408. package/src/messaging/providers/slack/backfill.ts +101 -0
  409. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  410. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  411. package/src/messaging/providers/slack/render-transcript.test.ts +1373 -0
  412. package/src/messaging/providers/slack/render-transcript.ts +443 -0
  413. package/src/messaging/style-analyzer.ts +5 -2
  414. package/src/notifications/README.md +9 -5
  415. package/src/notifications/decision-engine.ts +3 -9
  416. package/src/notifications/preference-extractor.ts +2 -6
  417. package/src/oauth/oauth-store.ts +1 -0
  418. package/src/oauth/platform-connection.test.ts +47 -0
  419. package/src/oauth/platform-connection.ts +15 -5
  420. package/src/oauth/seed-providers.ts +4 -2
  421. package/src/permissions/approval-policy.test.ts +948 -0
  422. package/src/permissions/approval-policy.ts +257 -0
  423. package/src/permissions/bash-risk-classifier.test.ts +1208 -0
  424. package/src/permissions/bash-risk-classifier.ts +707 -0
  425. package/src/permissions/checker.ts +217 -708
  426. package/src/permissions/command-registry.test.ts +535 -0
  427. package/src/permissions/command-registry.ts +825 -0
  428. package/src/permissions/defaults.ts +26 -78
  429. package/src/permissions/file-risk-classifier.test.ts +535 -0
  430. package/src/permissions/file-risk-classifier.ts +274 -0
  431. package/src/permissions/risk-types.ts +205 -0
  432. package/src/permissions/secret-prompter.ts +53 -2
  433. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  434. package/src/permissions/skill-risk-classifier.ts +214 -0
  435. package/src/permissions/trust-client.ts +52 -25
  436. package/src/permissions/trust-store-interface.ts +1 -6
  437. package/src/permissions/trust-store.ts +161 -62
  438. package/src/permissions/types.ts +23 -14
  439. package/src/permissions/web-risk-classifier.test.ts +170 -0
  440. package/src/permissions/web-risk-classifier.ts +89 -0
  441. package/src/permissions/workspace-policy.ts +1 -16
  442. package/src/platform/client.ts +19 -1
  443. package/src/prompts/persona-resolver.ts +3 -3
  444. package/src/prompts/system-prompt.ts +19 -20
  445. package/src/prompts/templates/SOUL.md +2 -2
  446. package/src/prompts/update-bulletin-job.ts +190 -0
  447. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  448. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  449. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  450. package/src/providers/anthropic/client.ts +183 -14
  451. package/src/providers/call-site-routing.ts +71 -0
  452. package/src/providers/gemini/client.ts +65 -2
  453. package/src/providers/managed-proxy/constants.ts +2 -1
  454. package/src/providers/model-catalog.ts +501 -33
  455. package/src/providers/model-intents.ts +4 -4
  456. package/src/providers/openai/chat-completions-provider.ts +57 -1
  457. package/src/providers/openai/responses-provider.ts +86 -9
  458. package/src/providers/openrouter/client.ts +76 -9
  459. package/src/providers/provider-env-vars.ts +56 -0
  460. package/src/providers/provider-send-message.ts +22 -5
  461. package/src/providers/ratelimit.ts +4 -0
  462. package/src/providers/registry.ts +19 -8
  463. package/src/providers/retry.ts +174 -39
  464. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  465. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  466. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  467. package/src/providers/speech-to-text/resolve.ts +7 -0
  468. package/src/providers/speech-to-text/xai-realtime.test.ts +578 -0
  469. package/src/providers/speech-to-text/xai-realtime.ts +796 -0
  470. package/src/providers/speech-to-text/xai.test.ts +155 -0
  471. package/src/providers/speech-to-text/xai.ts +97 -0
  472. package/src/providers/types.ts +93 -3
  473. package/src/runtime/AGENTS.md +2 -2
  474. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  475. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  476. package/src/runtime/agent-wake.ts +63 -22
  477. package/src/runtime/auth/route-policy.ts +4 -0
  478. package/src/runtime/btw-sidechain.ts +13 -3
  479. package/src/runtime/channel-reply-delivery.ts +106 -2
  480. package/src/runtime/decision-token.ts +116 -0
  481. package/src/runtime/gateway-client.ts +2 -2
  482. package/src/runtime/http-router.ts +32 -0
  483. package/src/runtime/http-server.ts +52 -1
  484. package/src/runtime/http-types.ts +23 -1
  485. package/src/runtime/interactive-ui.ts +362 -0
  486. package/src/runtime/invite-instruction-generator.ts +2 -2
  487. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  488. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  489. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  490. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  491. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  492. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  493. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  494. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  495. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  496. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  497. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  498. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  499. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  500. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  501. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  502. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  503. package/src/runtime/routes/approval-prompt-ts-tracker.ts +58 -0
  504. package/src/runtime/routes/approval-routes.ts +12 -17
  505. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  506. package/src/runtime/routes/avatar-routes.ts +20 -4
  507. package/src/runtime/routes/btw-routes.ts +1 -4
  508. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  509. package/src/runtime/routes/conversation-routes.ts +133 -27
  510. package/src/runtime/routes/debug-routes.ts +1 -1
  511. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  512. package/src/runtime/routes/events-routes.ts +16 -0
  513. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  514. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  515. package/src/runtime/routes/home-feed-routes.ts +120 -2
  516. package/src/runtime/routes/inbound-message-handler.ts +912 -2
  517. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  518. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  519. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  520. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  521. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  522. package/src/runtime/routes/migration-routes.ts +720 -124
  523. package/src/runtime/routes/settings-routes.ts +4 -2
  524. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  525. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  526. package/src/runtime/routes/work-items-routes.ts +3 -2
  527. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  528. package/src/runtime/services/analyze-conversation.ts +12 -16
  529. package/src/runtime/skill-route-registry.ts +28 -6
  530. package/src/schedule/scheduler.ts +8 -0
  531. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  532. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  533. package/src/security/oauth2.ts +98 -35
  534. package/src/security/secure-keys.ts +7 -8
  535. package/src/security/token-manager.ts +27 -13
  536. package/src/security/untrusted-content.ts +102 -0
  537. package/src/skills/catalog-cache.ts +26 -7
  538. package/src/skills/catalog-install.ts +31 -3
  539. package/src/skills/skill-cache-store.ts +97 -0
  540. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  541. package/src/stt/daemon-batch-transcriber.ts +33 -0
  542. package/src/stt/stt-stream-session.ts +8 -1
  543. package/src/stt/types.ts +5 -1
  544. package/src/subagent/manager.ts +41 -13
  545. package/src/tasks/ephemeral-permissions.ts +9 -4
  546. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  547. package/src/tools/browser/__tests__/browser-status.test.ts +45 -2
  548. package/src/tools/browser/browser-execution.ts +65 -38
  549. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  550. package/src/tools/credentials/tool-policy.ts +39 -5
  551. package/src/tools/credentials/vault.ts +9 -4
  552. package/src/tools/executor.ts +4 -0
  553. package/src/tools/filesystem/write.ts +52 -0
  554. package/src/tools/host-terminal/host-shell.ts +45 -5
  555. package/src/tools/memory/register.test.ts +185 -0
  556. package/src/tools/memory/register.ts +3 -1
  557. package/src/tools/network/web-fetch.ts +20 -10
  558. package/src/tools/network/web-search.ts +19 -4
  559. package/src/tools/permission-checker.ts +36 -15
  560. package/src/tools/policy-context.ts +25 -8
  561. package/src/tools/registry.ts +55 -3
  562. package/src/tools/side-effects.ts +0 -11
  563. package/src/tools/skills/execute.ts +2 -2
  564. package/src/tools/skills/sandbox-runner.ts +5 -2
  565. package/src/tools/terminal/backends/native.ts +51 -2
  566. package/src/tools/terminal/safe-env.ts +3 -2
  567. package/src/tools/terminal/shell.ts +1 -0
  568. package/src/tools/tool-manifest.ts +6 -21
  569. package/src/tools/types.ts +12 -3
  570. package/src/tools/verification-control-plane-policy.ts +1 -1
  571. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  572. package/src/tts/provider-catalog.ts +18 -0
  573. package/src/tts/providers/index.ts +2 -0
  574. package/src/tts/providers/xai-provider.ts +224 -0
  575. package/src/tts/types.ts +46 -0
  576. package/src/types/tar-stream.d.ts +66 -0
  577. package/src/util/json.ts +17 -0
  578. package/src/util/platform.ts +2 -2
  579. package/src/util/pricing.ts +15 -5
  580. package/src/watcher/engine.ts +1 -1
  581. package/src/watcher/providers/google-calendar.ts +134 -8
  582. package/src/watcher/providers/outlook-calendar.ts +42 -2
  583. package/src/workspace/git-service.ts +23 -4
  584. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  585. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  586. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  587. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +57 -0
  588. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  589. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  590. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  591. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  592. package/src/workspace/migrations/AGENTS.md +1 -1
  593. package/src/workspace/migrations/registry.ts +16 -0
  594. package/src/workspace/provider-commit-message-generator.ts +19 -38
  595. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  596. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  597. package/src/__tests__/gmail-preferences.test.ts +0 -117
  598. package/src/__tests__/outlook-attachments.test.ts +0 -301
  599. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  600. package/src/__tests__/outlook-categories.test.ts +0 -212
  601. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  602. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  603. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  604. package/src/__tests__/outlook-trash.test.ts +0 -77
  605. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  606. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  607. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  608. package/src/__tests__/update-bulletin.test.ts +0 -478
  609. package/src/__tests__/update-template-contract.test.ts +0 -29
  610. package/src/cli/commands/doctor.ts +0 -341
  611. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  612. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  613. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  614. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  615. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  616. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  617. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  618. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  619. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  620. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  621. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  622. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  623. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  624. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  625. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  626. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  627. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  628. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  629. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  630. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  631. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  632. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  633. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  634. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  635. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  636. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  637. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  638. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  639. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  640. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  641. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  642. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  643. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  644. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  645. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  646. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  647. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  648. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  649. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  650. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  651. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  652. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  653. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  654. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  655. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  656. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  657. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  658. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  659. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  660. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  661. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  662. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  663. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  664. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  665. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  666. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  667. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  668. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  669. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  670. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  671. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  672. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  673. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  674. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  675. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  676. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  677. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  678. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  679. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  680. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  681. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  682. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  683. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  684. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  685. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  686. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  687. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  688. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  689. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  690. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  691. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  692. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  693. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  694. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  695. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  696. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  697. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  698. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  699. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  700. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  701. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  702. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  703. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  704. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  705. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  706. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  707. package/src/prompts/templates/UPDATES.md +0 -50
  708. package/src/prompts/update-bulletin-format.ts +0 -85
  709. package/src/prompts/update-bulletin-state.ts +0 -58
  710. package/src/prompts/update-bulletin-template-path.ts +0 -13
  711. package/src/prompts/update-bulletin.ts +0 -139
  712. package/src/shared/provider-env-vars.ts +0 -19
  713. package/src/tools/watcher/create.ts +0 -86
  714. package/src/tools/watcher/delete.ts +0 -36
  715. package/src/tools/watcher/digest.ts +0 -54
  716. package/src/tools/watcher/list.ts +0 -83
  717. package/src/tools/watcher/update.ts +0 -71
@@ -0,0 +1,941 @@
1
+ /**
2
+ * PR 22 — verifies that an inbound Slack thread reply triggers a lazy
3
+ * backfill of the missing thread ancestors when the conversation has no
4
+ * record of the parent message, persists each backfilled message with a
5
+ * derived `slackMeta` envelope, de-dupes against rows already stored, and
6
+ * gates re-triggers behind a 10-minute idempotency cache so bursts of
7
+ * replies in the same thread do not flood the Slack API.
8
+ *
9
+ * Tests exercise the helper {@link triggerSlackThreadBackfillIfNeeded}
10
+ * directly against the real database (via the test-preload temp workspace).
11
+ * Only `backfillThread` is mocked, since the contract under test is "given
12
+ * what Slack returns, what does the daemon write to the DB".
13
+ */
14
+ import {
15
+ afterAll,
16
+ afterEach,
17
+ beforeEach,
18
+ describe,
19
+ expect,
20
+ mock,
21
+ spyOn,
22
+ test,
23
+ } from "bun:test";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Mocks (must precede module imports under test). Note: backfillThread is
27
+ // stubbed via spyOn (below) rather than mock.module so the stub does not leak
28
+ // into other test files (e.g. backfill.test.ts) that import the same module.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ mock.module("../util/logger.js", () => ({
32
+ getLogger: () =>
33
+ new Proxy({} as Record<string, unknown>, {
34
+ get: () => () => {},
35
+ }),
36
+ }));
37
+
38
+ mock.module("../config/env.js", () => ({
39
+ isHttpAuthDisabled: () => true,
40
+ getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
41
+ }));
42
+
43
+ mock.module("../tools/credentials/metadata-store.js", () => ({
44
+ getCredentialMetadata: () => undefined,
45
+ upsertCredentialMetadata: () => {},
46
+ deleteCredentialMetadata: () => {},
47
+ listCredentialMetadata: () => [],
48
+ }));
49
+
50
+ mock.module("../runtime/gateway-client.js", () => ({
51
+ deliverChannelReply: async () => {},
52
+ }));
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Imports (after mocks)
56
+ // ---------------------------------------------------------------------------
57
+
58
+ import { v4 as uuid } from "uuid";
59
+
60
+ import { upsertContactChannel } from "../contacts/contacts-write.js";
61
+ import { getDb, initializeDb } from "../memory/db.js";
62
+ import type { Message as MessagingMessage } from "../messaging/provider-types.js";
63
+ import * as slackBackfill from "../messaging/providers/slack/backfill.js";
64
+ import {
65
+ readSlackMetadata,
66
+ writeSlackMetadata,
67
+ } from "../messaging/providers/slack/message-metadata.js";
68
+ import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
69
+ import {
70
+ _backfillTriggerCache,
71
+ triggerSlackThreadBackfillIfNeeded,
72
+ } from "../runtime/routes/inbound-message-handler.js";
73
+
74
+ initializeDb();
75
+
76
+ // Spy on backfillThread so the stub is scoped to this test file only.
77
+ // Restoring after the file's tests run keeps cross-file leakage to zero —
78
+ // other tests (e.g. backfill.test.ts) keep seeing the real implementation.
79
+ const backfillThreadMock = spyOn(slackBackfill, "backfillThread");
80
+ backfillThreadMock.mockResolvedValue([]);
81
+
82
+ afterAll(() => {
83
+ backfillThreadMock.mockRestore();
84
+ });
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Helpers
88
+ // ---------------------------------------------------------------------------
89
+ //
90
+ // These helpers go directly against the SQLite layer rather than calling into
91
+ // `conversation-crud.js`. The reason is test isolation: several other test
92
+ // files in the suite mock `conversation-crud.js` partially (only the exports
93
+ // they need), and Bun does not always reset such mocks between files. When
94
+ // our test runs in the same process after one of those, calls like
95
+ // `getMessages` come back as `undefined`. Going around the module entirely
96
+ // keeps this test resilient to any future module-level mocks elsewhere.
97
+
98
+ const SLACK_CHANNEL_ID = "C0THREAD";
99
+
100
+ function resetState(): void {
101
+ const db = getDb();
102
+ db.$client.exec("DELETE FROM messages");
103
+ db.$client.exec("DELETE FROM conversations");
104
+ _backfillTriggerCache.clear();
105
+ backfillThreadMock.mockReset();
106
+ backfillThreadMock.mockImplementation(async () => []);
107
+ }
108
+
109
+ let convCounter = 0;
110
+
111
+ function makeConversationId(): string {
112
+ convCounter++;
113
+ return `conv-test-${convCounter}-${uuid()}`;
114
+ }
115
+
116
+ function createTestConversation(): { id: string } {
117
+ const db = getDb();
118
+ const id = makeConversationId();
119
+ const now = Date.now();
120
+ db.$client
121
+ .prepare(
122
+ `INSERT INTO conversations (
123
+ id, title, created_at, updated_at, total_input_tokens, total_output_tokens,
124
+ total_estimated_cost, context_compacted_message_count, conversation_type,
125
+ source, memory_scope_id, host_access, is_auto_title
126
+ ) VALUES (?, NULL, ?, ?, 0, 0, 0, 0, 'standard', 'user', 'default', 0, 1)`,
127
+ )
128
+ .run(id, now, now);
129
+ return { id };
130
+ }
131
+
132
+ let messageCounter = 0;
133
+
134
+ function insertMessage(
135
+ conversationId: string,
136
+ role: string,
137
+ content: string,
138
+ metadata?: Record<string, unknown>,
139
+ ): void {
140
+ const db = getDb();
141
+ const id = uuid();
142
+ // Use a strictly increasing timestamp so the ORDER BY in
143
+ // readMessagesByConversation is deterministic — Date.now() ties when
144
+ // multiple inserts happen inside the same millisecond.
145
+ messageCounter++;
146
+ const now = Date.now() + messageCounter;
147
+ const metadataStr = metadata ? JSON.stringify(metadata) : null;
148
+ db.$client
149
+ .prepare(
150
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
151
+ VALUES (?, ?, ?, ?, ?, ?)`,
152
+ )
153
+ .run(id, conversationId, role, content, now, metadataStr);
154
+ }
155
+
156
+ interface RawMessageRow {
157
+ role: string;
158
+ content: string;
159
+ metadata: string | null;
160
+ }
161
+
162
+ function readMessagesByConversation(conversationId: string): RawMessageRow[] {
163
+ const db = getDb();
164
+ return db.$client
165
+ .prepare(
166
+ "SELECT role, content, metadata FROM messages WHERE conversation_id = ? ORDER BY created_at ASC",
167
+ )
168
+ .all(conversationId) as RawMessageRow[];
169
+ }
170
+
171
+ function makeBackfillMessage(
172
+ overrides: Partial<MessagingMessage> = {},
173
+ ): MessagingMessage {
174
+ return {
175
+ id: "1234.0",
176
+ conversationId: SLACK_CHANNEL_ID,
177
+ sender: { id: "U_USER", name: "Alice" },
178
+ text: "thread parent",
179
+ timestamp: 1700000000_000,
180
+ threadId: undefined,
181
+ platform: "slack",
182
+ ...overrides,
183
+ };
184
+ }
185
+
186
+ interface PersistedRow {
187
+ role: string;
188
+ content: string;
189
+ channelTs: string | undefined;
190
+ threadTs: string | undefined;
191
+ displayName: string | undefined;
192
+ }
193
+
194
+ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
195
+ const rows = readMessagesByConversation(conversationId);
196
+ const out: PersistedRow[] = [];
197
+ for (const row of rows) {
198
+ const blank: PersistedRow = {
199
+ role: row.role,
200
+ content: row.content,
201
+ channelTs: undefined,
202
+ threadTs: undefined,
203
+ displayName: undefined,
204
+ };
205
+ if (!row.metadata) {
206
+ out.push(blank);
207
+ continue;
208
+ }
209
+ let envelope: Record<string, unknown>;
210
+ try {
211
+ const parsed = JSON.parse(row.metadata) as unknown;
212
+ if (
213
+ parsed === null ||
214
+ typeof parsed !== "object" ||
215
+ Array.isArray(parsed)
216
+ ) {
217
+ out.push(blank);
218
+ continue;
219
+ }
220
+ envelope = parsed as Record<string, unknown>;
221
+ } catch {
222
+ out.push(blank);
223
+ continue;
224
+ }
225
+ const slackMetaRaw = envelope.slackMeta;
226
+ if (typeof slackMetaRaw !== "string") {
227
+ out.push(blank);
228
+ continue;
229
+ }
230
+ const slackMeta = readSlackMetadata(slackMetaRaw);
231
+ out.push({
232
+ role: row.role,
233
+ content: row.content,
234
+ channelTs: slackMeta?.channelTs,
235
+ threadTs: slackMeta?.threadTs,
236
+ displayName: slackMeta?.displayName,
237
+ });
238
+ }
239
+ return out;
240
+ }
241
+
242
+ function seedSlackRow(
243
+ conversationId: string,
244
+ channelTs: string,
245
+ threadTs: string | undefined,
246
+ text: string,
247
+ ): void {
248
+ insertMessage(conversationId, "user", text, {
249
+ slackMeta: writeSlackMetadata({
250
+ source: "slack",
251
+ channelId: SLACK_CHANNEL_ID,
252
+ channelTs,
253
+ eventKind: "message",
254
+ ...(threadTs ? { threadTs } : {}),
255
+ }),
256
+ });
257
+ }
258
+
259
+ // ---------------------------------------------------------------------------
260
+ // Tests
261
+ // ---------------------------------------------------------------------------
262
+
263
+ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence", () => {
264
+ beforeEach(() => {
265
+ resetState();
266
+ });
267
+
268
+ afterEach(() => {
269
+ backfillThreadMock.mockReset();
270
+ _backfillTriggerCache.clear();
271
+ });
272
+
273
+ test("inbound thread reply with unseen parent triggers backfill and persists ancestors with slackMeta", async () => {
274
+ const conv = createTestConversation();
275
+
276
+ backfillThreadMock.mockImplementation(async () => [
277
+ makeBackfillMessage({
278
+ id: "1234.0",
279
+ text: "parent",
280
+ threadId: undefined,
281
+ sender: { id: "U_PARENT", name: "Parent User" },
282
+ }),
283
+ makeBackfillMessage({
284
+ id: "1234.1",
285
+ text: "first reply",
286
+ threadId: "1234.0",
287
+ sender: { id: "U_REPLY1", name: "Reply One" },
288
+ }),
289
+ makeBackfillMessage({
290
+ id: "1234.2",
291
+ text: "second reply",
292
+ threadId: "1234.0",
293
+ sender: { id: "U_REPLY2", name: "Reply Two" },
294
+ }),
295
+ ]);
296
+
297
+ await triggerSlackThreadBackfillIfNeeded({
298
+ conversationId: conv.id,
299
+ channelId: SLACK_CHANNEL_ID,
300
+ threadTs: "1234.0",
301
+ });
302
+
303
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
304
+ const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
305
+ expect(calledChannel).toBe(SLACK_CHANNEL_ID);
306
+ expect(calledThread).toBe("1234.0");
307
+
308
+ const persisted = readPersistedSlackRows(conv.id);
309
+ expect(persisted.length).toBe(3);
310
+
311
+ const byChannelTs = new Map(
312
+ persisted.map((p) => [p.channelTs ?? "<no-ts>", p]),
313
+ );
314
+ expect(byChannelTs.get("1234.0")?.content).toBe("parent");
315
+ expect(byChannelTs.get("1234.0")?.displayName).toBe("Parent User");
316
+ expect(byChannelTs.get("1234.0")?.threadTs).toBeUndefined();
317
+
318
+ expect(byChannelTs.get("1234.1")?.content).toBe("first reply");
319
+ expect(byChannelTs.get("1234.1")?.threadTs).toBe("1234.0");
320
+ expect(byChannelTs.get("1234.1")?.displayName).toBe("Reply One");
321
+
322
+ expect(byChannelTs.get("1234.2")?.content).toBe("second reply");
323
+ expect(byChannelTs.get("1234.2")?.threadTs).toBe("1234.0");
324
+ expect(byChannelTs.get("1234.2")?.displayName).toBe("Reply Two");
325
+ });
326
+
327
+ test("backfill is NOT triggered when the parent is already persisted", async () => {
328
+ const conv = createTestConversation();
329
+
330
+ // Seed the parent message before the trigger runs — simulates a
331
+ // conversation where the daemon has already seen the thread parent.
332
+ seedSlackRow(conv.id, "1234.0", undefined, "already here");
333
+
334
+ await triggerSlackThreadBackfillIfNeeded({
335
+ conversationId: conv.id,
336
+ channelId: SLACK_CHANNEL_ID,
337
+ threadTs: "1234.0",
338
+ });
339
+
340
+ expect(backfillThreadMock).not.toHaveBeenCalled();
341
+
342
+ const persisted = readPersistedSlackRows(conv.id);
343
+ expect(persisted.length).toBe(1);
344
+ expect(persisted[0].channelTs).toBe("1234.0");
345
+ });
346
+
347
+ test("idempotency cache: a second call inside the TTL window does not re-fetch", async () => {
348
+ const conv = createTestConversation();
349
+
350
+ backfillThreadMock.mockImplementation(async () => [
351
+ makeBackfillMessage({ id: "1234.0", text: "parent" }),
352
+ ]);
353
+
354
+ await triggerSlackThreadBackfillIfNeeded({
355
+ conversationId: conv.id,
356
+ channelId: SLACK_CHANNEL_ID,
357
+ threadTs: "1234.0",
358
+ });
359
+
360
+ // Second call for the same conversation+thread — must short-circuit on
361
+ // the in-memory cache without hitting backfillThread again.
362
+ await triggerSlackThreadBackfillIfNeeded({
363
+ conversationId: conv.id,
364
+ channelId: SLACK_CHANNEL_ID,
365
+ threadTs: "1234.0",
366
+ });
367
+
368
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
369
+
370
+ const persisted = readPersistedSlackRows(conv.id);
371
+ // Only one parent row (no duplicate from the second trigger).
372
+ expect(persisted.filter((p) => p.channelTs === "1234.0").length).toBe(1);
373
+ });
374
+
375
+ test("backfill error: turn proceeds, no crash, no rows written", async () => {
376
+ const conv = createTestConversation();
377
+
378
+ backfillThreadMock.mockImplementation(async () => {
379
+ throw new Error("Slack API error: thread_not_found");
380
+ });
381
+
382
+ // Must not throw — error handling is internal.
383
+ await triggerSlackThreadBackfillIfNeeded({
384
+ conversationId: conv.id,
385
+ channelId: SLACK_CHANNEL_ID,
386
+ threadTs: "1234.0",
387
+ });
388
+
389
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
390
+ expect(readPersistedSlackRows(conv.id).length).toBe(0);
391
+ });
392
+
393
+ test("backfill returns duplicates that are already stored — only new rows are inserted", async () => {
394
+ const conv = createTestConversation();
395
+
396
+ // Pre-seed sibling 1234.1 so the backfill response includes one row that
397
+ // already exists (and must not be re-inserted) plus two genuinely new
398
+ // ones (parent 1234.0 and sibling 1234.2).
399
+ seedSlackRow(conv.id, "1234.1", "1234.0", "already here");
400
+
401
+ backfillThreadMock.mockImplementation(async () => [
402
+ makeBackfillMessage({
403
+ id: "1234.0",
404
+ text: "parent",
405
+ threadId: undefined,
406
+ }),
407
+ makeBackfillMessage({
408
+ id: "1234.1",
409
+ text: "duplicate sibling — must be skipped",
410
+ threadId: "1234.0",
411
+ }),
412
+ makeBackfillMessage({
413
+ id: "1234.2",
414
+ text: "new sibling",
415
+ threadId: "1234.0",
416
+ }),
417
+ ]);
418
+
419
+ await triggerSlackThreadBackfillIfNeeded({
420
+ conversationId: conv.id,
421
+ channelId: SLACK_CHANNEL_ID,
422
+ threadTs: "1234.0",
423
+ });
424
+
425
+ const persisted = readPersistedSlackRows(conv.id);
426
+ expect(persisted.length).toBe(3);
427
+
428
+ const oneRow = persisted.find((p) => p.channelTs === "1234.1");
429
+ // The pre-seeded row's content remains; the duplicate from backfill was
430
+ // skipped (otherwise the count would be 4 or the content would change).
431
+ expect(oneRow?.content).toBe("already here");
432
+
433
+ expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
434
+ "parent",
435
+ );
436
+ expect(persisted.find((p) => p.channelTs === "1234.2")?.content).toBe(
437
+ "new sibling",
438
+ );
439
+ });
440
+
441
+ test("empty backfill response leaves the conversation untouched but still seeds the cache", async () => {
442
+ const conv = createTestConversation();
443
+
444
+ backfillThreadMock.mockImplementation(async () => []);
445
+
446
+ await triggerSlackThreadBackfillIfNeeded({
447
+ conversationId: conv.id,
448
+ channelId: SLACK_CHANNEL_ID,
449
+ threadTs: "1234.0",
450
+ });
451
+
452
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
453
+ expect(readPersistedSlackRows(conv.id).length).toBe(0);
454
+
455
+ // Cache should now be populated for this conversation+thread, so an
456
+ // immediate retry must not re-run the API call.
457
+ await triggerSlackThreadBackfillIfNeeded({
458
+ conversationId: conv.id,
459
+ channelId: SLACK_CHANNEL_ID,
460
+ threadTs: "1234.0",
461
+ });
462
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
463
+ });
464
+
465
+ test("two distinct threads in the same conversation each trigger their own backfill", async () => {
466
+ const conv = createTestConversation();
467
+
468
+ backfillThreadMock.mockImplementation(async (_channel, threadTs) => [
469
+ makeBackfillMessage({
470
+ id: threadTs as string,
471
+ text: `parent of ${threadTs as string}`,
472
+ }),
473
+ ]);
474
+
475
+ await triggerSlackThreadBackfillIfNeeded({
476
+ conversationId: conv.id,
477
+ channelId: SLACK_CHANNEL_ID,
478
+ threadTs: "1234.0",
479
+ });
480
+ await triggerSlackThreadBackfillIfNeeded({
481
+ conversationId: conv.id,
482
+ channelId: SLACK_CHANNEL_ID,
483
+ threadTs: "5678.0",
484
+ });
485
+
486
+ expect(backfillThreadMock).toHaveBeenCalledTimes(2);
487
+
488
+ const persisted = readPersistedSlackRows(conv.id);
489
+ expect(persisted.length).toBe(2);
490
+ expect(persisted.map((p) => p.channelTs).sort()).toEqual([
491
+ "1234.0",
492
+ "5678.0",
493
+ ]);
494
+ });
495
+
496
+ test("backfilled message without text is persisted with empty content", async () => {
497
+ const conv = createTestConversation();
498
+
499
+ backfillThreadMock.mockImplementation(async () => [
500
+ makeBackfillMessage({
501
+ id: "1234.0",
502
+ text: "",
503
+ }),
504
+ ]);
505
+
506
+ await triggerSlackThreadBackfillIfNeeded({
507
+ conversationId: conv.id,
508
+ channelId: SLACK_CHANNEL_ID,
509
+ threadTs: "1234.0",
510
+ });
511
+
512
+ const persisted = readPersistedSlackRows(conv.id);
513
+ expect(persisted.length).toBe(1);
514
+ expect(persisted[0].content).toBe("");
515
+ expect(persisted[0].channelTs).toBe("1234.0");
516
+ });
517
+
518
+ test("backfill skips messages with no id rather than crashing", async () => {
519
+ const conv = createTestConversation();
520
+
521
+ backfillThreadMock.mockImplementation(async () => [
522
+ makeBackfillMessage({ id: "", text: "no id" }),
523
+ makeBackfillMessage({ id: "1234.0", text: "valid parent" }),
524
+ ]);
525
+
526
+ await triggerSlackThreadBackfillIfNeeded({
527
+ conversationId: conv.id,
528
+ channelId: SLACK_CHANNEL_ID,
529
+ threadTs: "1234.0",
530
+ });
531
+
532
+ const persisted = readPersistedSlackRows(conv.id);
533
+ expect(persisted.length).toBe(1);
534
+ expect(persisted[0].channelTs).toBe("1234.0");
535
+ });
536
+
537
+ test("reaction row targeting the thread parent does not short-circuit ancestor backfill", async () => {
538
+ const conv = createTestConversation();
539
+
540
+ // A reaction on the thread parent stores the parent's ts as `channelTs`
541
+ // (the reaction *targets* that message). If the dedup scan includes
542
+ // reaction rows, ancestor backfill wrongly believes the parent is
543
+ // already persisted and skips the network fetch.
544
+ const db = getDb();
545
+ messageCounter++;
546
+ const now = Date.now() + messageCounter;
547
+ db.$client
548
+ .prepare(
549
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
550
+ VALUES (?, ?, ?, ?, ?, ?)`,
551
+ )
552
+ .run(
553
+ uuid(),
554
+ conv.id,
555
+ "user",
556
+ "+1",
557
+ now,
558
+ JSON.stringify({
559
+ slackMeta: writeSlackMetadata({
560
+ source: "slack",
561
+ channelId: SLACK_CHANNEL_ID,
562
+ channelTs: "1234.0",
563
+ eventKind: "reaction",
564
+ reaction: {
565
+ emoji: "+1",
566
+ targetChannelTs: "1234.0",
567
+ op: "added",
568
+ },
569
+ }),
570
+ }),
571
+ );
572
+
573
+ backfillThreadMock.mockImplementation(async () => [
574
+ makeBackfillMessage({ id: "1234.0", text: "parent" }),
575
+ ]);
576
+
577
+ await triggerSlackThreadBackfillIfNeeded({
578
+ conversationId: conv.id,
579
+ channelId: SLACK_CHANNEL_ID,
580
+ threadTs: "1234.0",
581
+ });
582
+
583
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
584
+ const persisted = readPersistedSlackRows(conv.id);
585
+ // Reaction row + newly backfilled parent message row.
586
+ expect(persisted.length).toBe(2);
587
+ expect(
588
+ persisted.find((p) => p.channelTs === "1234.0" && p.content === "parent"),
589
+ ).toBeDefined();
590
+ });
591
+
592
+ test("excludeChannelTs pre-seeds the dedup set so the inbound message is not re-persisted", async () => {
593
+ const conv = createTestConversation();
594
+
595
+ // Simulate Slack's conversations.replies returning the just-received
596
+ // inbound message alongside the thread parent — this is the normal
597
+ // response shape. Without excludeChannelTs, the inbound row (persisted
598
+ // concurrently in the background) would race the backfill and produce
599
+ // a duplicate.
600
+ backfillThreadMock.mockImplementation(async () => [
601
+ makeBackfillMessage({
602
+ id: "1234.0",
603
+ text: "parent",
604
+ threadId: undefined,
605
+ }),
606
+ makeBackfillMessage({
607
+ id: "1234.5",
608
+ text: "inbound reply — must be skipped",
609
+ threadId: "1234.0",
610
+ }),
611
+ ]);
612
+
613
+ await triggerSlackThreadBackfillIfNeeded({
614
+ conversationId: conv.id,
615
+ channelId: SLACK_CHANNEL_ID,
616
+ threadTs: "1234.0",
617
+ excludeChannelTs: "1234.5",
618
+ });
619
+
620
+ const persisted = readPersistedSlackRows(conv.id);
621
+ // Only the parent should be persisted by backfill; the inbound (1234.5)
622
+ // is owned by the concurrent inbound-processing path.
623
+ expect(persisted.length).toBe(1);
624
+ expect(persisted[0].channelTs).toBe("1234.0");
625
+ expect(persisted.find((p) => p.channelTs === "1234.5")).toBeUndefined();
626
+ });
627
+
628
+ test("messages with malformed metadata in the conversation are tolerated when scanning", async () => {
629
+ const conv = createTestConversation();
630
+
631
+ // Insert a message with malformed (non-JSON) metadata directly. The
632
+ // scan must not throw on parse errors.
633
+ insertMessage(conv.id, "user", "malformed", { foo: "bar" });
634
+ const db = getDb();
635
+ db.$client
636
+ .prepare(
637
+ "UPDATE messages SET metadata = 'not-json' WHERE conversation_id = ?",
638
+ )
639
+ .run(conv.id);
640
+
641
+ backfillThreadMock.mockImplementation(async () => [
642
+ makeBackfillMessage({ id: "1234.0", text: "parent" }),
643
+ ]);
644
+
645
+ await triggerSlackThreadBackfillIfNeeded({
646
+ conversationId: conv.id,
647
+ channelId: SLACK_CHANNEL_ID,
648
+ threadTs: "1234.0",
649
+ });
650
+
651
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
652
+ const persisted = readPersistedSlackRows(conv.id);
653
+ // Two rows: the malformed row + the newly backfilled parent.
654
+ expect(persisted.length).toBe(2);
655
+ expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
656
+ "parent",
657
+ );
658
+ });
659
+ });
660
+
661
+ // ---------------------------------------------------------------------------
662
+ // Integration through handleChannelInbound — the wiring contract
663
+ // ---------------------------------------------------------------------------
664
+
665
+ const TEST_BEARER_TOKEN = "test-token";
666
+ const HTTP_SLACK_CHANNEL_ID = "C0HTTPTHREAD";
667
+ const HTTP_SLACK_USER_ID = "U_HTTP_USER";
668
+ const HTTP_SLACK_DISPLAY_NAME = "Charlie Threader";
669
+
670
+ function resetHttpState(): void {
671
+ const db = getDb();
672
+ db.run("DELETE FROM messages");
673
+ db.run("DELETE FROM channel_inbound_events");
674
+ db.run("DELETE FROM conversations");
675
+ db.run("DELETE FROM contact_channels");
676
+ db.run("DELETE FROM contacts");
677
+ _backfillTriggerCache.clear();
678
+ backfillThreadMock.mockReset();
679
+ backfillThreadMock.mockImplementation(async () => []);
680
+ }
681
+
682
+ function seedHttpActiveMember(): void {
683
+ upsertContactChannel({
684
+ sourceChannel: "slack",
685
+ externalUserId: HTTP_SLACK_USER_ID,
686
+ externalChatId: HTTP_SLACK_CHANNEL_ID,
687
+ status: "active",
688
+ policy: "allow",
689
+ displayName: HTTP_SLACK_DISPLAY_NAME,
690
+ });
691
+ }
692
+
693
+ let httpMsgCounter = 0;
694
+
695
+ function buildThreadReplyRequest(
696
+ threadId: string,
697
+ messageId: string,
698
+ overrides: Record<string, unknown> = {},
699
+ ): Request {
700
+ httpMsgCounter++;
701
+ const body: Record<string, unknown> = {
702
+ sourceChannel: "slack",
703
+ interface: "slack",
704
+ conversationExternalId: HTTP_SLACK_CHANNEL_ID,
705
+ externalMessageId: `${HTTP_SLACK_CHANNEL_ID}:${messageId}:${httpMsgCounter}`,
706
+ content: "thread reply text",
707
+ actorExternalId: HTTP_SLACK_USER_ID,
708
+ actorDisplayName: HTTP_SLACK_DISPLAY_NAME,
709
+ actorUsername: "charlie",
710
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
711
+ sourceMetadata: {
712
+ messageId,
713
+ threadId,
714
+ chatType: "channel",
715
+ },
716
+ ...overrides,
717
+ };
718
+
719
+ return new Request("http://localhost:8080/channels/inbound", {
720
+ method: "POST",
721
+ headers: {
722
+ "Content-Type": "application/json",
723
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
724
+ },
725
+ body: JSON.stringify(body),
726
+ });
727
+ }
728
+
729
+ describe("handleChannelInbound — Slack thread backfill wiring", () => {
730
+ beforeEach(() => {
731
+ resetHttpState();
732
+ seedHttpActiveMember();
733
+ httpMsgCounter = 0;
734
+ });
735
+
736
+ afterEach(() => {
737
+ backfillThreadMock.mockReset();
738
+ _backfillTriggerCache.clear();
739
+ });
740
+
741
+ test("inbound thread reply with no stored parent triggers backfill from the HTTP path", async () => {
742
+ backfillThreadMock.mockImplementation(async () => [
743
+ makeBackfillMessage({
744
+ id: "1234.0",
745
+ text: "parent",
746
+ threadId: undefined,
747
+ sender: { id: "U_PARENT", name: "Original Poster" },
748
+ }),
749
+ makeBackfillMessage({
750
+ id: "1234.1",
751
+ text: "earlier sibling",
752
+ threadId: "1234.0",
753
+ sender: { id: "U_SIB", name: "Earlier Sibling" },
754
+ }),
755
+ ]);
756
+
757
+ const processMessage = async (): Promise<{ messageId: string }> => {
758
+ return { messageId: "agent-result-id" };
759
+ };
760
+
761
+ const req = buildThreadReplyRequest("1234.0", "1234.3");
762
+ const resp = await handleChannelInbound(
763
+ req,
764
+ processMessage,
765
+ TEST_BEARER_TOKEN,
766
+ );
767
+ const json = (await resp.json()) as Record<string, unknown>;
768
+
769
+ expect(json.accepted).toBe(true);
770
+ expect(json.duplicate).toBe(false);
771
+
772
+ // The backfill is fire-and-forget; settle the microtask queue so the
773
+ // void-promise has time to write to the DB before we assert.
774
+ await new Promise((resolve) => setTimeout(resolve, 100));
775
+
776
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
777
+ const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
778
+ expect(calledChannel).toBe(HTTP_SLACK_CHANNEL_ID);
779
+ expect(calledThread).toBe("1234.0");
780
+
781
+ const db = getDb();
782
+ const rows = db.$client
783
+ .prepare("SELECT metadata FROM messages")
784
+ .all() as Array<{ metadata: string | null }>;
785
+
786
+ const channelTimestamps = new Set<string>();
787
+ for (const row of rows) {
788
+ if (!row.metadata) continue;
789
+ try {
790
+ const envelope = JSON.parse(row.metadata) as Record<string, unknown>;
791
+ if (typeof envelope.slackMeta === "string") {
792
+ const slackMeta = readSlackMetadata(envelope.slackMeta);
793
+ if (slackMeta) channelTimestamps.add(slackMeta.channelTs);
794
+ }
795
+ } catch {
796
+ // ignore
797
+ }
798
+ }
799
+
800
+ expect(channelTimestamps.has("1234.0")).toBe(true);
801
+ expect(channelTimestamps.has("1234.1")).toBe(true);
802
+ });
803
+
804
+ test("second thread reply within the TTL window does not re-trigger backfill", async () => {
805
+ backfillThreadMock.mockImplementation(async () => [
806
+ makeBackfillMessage({ id: "5678.0", text: "parent" }),
807
+ ]);
808
+
809
+ const processMessage = async (): Promise<{ messageId: string }> => ({
810
+ messageId: "agent-result-id",
811
+ });
812
+
813
+ const r1 = await handleChannelInbound(
814
+ buildThreadReplyRequest("5678.0", "5678.1"),
815
+ processMessage,
816
+ TEST_BEARER_TOKEN,
817
+ );
818
+ expect(r1.status).toBe(200);
819
+ await new Promise((resolve) => setTimeout(resolve, 100));
820
+
821
+ const r2 = await handleChannelInbound(
822
+ buildThreadReplyRequest("5678.0", "5678.2"),
823
+ processMessage,
824
+ TEST_BEARER_TOKEN,
825
+ );
826
+ expect(r2.status).toBe(200);
827
+ await new Promise((resolve) => setTimeout(resolve, 100));
828
+
829
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
830
+ });
831
+
832
+ test("backfill error from the HTTP path does not crash the request", async () => {
833
+ backfillThreadMock.mockImplementation(async () => {
834
+ throw new Error("Slack API offline");
835
+ });
836
+
837
+ const processMessage = async (): Promise<{ messageId: string }> => ({
838
+ messageId: "agent-result-id",
839
+ });
840
+
841
+ const resp = await handleChannelInbound(
842
+ buildThreadReplyRequest("9999.0", "9999.1"),
843
+ processMessage,
844
+ TEST_BEARER_TOKEN,
845
+ );
846
+
847
+ expect(resp.status).toBe(200);
848
+ const json = (await resp.json()) as Record<string, unknown>;
849
+ expect(json.accepted).toBe(true);
850
+ });
851
+
852
+ test("backfill is awaited: parent is stored before handleChannelInbound returns", async () => {
853
+ // Replace the resolved promise with a manually-controlled deferred so we
854
+ // can prove that the inbound handler awaits the backfill rather than
855
+ // racing it against the agent-loop dispatch. If `await` were missing,
856
+ // `handleChannelInbound` would resolve before the parent row hit the
857
+ // database and the immediate post-response read below would miss it.
858
+ let resolveBackfill: (() => void) | null = null;
859
+ const backfillCompleted = new Promise<void>((resolve) => {
860
+ resolveBackfill = resolve;
861
+ });
862
+
863
+ backfillThreadMock.mockImplementation(async () => {
864
+ await backfillCompleted;
865
+ return [
866
+ makeBackfillMessage({
867
+ id: "8888.0",
868
+ text: "thread parent",
869
+ threadId: undefined,
870
+ sender: { id: "U_PARENT_AWAIT", name: "Parent Author" },
871
+ }),
872
+ ];
873
+ });
874
+
875
+ let agentLoopFired = false;
876
+ const processMessage = async (): Promise<{ messageId: string }> => {
877
+ // The agent loop runs *after* backfill. Confirm the parent row is
878
+ // already visible at this point — that proves the backfill landed
879
+ // before dispatch.
880
+ agentLoopFired = true;
881
+ return { messageId: "agent-result-id" };
882
+ };
883
+
884
+ const inboundPromise = handleChannelInbound(
885
+ buildThreadReplyRequest("8888.0", "8888.1"),
886
+ processMessage,
887
+ TEST_BEARER_TOKEN,
888
+ );
889
+
890
+ // Give the handler enough microtasks to reach the awaited backfill.
891
+ await new Promise((resolve) => setTimeout(resolve, 25));
892
+
893
+ // Backfill is suspended at the awaited deferred — the parent row should
894
+ // not yet be persisted, and the agent loop must not have fired.
895
+ const db = getDb();
896
+ const rowsBeforeResolve = db.$client
897
+ .prepare("SELECT metadata FROM messages")
898
+ .all() as Array<{ metadata: string | null }>;
899
+ const tsBefore = rowsBeforeResolve
900
+ .map((row) => {
901
+ if (!row.metadata) return undefined;
902
+ try {
903
+ const env = JSON.parse(row.metadata) as Record<string, unknown>;
904
+ if (typeof env.slackMeta !== "string") return undefined;
905
+ const meta = readSlackMetadata(env.slackMeta);
906
+ return meta?.channelTs;
907
+ } catch {
908
+ return undefined;
909
+ }
910
+ })
911
+ .filter((ts): ts is string => ts !== undefined);
912
+ expect(tsBefore.includes("8888.0")).toBe(false);
913
+ expect(agentLoopFired).toBe(false);
914
+
915
+ // Release the backfill mock; the awaited handler should now finish.
916
+ resolveBackfill!();
917
+ const resp = await inboundPromise;
918
+ expect(resp.status).toBe(200);
919
+
920
+ const rowsAfter = db.$client
921
+ .prepare("SELECT metadata FROM messages")
922
+ .all() as Array<{ metadata: string | null }>;
923
+ const tsAfter = rowsAfter
924
+ .map((row) => {
925
+ if (!row.metadata) return undefined;
926
+ try {
927
+ const env = JSON.parse(row.metadata) as Record<string, unknown>;
928
+ if (typeof env.slackMeta !== "string") return undefined;
929
+ const meta = readSlackMetadata(env.slackMeta);
930
+ return meta?.channelTs;
931
+ } catch {
932
+ return undefined;
933
+ }
934
+ })
935
+ .filter((ts): ts is string => ts !== undefined);
936
+
937
+ // The parent row is present before the response is delivered, so the
938
+ // agent loop dispatched after this point sees it.
939
+ expect(tsAfter.includes("8888.0")).toBe(true);
940
+ });
941
+ });