@vellumai/assistant 0.7.0 → 0.7.1

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 (666) hide show
  1. package/ARCHITECTURE.md +6 -7
  2. package/Dockerfile +1 -0
  3. package/README.md +2 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +79 -139
  5. package/bun.lock +3 -0
  6. package/docs/architecture/security.md +18 -16
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
  9. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -5
  10. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
  11. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
  12. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
  13. package/node_modules/@vellumai/slack-text/bun.lock +24 -0
  14. package/node_modules/@vellumai/slack-text/package.json +18 -0
  15. package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
  16. package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
  17. package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
  18. package/openapi.yaml +294 -107
  19. package/package.json +4 -2
  20. package/scripts/generate-openapi.ts +16 -111
  21. package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
  22. package/src/__tests__/anthropic-provider.test.ts +56 -13
  23. package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
  24. package/src/__tests__/app-conversation-ids.test.ts +151 -0
  25. package/src/__tests__/approval-cascade.test.ts +0 -15
  26. package/src/__tests__/approval-routes-http.test.ts +6 -17
  27. package/src/__tests__/assistant-event-hub.test.ts +126 -77
  28. package/src/__tests__/assistant-event.test.ts +0 -5
  29. package/src/__tests__/assistant-events-sse-hardening.test.ts +37 -15
  30. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -29
  31. package/src/__tests__/background-shell-host-bash.test.ts +34 -43
  32. package/src/__tests__/call-controller.test.ts +1 -1
  33. package/src/__tests__/call-site-routing-provider.test.ts +193 -0
  34. package/src/__tests__/channel-approval-routes.test.ts +10 -296
  35. package/src/__tests__/channel-approvals.test.ts +25 -17
  36. package/src/__tests__/channel-guardian.test.ts +100 -146
  37. package/src/__tests__/checker.test.ts +20 -34
  38. package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
  39. package/src/__tests__/compaction-events.test.ts +2 -0
  40. package/src/__tests__/config-schema.test.ts +6 -48
  41. package/src/__tests__/config-watcher.test.ts +12 -0
  42. package/src/__tests__/connection-policy.test.ts +1 -52
  43. package/src/__tests__/contacts-write.test.ts +2 -64
  44. package/src/__tests__/context-image-dimensions.test.ts +1 -1
  45. package/src/__tests__/context-search-memory-source.test.ts +120 -1
  46. package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
  47. package/src/__tests__/context-search-pkb-source.test.ts +49 -0
  48. package/src/__tests__/context-search-workspace-source.test.ts +9 -22
  49. package/src/__tests__/context-window-manager.test.ts +46 -0
  50. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  51. package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
  52. package/src/__tests__/conversation-agent-loop.test.ts +980 -13
  53. package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
  54. package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
  55. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
  56. package/src/__tests__/conversation-history-web-search.test.ts +4 -3
  57. package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
  58. package/src/__tests__/conversation-lifecycle.test.ts +4 -4
  59. package/src/__tests__/conversation-process-callsite.test.ts +79 -2
  60. package/src/__tests__/conversation-queue.test.ts +3 -8
  61. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -161
  62. package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -32
  63. package/src/__tests__/conversation-routes-slash-commands.test.ts +75 -66
  64. package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
  65. package/src/__tests__/conversation-slash-commands.test.ts +24 -4
  66. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  67. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  68. package/src/__tests__/conversation-starter-routes.test.ts +79 -2
  69. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
  70. package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
  71. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
  72. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
  73. package/src/__tests__/conversation-usage.test.ts +253 -3
  74. package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
  75. package/src/__tests__/credential-health-service.test.ts +68 -0
  76. package/src/__tests__/credential-security-e2e.test.ts +4 -3
  77. package/src/__tests__/credential-security-invariants.test.ts +1 -5
  78. package/src/__tests__/credential-token-resolver.test.ts +180 -0
  79. package/src/__tests__/cu-unified-flow.test.ts +33 -16
  80. package/src/__tests__/daemon-assistant-events.test.ts +34 -21
  81. package/src/__tests__/daemon-credential-client.test.ts +4 -1
  82. package/src/__tests__/db-connection-isolation.test.ts +125 -0
  83. package/src/__tests__/db-migration-rollback.test.ts +101 -0
  84. package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
  85. package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
  86. package/src/__tests__/document-conversations.test.ts +332 -0
  87. package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
  88. package/src/__tests__/emit-event-signal.test.ts +4 -6
  89. package/src/__tests__/events-client-registration.test.ts +193 -49
  90. package/src/__tests__/filing-service.test.ts +58 -7
  91. package/src/__tests__/first-greeting.test.ts +156 -150
  92. package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
  93. package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
  94. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  95. package/src/__tests__/guardian-dispatch.test.ts +1 -1
  96. package/src/__tests__/guardian-grant-minting.test.ts +7 -2
  97. package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
  98. package/src/__tests__/guardian-routing-state.test.ts +1 -1
  99. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +32 -11
  100. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -83
  101. package/src/__tests__/headless-browser-mode.test.ts +4 -9
  102. package/src/__tests__/headless-browser-navigate.test.ts +21 -20
  103. package/src/__tests__/heartbeat-service.test.ts +289 -7
  104. package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
  105. package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
  106. package/src/__tests__/host-bash-proxy.test.ts +46 -122
  107. package/src/__tests__/host-browser-e2e-cloud.test.ts +36 -497
  108. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +26 -96
  109. package/src/__tests__/host-browser-proxy.test.ts +111 -185
  110. package/src/__tests__/host-browser-routes.test.ts +45 -75
  111. package/src/__tests__/host-browser-ws-events-e2e.test.ts +26 -30
  112. package/src/__tests__/host-cu-proxy.test.ts +56 -111
  113. package/src/__tests__/host-file-proxy.test.ts +44 -98
  114. package/src/__tests__/host-file-read-tool.test.ts +42 -21
  115. package/src/__tests__/host-shell-tool.test.ts +33 -68
  116. package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
  117. package/src/__tests__/host-transfer-proxy.test.ts +43 -53
  118. package/src/__tests__/http-user-message-parity.test.ts +0 -6
  119. package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
  120. package/src/__tests__/injector-chain.test.ts +10 -5
  121. package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
  122. package/src/__tests__/inline-command-runner.test.ts +0 -66
  123. package/src/__tests__/inline-skill-load-permissions.test.ts +0 -2
  124. package/src/__tests__/install-skill-routing.test.ts +1 -13
  125. package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
  126. package/src/__tests__/llm-catalog-parity.test.ts +90 -0
  127. package/src/__tests__/llm-context-resolution.test.ts +180 -0
  128. package/src/__tests__/llm-resolver.test.ts +80 -12
  129. package/src/__tests__/llm-usage-store.test.ts +269 -4
  130. package/src/__tests__/log-export-routes.test.ts +89 -0
  131. package/src/__tests__/managed-profile-guard.test.ts +225 -0
  132. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -10
  133. package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
  134. package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
  135. package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
  136. package/src/__tests__/migration-export-http.test.ts +33 -26
  137. package/src/__tests__/migration-export-streaming.test.ts +18 -10
  138. package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
  139. package/src/__tests__/migration-import-commit-http.test.ts +66 -21
  140. package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
  141. package/src/__tests__/migration-import-from-url.test.ts +20 -6
  142. package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
  143. package/src/__tests__/migration-parity-persistence.test.ts +62 -25
  144. package/src/__tests__/migration-transport.test.ts +115 -23
  145. package/src/__tests__/migration-validate-http.test.ts +105 -80
  146. package/src/__tests__/migration-wizard.test.ts +133 -27
  147. package/src/__tests__/non-member-access-request.test.ts +1 -1
  148. package/src/__tests__/notification-guardian-path.test.ts +1 -1
  149. package/src/__tests__/oauth-store.test.ts +19 -0
  150. package/src/__tests__/platform-bash-auto-approve.test.ts +21 -12
  151. package/src/__tests__/prechat-onboarding-contract.test.ts +31 -7
  152. package/src/__tests__/pricing.test.ts +68 -4
  153. package/src/__tests__/process-message-background-slack.test.ts +331 -0
  154. package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
  155. package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
  156. package/src/__tests__/provider-usage-tracking.test.ts +208 -0
  157. package/src/__tests__/reaction-persistence.test.ts +9 -6
  158. package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
  159. package/src/__tests__/recording-handler.test.ts +64 -81
  160. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
  161. package/src/__tests__/relay-server.test.ts +18 -13
  162. package/src/__tests__/require-fresh-approval.test.ts +13 -22
  163. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  164. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  165. package/src/__tests__/runtime-events-sse.test.ts +3 -12
  166. package/src/__tests__/search-skills-unified.test.ts +9 -15
  167. package/src/__tests__/secret-ingress-cli.test.ts +2 -5
  168. package/src/__tests__/secret-ingress-http.test.ts +0 -4
  169. package/src/__tests__/secret-onetime-send.test.ts +4 -2
  170. package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
  171. package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
  172. package/src/__tests__/secret-response-routing.test.ts +29 -15
  173. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
  174. package/src/__tests__/secret-scanner.test.ts +2 -545
  175. package/src/__tests__/send-endpoint-busy.test.ts +9 -24
  176. package/src/__tests__/settings-routes.test.ts +1 -1
  177. package/src/__tests__/shell-credential-ref.test.ts +0 -8
  178. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -56
  179. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -11
  180. package/src/__tests__/skill-tool-factory.test.ts +97 -0
  181. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
  182. package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
  183. package/src/__tests__/slack-inbound-verification.test.ts +1 -62
  184. package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
  185. package/src/__tests__/subagent-manager-notify.test.ts +70 -70
  186. package/src/__tests__/subagent-notify-parent.test.ts +80 -83
  187. package/src/__tests__/system-prompt.test.ts +115 -13
  188. package/src/__tests__/terminal-tools.test.ts +0 -89
  189. package/src/__tests__/thread-backfill.test.ts +945 -31
  190. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
  191. package/src/__tests__/tool-execute-pipeline.test.ts +0 -6
  192. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -16
  193. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
  194. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -7
  195. package/src/__tests__/tool-executor.test.ts +12 -19
  196. package/src/__tests__/tool-metrics-listener.test.ts +0 -35
  197. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  198. package/src/__tests__/tool-trace-listener.test.ts +0 -17
  199. package/src/__tests__/transfer-progress-screen.test.ts +63 -26
  200. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
  201. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
  202. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  203. package/src/__tests__/tts-catalog-parity.test.ts +16 -5
  204. package/src/__tests__/usage-attribution.test.ts +247 -0
  205. package/src/__tests__/usage-cli.test.ts +143 -0
  206. package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
  207. package/src/__tests__/usage-routes.test.ts +150 -0
  208. package/src/__tests__/validation-results-screen.test.ts +39 -16
  209. package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
  210. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +49 -137
  211. package/src/__tests__/verification-control-plane-policy.test.ts +4 -7
  212. package/src/__tests__/voice-session-bridge.test.ts +5 -5
  213. package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
  214. package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
  215. package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
  216. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
  217. package/src/acp/index.ts +0 -15
  218. package/src/acp/session-manager.ts +37 -34
  219. package/src/agent/loop.ts +16 -1
  220. package/src/approvals/AGENTS.md +4 -0
  221. package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
  222. package/src/approvals/guardian-request-resolvers.ts +10 -2
  223. package/src/backup/__tests__/backup-worker.test.ts +36 -8
  224. package/src/backup/__tests__/paths.test.ts +2 -2
  225. package/src/backup/__tests__/restore.test.ts +45 -28
  226. package/src/backup/backup-worker.ts +36 -2
  227. package/src/backup/paths.ts +9 -6
  228. package/src/browser-session/events.ts +0 -9
  229. package/src/calls/call-store.ts +1 -34
  230. package/src/calls/guardian-question-copy.ts +0 -108
  231. package/src/calls/relay-server.ts +0 -24
  232. package/src/calls/twilio-rest.ts +0 -38
  233. package/src/calls/twilio-routes.ts +1 -1
  234. package/src/calls/voice-session-bridge.ts +7 -38
  235. package/src/channels/types.ts +1 -36
  236. package/src/cli/commands/__tests__/cache.test.ts +152 -5
  237. package/src/cli/commands/__tests__/memory-v2.test.ts +14 -28
  238. package/src/cli/commands/__tests__/trust.test.ts +21 -387
  239. package/src/cli/commands/backup.ts +4 -4
  240. package/src/cli/commands/cache-fs.ts +8 -0
  241. package/src/cli/commands/cache.ts +153 -82
  242. package/src/cli/commands/clients.ts +63 -5
  243. package/src/cli/commands/completions.ts +3 -3
  244. package/src/cli/commands/contacts.ts +231 -76
  245. package/src/cli/commands/keys.ts +4 -1
  246. package/src/cli/commands/memory-v2.ts +24 -52
  247. package/src/cli/commands/oauth/shared.ts +2 -29
  248. package/src/cli/commands/pending.ts +102 -0
  249. package/src/cli/commands/skills.ts +77 -35
  250. package/src/cli/commands/trust.ts +70 -430
  251. package/src/cli/commands/usage.ts +25 -16
  252. package/src/cli/lib/daemon-credential-client.ts +14 -0
  253. package/src/cli/program.ts +2 -0
  254. package/src/cli.ts +0 -21
  255. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  256. package/src/config/bundled-skills/messaging/TOOLS.json +14 -4
  257. package/src/config/env-registry.ts +12 -2
  258. package/src/config/env.ts +3 -14
  259. package/src/config/feature-flag-registry.json +30 -30
  260. package/src/config/llm-callsite-catalog.ts +12 -0
  261. package/src/config/llm-context-resolution.ts +80 -0
  262. package/src/config/llm-resolver.ts +58 -22
  263. package/src/config/loader.ts +3 -3
  264. package/src/config/schema.ts +2 -158
  265. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  266. package/src/config/schemas/call-site-catalog.ts +271 -0
  267. package/src/config/schemas/calls.ts +5 -5
  268. package/src/config/schemas/inference.ts +1 -1
  269. package/src/config/schemas/ingress.ts +1 -1
  270. package/src/config/schemas/llm.ts +31 -3
  271. package/src/config/schemas/memory-retrieval.ts +2 -2
  272. package/src/config/schemas/memory-v2.ts +9 -0
  273. package/src/config/schemas/security.ts +1 -42
  274. package/src/config/schemas/services.ts +6 -6
  275. package/src/config/schemas/skills.ts +5 -5
  276. package/src/config/schemas/tts.ts +1 -1
  277. package/src/config/seed-inference-profiles.ts +117 -0
  278. package/src/config/skills.ts +0 -90
  279. package/src/config/types.ts +3 -6
  280. package/src/contacts/contact-store.ts +0 -17
  281. package/src/contacts/contacts-write.ts +1 -105
  282. package/src/context/window-manager.ts +44 -5
  283. package/src/credential-execution/process-manager.ts +34 -10
  284. package/src/credential-health/credential-health-service.ts +21 -16
  285. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
  286. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
  287. package/src/daemon/connection-policy.ts +1 -26
  288. package/src/daemon/conversation-agent-loop-handlers.ts +53 -4
  289. package/src/daemon/conversation-agent-loop.ts +277 -36
  290. package/src/daemon/conversation-history.ts +8 -8
  291. package/src/daemon/conversation-launch.ts +20 -135
  292. package/src/daemon/conversation-lifecycle.ts +1 -1
  293. package/src/daemon/conversation-messaging.ts +1 -0
  294. package/src/daemon/conversation-process.ts +83 -163
  295. package/src/daemon/conversation-runtime-assembly.ts +219 -76
  296. package/src/daemon/conversation-slash.ts +47 -5
  297. package/src/daemon/conversation-store.ts +7 -31
  298. package/src/daemon/conversation-surfaces.ts +22 -28
  299. package/src/daemon/conversation-tool-setup.ts +3 -33
  300. package/src/daemon/conversation-usage.ts +36 -0
  301. package/src/daemon/conversation.ts +117 -233
  302. package/src/daemon/daemon-control.ts +3 -71
  303. package/src/daemon/daemon-skill-host.ts +8 -11
  304. package/src/daemon/dictation-profile-store.ts +2 -26
  305. package/src/daemon/first-greeting.ts +44 -156
  306. package/src/daemon/handlers/config-channels.ts +12 -12
  307. package/src/daemon/handlers/config-ingress.ts +4 -165
  308. package/src/daemon/handlers/config-model.ts +1 -1
  309. package/src/daemon/handlers/config-voice.ts +0 -42
  310. package/src/daemon/handlers/conversations.ts +11 -190
  311. package/src/daemon/handlers/recording.ts +26 -158
  312. package/src/daemon/handlers/shared.ts +23 -71
  313. package/src/daemon/handlers/skills.ts +42 -93
  314. package/src/daemon/host-bash-proxy.ts +67 -45
  315. package/src/daemon/host-browser-proxy.ts +65 -27
  316. package/src/daemon/host-cu-proxy.ts +40 -39
  317. package/src/daemon/host-file-proxy.ts +58 -37
  318. package/src/daemon/host-transfer-proxy.ts +84 -46
  319. package/src/daemon/lifecycle.ts +49 -15
  320. package/src/daemon/message-types/conversations.ts +7 -0
  321. package/src/daemon/message-types/host-bash.ts +1 -0
  322. package/src/daemon/message-types/host-cu.ts +1 -0
  323. package/src/daemon/message-types/host-file.ts +1 -0
  324. package/src/daemon/message-types/host-transfer.ts +1 -0
  325. package/src/daemon/message-types/messages.ts +10 -9
  326. package/src/daemon/message-types/workspace.ts +1 -1
  327. package/src/daemon/process-message.ts +102 -239
  328. package/src/daemon/server.ts +13 -462
  329. package/src/daemon/shutdown-handlers.ts +2 -2
  330. package/src/daemon/tool-side-effects.ts +125 -107
  331. package/src/daemon/trust-context.ts +13 -0
  332. package/src/daemon/wake-target-adapter.ts +4 -9
  333. package/src/events/domain-events.ts +0 -8
  334. package/src/events/tool-audit-listener.ts +3 -1
  335. package/src/events/tool-domain-event-publisher.ts +0 -10
  336. package/src/events/tool-metrics-listener.ts +0 -17
  337. package/src/events/tool-trace-listener.ts +0 -14
  338. package/src/filing/filing-service.ts +13 -1
  339. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +6 -2
  340. package/src/heartbeat/heartbeat-service.ts +23 -5
  341. package/src/home/__tests__/feed-writer.test.ts +0 -4
  342. package/src/home/__tests__/relationship-state-writer.test.ts +30 -0
  343. package/src/home/feed-writer.ts +1 -2
  344. package/src/home/relationship-state-writer.ts +16 -3
  345. package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
  346. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
  347. package/src/ipc/assistant-server.ts +3 -10
  348. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
  349. package/src/ipc/routes/route-adapter.ts +1 -1
  350. package/src/ipc/routes/trust-rules.test.ts +0 -95
  351. package/src/ipc/skill-ipc-types.ts +41 -0
  352. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
  353. package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
  354. package/src/ipc/skill-routes/events.ts +12 -23
  355. package/src/ipc/skill-routes/identity.ts +4 -17
  356. package/src/ipc/skill-routes/index.ts +1 -1
  357. package/src/ipc/skill-server.ts +6 -39
  358. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
  359. package/src/live-voice/protocol.ts +4 -13
  360. package/src/mcp/manager.ts +0 -5
  361. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
  362. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
  363. package/src/memory/app-git-service.ts +0 -32
  364. package/src/memory/app-store.ts +154 -0
  365. package/src/memory/attachments-store.ts +6 -0
  366. package/src/memory/context-search/sources/memory-v2.ts +578 -0
  367. package/src/memory/context-search/sources/memory.ts +5 -0
  368. package/src/memory/context-search/sources/pkb.ts +10 -1
  369. package/src/memory/context-search/sources/workspace.ts +3 -2
  370. package/src/memory/conversation-crud.ts +29 -4
  371. package/src/memory/conversation-disk-view.ts +1 -5
  372. package/src/memory/conversation-starter-checkpoints.ts +63 -0
  373. package/src/memory/db-connection.ts +62 -0
  374. package/src/memory/db-init.ts +14 -0
  375. package/src/memory/embedding-backend.ts +3 -21
  376. package/src/memory/embedding-gemini.ts +0 -2
  377. package/src/memory/embedding-local.ts +6 -6
  378. package/src/memory/embedding-ollama.ts +6 -6
  379. package/src/memory/embedding-openai.ts +6 -6
  380. package/src/memory/embedding-types.ts +21 -0
  381. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +3 -7
  382. package/src/memory/graph/conversation-graph-memory.ts +35 -13
  383. package/src/memory/graph/injection.test.ts +2 -2
  384. package/src/memory/graph/injection.ts +1 -1
  385. package/src/memory/guardian-action-store.ts +0 -83
  386. package/src/memory/guardian-approvals.ts +0 -48
  387. package/src/memory/indexer.ts +1 -15
  388. package/src/memory/job-handlers/conversation-starters.ts +36 -53
  389. package/src/memory/job-utils.ts +0 -6
  390. package/src/memory/jobs-store.ts +0 -1
  391. package/src/memory/jobs-worker.ts +2 -16
  392. package/src/memory/llm-request-log-store.ts +0 -41
  393. package/src/memory/llm-usage-store.ts +129 -43
  394. package/src/memory/memory-v2-activation-log-store.ts +115 -0
  395. package/src/memory/migrations/233-document-conversations.ts +54 -0
  396. package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
  397. package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
  398. package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
  399. package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
  400. package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
  401. package/src/memory/migrations/index.ts +14 -0
  402. package/src/memory/migrations/registry.ts +24 -0
  403. package/src/memory/raw-query.ts +2 -68
  404. package/src/memory/schema/conversations.ts +7 -0
  405. package/src/memory/schema/infrastructure.ts +25 -0
  406. package/src/memory/search/semantic.ts +5 -16
  407. package/src/memory/tool-usage-store.ts +2 -0
  408. package/src/memory/usage-buckets.ts +40 -1
  409. package/src/memory/usage-grouped-buckets.ts +127 -0
  410. package/src/memory/v2/__tests__/activation.test.ts +289 -90
  411. package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
  412. package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
  413. package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
  414. package/src/memory/v2/__tests__/injection.test.ts +384 -15
  415. package/src/memory/v2/__tests__/migration.test.ts +64 -36
  416. package/src/memory/v2/__tests__/page-store.test.ts +191 -8
  417. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
  418. package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
  419. package/src/memory/v2/__tests__/static-context.test.ts +153 -0
  420. package/src/memory/v2/activation.ts +168 -97
  421. package/src/memory/v2/backfill-jobs.ts +15 -100
  422. package/src/memory/v2/consolidation-job.ts +14 -12
  423. package/src/memory/v2/edge-index.ts +191 -0
  424. package/src/memory/v2/injection.ts +182 -58
  425. package/src/memory/v2/migration.ts +57 -64
  426. package/src/memory/v2/now-text.ts +2 -3
  427. package/src/memory/v2/page-store.ts +168 -31
  428. package/src/memory/v2/prompts/consolidation.ts +118 -42
  429. package/src/memory/v2/prompts/sweep.ts +3 -3
  430. package/src/memory/v2/skill-store.ts +55 -7
  431. package/src/memory/v2/static-context.ts +62 -0
  432. package/src/memory/v2/types.ts +10 -20
  433. package/src/memory/validation.ts +0 -11
  434. package/src/messaging/draft-store.ts +0 -6
  435. package/src/messaging/provider-types.ts +8 -0
  436. package/src/messaging/provider.ts +7 -0
  437. package/src/messaging/providers/gmail/client.ts +1 -121
  438. package/src/messaging/providers/outlook/client.ts +0 -73
  439. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
  440. package/src/messaging/providers/slack/adapter.ts +122 -21
  441. package/src/messaging/providers/slack/backfill.test.ts +95 -6
  442. package/src/messaging/providers/slack/backfill.ts +89 -11
  443. package/src/messaging/providers/slack/client.ts +10 -124
  444. package/src/messaging/providers/slack/message-metadata.ts +12 -2
  445. package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
  446. package/src/messaging/providers/slack/render-transcript.ts +126 -25
  447. package/src/messaging/providers/slack/types.ts +1 -0
  448. package/src/oauth/connection-resolver.test.ts +8 -0
  449. package/src/oauth/connection-resolver.ts +8 -16
  450. package/src/oauth/credential-token-resolver.ts +97 -0
  451. package/src/oauth/manual-token-connection.ts +30 -34
  452. package/src/oauth/oauth-store.ts +6 -4
  453. package/src/outbound-proxy/certs.ts +0 -7
  454. package/src/outbound-proxy/config.ts +0 -74
  455. package/src/outbound-proxy/health.ts +0 -44
  456. package/src/outbound-proxy/index.ts +0 -22
  457. package/src/permissions/approval-provenance.test.ts +184 -0
  458. package/src/permissions/approval-provenance.ts +70 -0
  459. package/src/permissions/checker.ts +4 -1
  460. package/src/permissions/gateway-threshold-reader.ts +4 -1
  461. package/src/permissions/prompter.ts +9 -2
  462. package/src/permissions/secret-prompter.ts +21 -48
  463. package/src/permissions/types.ts +33 -0
  464. package/src/permissions/workspace-policy.ts +0 -5
  465. package/src/platform/sync-identity.ts +0 -8
  466. package/src/plugins/defaults/injectors.ts +69 -2
  467. package/src/plugins/defaults/overflow-reduce.ts +3 -2
  468. package/src/plugins/types.ts +8 -0
  469. package/src/prompts/system-prompt.ts +34 -70
  470. package/src/prompts/templates/BOOTSTRAP.md +52 -6
  471. package/src/prompts/update-bulletin-job.ts +2 -0
  472. package/src/providers/__tests__/retry-callsite.test.ts +138 -1
  473. package/src/providers/anthropic/client.ts +72 -33
  474. package/src/providers/call-site-routing.ts +42 -3
  475. package/src/providers/gemini/client.ts +18 -2
  476. package/src/providers/managed-proxy/context.ts +0 -5
  477. package/src/providers/model-catalog.ts +105 -19
  478. package/src/providers/openai/chat-completions-provider.ts +6 -0
  479. package/src/providers/openai/responses-provider.ts +7 -1
  480. package/src/providers/provider-send-message.ts +45 -2
  481. package/src/providers/ratelimit.ts +7 -2
  482. package/src/providers/registry.ts +14 -9
  483. package/src/providers/retry.ts +96 -8
  484. package/src/providers/types.ts +13 -0
  485. package/src/providers/usage-tracking.ts +96 -0
  486. package/src/runtime/AGENTS.md +10 -6
  487. package/src/runtime/__tests__/agent-wake.test.ts +89 -0
  488. package/src/runtime/agent-wake.ts +39 -2
  489. package/src/runtime/assistant-event-hub.ts +541 -45
  490. package/src/runtime/assistant-event.ts +1 -6
  491. package/src/runtime/auth/context.ts +0 -9
  492. package/src/runtime/auth/middleware.ts +1 -1
  493. package/src/runtime/auth/route-policy.ts +11 -9
  494. package/src/runtime/auth/token-service.ts +0 -11
  495. package/src/runtime/channel-approvals.ts +6 -2
  496. package/src/runtime/channel-verification-service.ts +3 -5
  497. package/src/runtime/http-errors.ts +0 -34
  498. package/src/runtime/http-router.ts +6 -3
  499. package/src/runtime/http-server.ts +22 -82
  500. package/src/runtime/http-types.ts +5 -0
  501. package/src/runtime/interactive-ui.ts +0 -1
  502. package/src/runtime/middleware/auth.ts +0 -20
  503. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
  504. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
  505. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
  506. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
  507. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
  508. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
  509. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +143 -79
  510. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
  511. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
  512. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +371 -0
  513. package/src/runtime/migrations/migration-transport.ts +46 -13
  514. package/src/runtime/migrations/migration-wizard.ts +2 -2
  515. package/src/runtime/migrations/origin-mode.ts +40 -0
  516. package/src/runtime/migrations/vbundle-builder.ts +133 -79
  517. package/src/runtime/migrations/vbundle-import-analyzer.ts +9 -7
  518. package/src/runtime/migrations/vbundle-importer.ts +7 -7
  519. package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
  520. package/src/runtime/migrations/vbundle-streaming-importer.ts +3 -3
  521. package/src/runtime/migrations/vbundle-streaming-validator.ts +48 -26
  522. package/src/runtime/migrations/vbundle-validator.ts +214 -41
  523. package/src/runtime/pending-interactions.ts +13 -4
  524. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
  525. package/src/runtime/routes/__tests__/backup-routes.test.ts +28 -19
  526. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +235 -0
  527. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
  528. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
  529. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
  530. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
  531. package/src/runtime/routes/acp-routes.test.ts +0 -3
  532. package/src/runtime/routes/acp-routes.ts +3 -7
  533. package/src/runtime/routes/app-management-routes.ts +18 -9
  534. package/src/runtime/routes/approval-routes.ts +55 -14
  535. package/src/runtime/routes/avatar-routes.ts +3 -5
  536. package/src/runtime/routes/browser-routes.ts +1 -15
  537. package/src/runtime/routes/channel-guardian-routes.ts +1 -5
  538. package/src/runtime/routes/channel-readiness-routes.ts +3 -7
  539. package/src/runtime/routes/channel-route-shared.ts +2 -28
  540. package/src/runtime/routes/client-routes.ts +45 -12
  541. package/src/runtime/routes/consolidation-routes.ts +115 -0
  542. package/src/runtime/routes/conversation-list-routes.ts +12 -29
  543. package/src/runtime/routes/conversation-management-routes.ts +14 -51
  544. package/src/runtime/routes/conversation-query-routes.ts +120 -8
  545. package/src/runtime/routes/conversation-routes.ts +44 -528
  546. package/src/runtime/routes/conversation-starter-routes.ts +19 -40
  547. package/src/runtime/routes/documents-routes.ts +53 -18
  548. package/src/runtime/routes/events-routes.ts +59 -91
  549. package/src/runtime/routes/filing-routes.ts +18 -1
  550. package/src/runtime/routes/guardian-action-routes.ts +4 -9
  551. package/src/runtime/routes/host-bash-routes.ts +3 -2
  552. package/src/runtime/routes/host-browser-routes.ts +9 -33
  553. package/src/runtime/routes/host-cu-routes.ts +6 -1
  554. package/src/runtime/routes/host-file-routes.ts +3 -2
  555. package/src/runtime/routes/host-transfer-routes.ts +11 -15
  556. package/src/runtime/routes/identity-routes.ts +78 -6
  557. package/src/runtime/routes/inbound-message-handler.ts +580 -137
  558. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -88
  559. package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
  560. package/src/runtime/routes/index.ts +4 -0
  561. package/src/runtime/routes/integrations/slack/channel.ts +0 -24
  562. package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
  563. package/src/runtime/routes/memory-v2-routes.ts +10 -15
  564. package/src/runtime/routes/migration-routes.ts +188 -31
  565. package/src/runtime/routes/playground/guard.ts +1 -1
  566. package/src/runtime/routes/playground/index.ts +0 -2
  567. package/src/runtime/routes/recording-routes.ts +4 -24
  568. package/src/runtime/routes/rename-conversation-routes.ts +2 -6
  569. package/src/runtime/routes/schedule-routes.ts +3 -6
  570. package/src/runtime/routes/secret-routes.ts +87 -18
  571. package/src/runtime/routes/settings-routes.ts +29 -28
  572. package/src/runtime/routes/skills-routes.ts +12 -31
  573. package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
  574. package/src/runtime/routes/task-routes.ts +6 -6
  575. package/src/runtime/routes/trust-rules-routes.ts +3 -94
  576. package/src/runtime/routes/types.ts +4 -4
  577. package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
  578. package/src/runtime/routes/usage-routes.ts +87 -10
  579. package/src/runtime/routes/user-routes.ts +17 -31
  580. package/src/runtime/routes/work-items-routes.ts +1 -4
  581. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
  582. package/src/runtime/services/analyze-conversation.ts +7 -17
  583. package/src/runtime/services/conversation-serializer.ts +2 -4
  584. package/src/runtime/verification-outbound-actions.ts +1 -1
  585. package/src/runtime/verification-rate-limiter.ts +1 -1
  586. package/src/schedule/schedule-store.ts +0 -16
  587. package/src/security/secret-scanner.ts +14 -547
  588. package/src/security/secure-keys.ts +31 -11
  589. package/src/security/token-manager.ts +7 -3
  590. package/src/signals/cancel.ts +16 -25
  591. package/src/signals/conversation-undo.ts +2 -27
  592. package/src/signals/emit-event.ts +1 -2
  593. package/src/signals/user-message.ts +108 -22
  594. package/src/skills/catalog-install.ts +1 -0
  595. package/src/skills/clawhub.ts +2 -2
  596. package/src/skills/inline-command-runner.ts +1 -7
  597. package/src/subagent/manager.ts +67 -84
  598. package/src/tasks/task-store.ts +1 -28
  599. package/src/telemetry/types.ts +6 -0
  600. package/src/telemetry/usage-telemetry-reporter.test.ts +38 -15
  601. package/src/telemetry/usage-telemetry-reporter.ts +3 -5
  602. package/src/tools/acp/spawn.test.ts +1 -2
  603. package/src/tools/acp/steer.test.ts +1 -2
  604. package/src/tools/browser/__tests__/browser-status.test.ts +44 -127
  605. package/src/tools/browser/browser-execution.ts +31 -147
  606. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +92 -68
  607. package/src/tools/browser/cdp-client/factory.ts +48 -76
  608. package/src/tools/browser/cdp-client/index.ts +1 -14
  609. package/src/tools/executor.ts +44 -31
  610. package/src/tools/host-filesystem/edit.ts +3 -2
  611. package/src/tools/host-filesystem/read.ts +3 -2
  612. package/src/tools/host-filesystem/transfer.test.ts +45 -42
  613. package/src/tools/host-filesystem/transfer.ts +4 -3
  614. package/src/tools/host-filesystem/write.ts +3 -2
  615. package/src/tools/host-terminal/host-shell.ts +4 -3
  616. package/src/tools/network/script-proxy/index.ts +1 -10
  617. package/src/tools/permission-checker.ts +66 -1
  618. package/src/tools/skills/sandbox-runner.ts +1 -6
  619. package/src/tools/skills/skill-tool-factory.ts +32 -0
  620. package/src/tools/terminal/safe-env.ts +1 -0
  621. package/src/tools/terminal/shell.ts +2 -78
  622. package/src/tools/types.ts +12 -39
  623. package/src/tts/__tests__/provider-catalog.test.ts +2 -2
  624. package/src/tts/provider-catalog.ts +1 -1
  625. package/src/usage/actors.ts +2 -1
  626. package/src/usage/attribution.ts +185 -0
  627. package/src/usage/pricing.ts +166 -0
  628. package/src/usage/types.ts +14 -0
  629. package/src/util/json.ts +13 -0
  630. package/src/util/logger.ts +3 -3
  631. package/src/util/pricing.ts +50 -3
  632. package/src/work-items/work-item-runner.ts +15 -42
  633. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +4 -3
  634. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
  635. package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
  636. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +59 -0
  637. package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
  638. package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
  639. package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
  640. package/src/workspace/migrations/registry.ts +8 -0
  641. package/src/workspace/provider-commit-message-generator.ts +3 -3
  642. package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
  643. package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
  644. package/src/__tests__/secret-detection-handler.test.ts +0 -67
  645. package/src/__tests__/secret-scanner-executor.test.ts +0 -450
  646. package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
  647. package/src/__tests__/terminal-sandbox.test.ts +0 -374
  648. package/src/__tests__/tool-notification-listener.test.ts +0 -65
  649. package/src/context/__tests__/microcompact.test.ts +0 -805
  650. package/src/context/microcompact.ts +0 -443
  651. package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
  652. package/src/events/tool-notification-listener.ts +0 -17
  653. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
  654. package/src/memory/v2/__tests__/edges.test.ts +0 -435
  655. package/src/memory/v2/edges.ts +0 -217
  656. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
  657. package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
  658. package/src/runtime/__tests__/client-registry.test.ts +0 -271
  659. package/src/runtime/chrome-extension-registry.ts +0 -368
  660. package/src/runtime/client-registry.ts +0 -254
  661. package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
  662. package/src/tools/secret-detection-handler.ts +0 -269
  663. package/src/tools/terminal/backends/native.ts +0 -327
  664. package/src/tools/terminal/backends/types.ts +0 -37
  665. package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
  666. package/src/tools/terminal/sandbox.ts +0 -40
@@ -3,13 +3,13 @@
3
3
  * backfill of the missing thread ancestors when the conversation has no
4
4
  * record of the parent message, persists each backfilled message with a
5
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.
6
+ * gates exact-window re-triggers behind a 10-minute idempotency cache so
7
+ * bursts of retries for the same gap do not flood the Slack API.
8
8
  *
9
9
  * Tests exercise the helper {@link triggerSlackThreadBackfillIfNeeded}
10
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".
11
+ * Only the Slack backfill read is mocked, since the contract under test is
12
+ * "given what Slack returns, what does the daemon write to the DB".
13
13
  */
14
14
  import {
15
15
  afterAll,
@@ -23,7 +23,7 @@ import {
23
23
  } from "bun:test";
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
- // Mocks (must precede module imports under test). Note: backfillThread is
26
+ // Mocks (must precede module imports under test). Note: backfillThreadWindow is
27
27
  // stubbed via spyOn (below) rather than mock.module so the stub does not leak
28
28
  // into other test files (e.g. backfill.test.ts) that import the same module.
29
29
  // ---------------------------------------------------------------------------
@@ -58,6 +58,11 @@ mock.module("../runtime/gateway-client.js", () => ({
58
58
  import { v4 as uuid } from "uuid";
59
59
 
60
60
  import { upsertContactChannel } from "../contacts/contacts-write.js";
61
+ import {
62
+ type ChannelCapabilities,
63
+ loadSlackChronologicalContext,
64
+ } from "../daemon/conversation-runtime-assembly.js";
65
+ import type { MessageRow } from "../memory/conversation-crud.js";
61
66
  import { getDb } from "../memory/db-connection.js";
62
67
  import { initializeDb } from "../memory/db-init.js";
63
68
  import type { Message as MessagingMessage } from "../messaging/provider-types.js";
@@ -66,22 +71,39 @@ import {
66
71
  readSlackMetadata,
67
72
  writeSlackMetadata,
68
73
  } from "../messaging/providers/slack/message-metadata.js";
74
+ import type { Message } from "../providers/types.js";
69
75
  import {
70
76
  _backfillTriggerCache,
71
77
  triggerSlackThreadBackfillIfNeeded,
72
78
  } from "../runtime/routes/inbound-message-handler.js";
73
- import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
79
+ import {
80
+ handleChannelInbound,
81
+ setAdapterProcessMessage,
82
+ } from "./helpers/channel-test-adapter.js";
74
83
 
75
84
  initializeDb();
76
85
 
77
- // Spy on backfillThread so the stub is scoped to this test file only.
78
- // Restoring after the file's tests run keeps cross-file leakage to zero —
79
- // other tests (e.g. backfill.test.ts) keep seeing the real implementation.
80
- const backfillThreadMock = spyOn(slackBackfill, "backfillThread");
86
+ // Spy on backfillThreadWindowPage so the stub is scoped to this test file
87
+ // only. Existing tests drive the message array through `backfillThreadMock`;
88
+ // page metadata defaults to "complete" unless a test overrides the page spy.
89
+ const backfillThreadMock = mock<typeof slackBackfill.backfillThreadWindow>(
90
+ async () => [],
91
+ );
92
+ const backfillThreadPageMock = spyOn(slackBackfill, "backfillThreadWindowPage");
93
+ function installDefaultThreadPageMock(): void {
94
+ backfillThreadPageMock.mockImplementation(async (...args) => ({
95
+ messages: await backfillThreadMock(...args),
96
+ hasMore: false,
97
+ }));
98
+ }
99
+ installDefaultThreadPageMock();
81
100
  backfillThreadMock.mockResolvedValue([]);
101
+ const backfillDmMock = spyOn(slackBackfill, "backfillDm");
102
+ backfillDmMock.mockResolvedValue([]);
82
103
 
83
104
  afterAll(() => {
84
- backfillThreadMock.mockRestore();
105
+ backfillThreadPageMock.mockRestore();
106
+ backfillDmMock.mockRestore();
85
107
  });
86
108
 
87
109
  // ---------------------------------------------------------------------------
@@ -105,6 +127,8 @@ function resetState(): void {
105
127
  _backfillTriggerCache.clear();
106
128
  backfillThreadMock.mockReset();
107
129
  backfillThreadMock.mockImplementation(async () => []);
130
+ backfillDmMock.mockReset();
131
+ backfillDmMock.mockImplementation(async () => []);
108
132
  }
109
133
 
110
134
  let convCounter = 0;
@@ -137,7 +161,7 @@ function insertMessage(
137
161
  role: string,
138
162
  content: string,
139
163
  metadata?: Record<string, unknown>,
140
- ): void {
164
+ ): string {
141
165
  const db = getDb();
142
166
  const id = uuid();
143
167
  // Use a strictly increasing timestamp so the ORDER BY in
@@ -152,6 +176,7 @@ function insertMessage(
152
176
  VALUES (?, ?, ?, ?, ?, ?)`,
153
177
  )
154
178
  .run(id, conversationId, role, content, now, metadataStr);
179
+ return id;
155
180
  }
156
181
 
157
182
  interface RawMessageRow {
@@ -169,6 +194,19 @@ function readMessagesByConversation(conversationId: string): RawMessageRow[] {
169
194
  .all(conversationId) as RawMessageRow[];
170
195
  }
171
196
 
197
+ function readMessageRowsByConversation(conversationId: string): MessageRow[] {
198
+ const db = getDb();
199
+ return db.$client
200
+ .prepare(
201
+ `SELECT id, conversation_id AS conversationId, role, content,
202
+ created_at AS createdAt, metadata
203
+ FROM messages
204
+ WHERE conversation_id = ?
205
+ ORDER BY created_at ASC`,
206
+ )
207
+ .all(conversationId) as MessageRow[];
208
+ }
209
+
172
210
  function makeBackfillMessage(
173
211
  overrides: Partial<MessagingMessage> = {},
174
212
  ): MessagingMessage {
@@ -190,6 +228,7 @@ interface PersistedRow {
190
228
  channelTs: string | undefined;
191
229
  threadTs: string | undefined;
192
230
  displayName: string | undefined;
231
+ slackFiles: Array<{ name: string; mimetype?: string }> | undefined;
193
232
  }
194
233
 
195
234
  function readPersistedSlackRows(conversationId: string): PersistedRow[] {
@@ -202,6 +241,7 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
202
241
  channelTs: undefined,
203
242
  threadTs: undefined,
204
243
  displayName: undefined,
244
+ slackFiles: undefined,
205
245
  };
206
246
  if (!row.metadata) {
207
247
  out.push(blank);
@@ -235,6 +275,10 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
235
275
  channelTs: slackMeta?.channelTs,
236
276
  threadTs: slackMeta?.threadTs,
237
277
  displayName: slackMeta?.displayName,
278
+ slackFiles: slackMeta?.slackFiles?.map((file) => ({
279
+ name: file.name,
280
+ ...(file.mimetype ? { mimetype: file.mimetype } : {}),
281
+ })),
238
282
  });
239
283
  }
240
284
  return out;
@@ -268,6 +312,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
268
312
 
269
313
  afterEach(() => {
270
314
  backfillThreadMock.mockReset();
315
+ installDefaultThreadPageMock();
271
316
  _backfillTriggerCache.clear();
272
317
  });
273
318
 
@@ -325,11 +370,297 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
325
370
  expect(byChannelTs.get("1234.2")?.displayName).toBe("Reply Two");
326
371
  });
327
372
 
328
- test("backfill is NOT triggered when the parent is already persisted", async () => {
373
+ test("initial late-join backfill keeps the newest bounded page before the inbound mention", async () => {
374
+ const conv = createTestConversation();
375
+ const ts = (n: number) => `1700000000.${String(n).padStart(6, "0")}`;
376
+ const inboundTs = ts(500000);
377
+
378
+ backfillThreadPageMock.mockImplementation(async (...args) => {
379
+ const messages = await backfillThreadMock(...args);
380
+ const opts = args[2];
381
+ if (opts?.limit === 25) {
382
+ return { messages, hasMore: true, nextCursor: "early-page-2" };
383
+ }
384
+ return { messages, hasMore: false };
385
+ });
386
+ backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
387
+ if (opts?.limit === 25) {
388
+ return Array.from({ length: 25 }, (_, i) =>
389
+ makeBackfillMessage({
390
+ id: ts(i),
391
+ text: i === 0 ? "root context" : `early ${i}`,
392
+ threadId: i === 0 ? undefined : ts(0),
393
+ }),
394
+ );
395
+ }
396
+ if (opts?.before === inboundTs && opts.after !== undefined) {
397
+ return [
398
+ ...Array.from({ length: 50 }, (_, i) => {
399
+ const n = 499950 + i;
400
+ return makeBackfillMessage({
401
+ id: ts(n),
402
+ text: n === 499999 ? "newest file share" : `recent ${n}`,
403
+ threadId: ts(0),
404
+ ...(n === 499999
405
+ ? {
406
+ metadata: {
407
+ slackFiles: [
408
+ {
409
+ id: "F123",
410
+ name: "requirements.txt",
411
+ mimetype: "text/plain",
412
+ },
413
+ ],
414
+ },
415
+ }
416
+ : {}),
417
+ });
418
+ }),
419
+ makeBackfillMessage({
420
+ id: ts(499960),
421
+ text: "duplicate recent row",
422
+ threadId: ts(0),
423
+ }),
424
+ ];
425
+ }
426
+ return [];
427
+ });
428
+
429
+ const result = await triggerSlackThreadBackfillIfNeeded({
430
+ conversationId: conv.id,
431
+ channelId: SLACK_CHANNEL_ID,
432
+ threadTs: ts(0),
433
+ excludeChannelTs: inboundTs,
434
+ });
435
+
436
+ expect(backfillThreadMock).toHaveBeenCalledTimes(2);
437
+ expect(backfillThreadMock.mock.calls[0][2]?.limit).toBe(25);
438
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
439
+ expect(backfillThreadMock.mock.calls[1][2]?.limit).toBe(50);
440
+ expect(backfillThreadMock.mock.calls[1][2]?.before).toBe(inboundTs);
441
+ expect(backfillThreadMock.mock.calls[1][2]?.after).toBeDefined();
442
+
443
+ expect(result.reason).toBe("thread_late_join");
444
+ expect(result.omittedMiddle).toBe(true);
445
+
446
+ const persisted = readPersistedSlackRows(conv.id);
447
+ expect(persisted.length).toBe(75);
448
+ expect(persisted.find((p) => p.channelTs === ts(0))?.content).toBe(
449
+ "root context",
450
+ );
451
+ expect(persisted.find((p) => p.channelTs === ts(250000))).toBeUndefined();
452
+ expect(persisted.find((p) => p.channelTs === ts(499999))?.content).toBe(
453
+ "newest file share",
454
+ );
455
+ expect(
456
+ persisted.filter((p) => p.channelTs === ts(499960)).map((p) => p.content),
457
+ ).toEqual(["recent 499960"]);
458
+ expect(
459
+ persisted.find((p) => p.channelTs === ts(499999))?.slackFiles,
460
+ ).toEqual([{ name: "requirements.txt", mimetype: "text/plain" }]);
461
+ });
462
+
463
+ test("high-throughput initial backfill keeps shrinking after a truncated probe and persists newest pre-mention rows", async () => {
464
+ const conv = createTestConversation();
465
+ const ts = (seconds: number, micros = 0) =>
466
+ `${seconds}.${String(micros).padStart(6, "0")}`;
467
+ const threadTs = ts(1700000000);
468
+ const inboundTs = ts(1700001000);
469
+ const fiveMinuteAfter = ts(1700000700);
470
+ const sixtySecondAfter = ts(1700000940);
471
+ const tenSecondAfter = ts(1700000990);
472
+ const newestPreMention = [
473
+ makeBackfillMessage({
474
+ id: ts(1700000997, 100000),
475
+ text: "newest context 1",
476
+ threadId: threadTs,
477
+ }),
478
+ makeBackfillMessage({
479
+ id: ts(1700000998, 200000),
480
+ text: "newest context 2",
481
+ threadId: threadTs,
482
+ }),
483
+ makeBackfillMessage({
484
+ id: ts(1700000999, 300000),
485
+ text: "newest context 3",
486
+ threadId: threadTs,
487
+ }),
488
+ ];
489
+
490
+ backfillThreadPageMock.mockImplementation(
491
+ async (_channel, _thread, opts) => {
492
+ if (opts?.limit === 25 && opts.before === undefined) {
493
+ return {
494
+ messages: [
495
+ makeBackfillMessage({
496
+ id: threadTs,
497
+ text: "thread parent",
498
+ threadId: undefined,
499
+ }),
500
+ ],
501
+ hasMore: true,
502
+ };
503
+ }
504
+
505
+ if (opts?.limit === 50 && opts.before === inboundTs) {
506
+ if (
507
+ opts.after === fiveMinuteAfter ||
508
+ opts.after === sixtySecondAfter
509
+ ) {
510
+ return {
511
+ messages: Array.from({ length: 50 }, (_, i) =>
512
+ makeBackfillMessage({
513
+ id: ts(1700000940 + i, i),
514
+ text: `truncated high-throughput ${i}`,
515
+ threadId: threadTs,
516
+ }),
517
+ ),
518
+ hasMore: true,
519
+ nextCursor: "still-truncated",
520
+ };
521
+ }
522
+
523
+ if (opts.after === tenSecondAfter) {
524
+ return { messages: newestPreMention, hasMore: false };
525
+ }
526
+ }
527
+
528
+ return { messages: [], hasMore: false };
529
+ },
530
+ );
531
+
532
+ const pageCallOffset = backfillThreadPageMock.mock.calls.length;
533
+
534
+ const result = await triggerSlackThreadBackfillIfNeeded({
535
+ conversationId: conv.id,
536
+ channelId: SLACK_CHANNEL_ID,
537
+ threadTs,
538
+ excludeChannelTs: inboundTs,
539
+ });
540
+
541
+ const afterAttempts = backfillThreadPageMock.mock.calls
542
+ .slice(pageCallOffset)
543
+ .map((call) => call[2]?.after)
544
+ .filter((after): after is string => after !== undefined);
545
+ expect(afterAttempts).toContain(sixtySecondAfter);
546
+ expect(afterAttempts).toContain(tenSecondAfter);
547
+ expect(afterAttempts.indexOf(tenSecondAfter)).toBeGreaterThan(
548
+ afterAttempts.indexOf(sixtySecondAfter),
549
+ );
550
+
551
+ expect(result.reason).toBe("thread_late_join");
552
+ expect(result.omittedMiddle).toBe(true);
553
+
554
+ const persisted = readPersistedSlackRows(conv.id);
555
+ expect(
556
+ persisted.filter((p) => p.threadTs === threadTs).map((p) => p.content),
557
+ ).toEqual(["newest context 1", "newest context 2", "newest context 3"]);
558
+ expect(
559
+ persisted.some((p) => p.content.startsWith("truncated high-throughput")),
560
+ ).toBe(false);
561
+ expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
562
+ });
563
+
564
+ test("high-throughput initial backfill still runs near-upper fallback after shrinking attempts are exhausted", async () => {
565
+ const conv = createTestConversation();
566
+ const ts = (seconds: number, micros = 0) =>
567
+ `${seconds}.${String(micros).padStart(6, "0")}`;
568
+ const threadTs = ts(1700000000);
569
+ const inboundTs = ts(1700001000);
570
+ const fiveMinuteAfter = ts(1700000700);
571
+ const sixtySecondAfter = ts(1700000940);
572
+ const tenSecondAfter = ts(1700000990);
573
+ const oneSecondAfter = ts(1700000999);
574
+ const hundredMillisecondAfter = ts(1700000999, 900000);
575
+ const nearUpperFallbackAfter = ts(1700000999, 999998);
576
+
577
+ backfillThreadPageMock.mockImplementation(
578
+ async (_channel, _thread, opts) => {
579
+ if (opts?.limit === 25 && opts.before === undefined) {
580
+ return {
581
+ messages: [
582
+ makeBackfillMessage({
583
+ id: threadTs,
584
+ text: "thread parent",
585
+ threadId: undefined,
586
+ }),
587
+ ],
588
+ hasMore: true,
589
+ };
590
+ }
591
+
592
+ if (opts?.limit === 50 && opts.before === inboundTs) {
593
+ if (opts.after === nearUpperFallbackAfter) {
594
+ return {
595
+ messages: [
596
+ makeBackfillMessage({
597
+ id: ts(1700000999, 999999),
598
+ text: "newest context after exhausted probes",
599
+ threadId: threadTs,
600
+ }),
601
+ ],
602
+ hasMore: false,
603
+ };
604
+ }
605
+
606
+ return {
607
+ messages: Array.from({ length: 50 }, (_, i) =>
608
+ makeBackfillMessage({
609
+ id: ts(1700000999, 900000 + i),
610
+ text: `truncated exhausted probe ${i}`,
611
+ threadId: threadTs,
612
+ }),
613
+ ),
614
+ hasMore: true,
615
+ nextCursor: "still-truncated",
616
+ };
617
+ }
618
+
619
+ return { messages: [], hasMore: false };
620
+ },
621
+ );
622
+
623
+ const exhaustedPageCallOffset = backfillThreadPageMock.mock.calls.length;
624
+
625
+ const result = await triggerSlackThreadBackfillIfNeeded({
626
+ conversationId: conv.id,
627
+ channelId: SLACK_CHANNEL_ID,
628
+ threadTs,
629
+ excludeChannelTs: inboundTs,
630
+ });
631
+
632
+ const afterAttempts = backfillThreadPageMock.mock.calls
633
+ .slice(exhaustedPageCallOffset)
634
+ .map((call) => call[2]?.after)
635
+ .filter((after): after is string => after !== undefined);
636
+ expect(afterAttempts).toEqual([
637
+ fiveMinuteAfter,
638
+ sixtySecondAfter,
639
+ tenSecondAfter,
640
+ oneSecondAfter,
641
+ hundredMillisecondAfter,
642
+ nearUpperFallbackAfter,
643
+ ]);
644
+
645
+ expect(result.reason).toBe("thread_late_join");
646
+ expect(result.omittedMiddle).toBe(true);
647
+
648
+ const persisted = readPersistedSlackRows(conv.id);
649
+ expect(
650
+ persisted.find((p) => p.channelTs === ts(1700000999, 999999))?.content,
651
+ ).toBe("newest context after exhausted probes");
652
+ expect(
653
+ persisted.some((p) => p.content.startsWith("truncated exhausted probe")),
654
+ ).toBe(false);
655
+ expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
656
+ });
657
+
658
+ test("backfill is NOT triggered when the parent is already persisted and no upper-bound gap is known", async () => {
329
659
  const conv = createTestConversation();
330
660
 
331
661
  // Seed the parent message before the trigger runs — simulates a
332
- // conversation where the daemon has already seen the thread parent.
662
+ // conversation where the daemon has already seen the thread parent but
663
+ // the caller did not provide the inbound Slack ts needed to bound a gap.
333
664
  seedSlackRow(conv.id, "1234.0", undefined, "already here");
334
665
 
335
666
  await triggerSlackThreadBackfillIfNeeded({
@@ -345,6 +676,175 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
345
676
  expect(persisted[0].channelTs).toBe("1234.0");
346
677
  });
347
678
 
679
+ test("parent already persisted but later replies are missing triggers a bounded delta backfill", async () => {
680
+ const conv = createTestConversation();
681
+
682
+ seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
683
+
684
+ backfillThreadMock.mockImplementation(async () => [
685
+ makeBackfillMessage({
686
+ id: "1234.0",
687
+ text: "duplicate parent",
688
+ threadId: undefined,
689
+ }),
690
+ makeBackfillMessage({
691
+ id: "1234.1",
692
+ text: "unseen earlier reply",
693
+ threadId: "1234.0",
694
+ }),
695
+ makeBackfillMessage({
696
+ id: "1234.5",
697
+ text: "live inbound reply",
698
+ threadId: "1234.0",
699
+ }),
700
+ ]);
701
+
702
+ await triggerSlackThreadBackfillIfNeeded({
703
+ conversationId: conv.id,
704
+ channelId: SLACK_CHANNEL_ID,
705
+ threadTs: "1234.0",
706
+ excludeChannelTs: "1234.5",
707
+ });
708
+
709
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
710
+ const [, , opts] = backfillThreadMock.mock.calls[0];
711
+ expect(opts?.after).toBe("1234.0");
712
+ expect(opts?.before).toBe("1234.5");
713
+
714
+ const persisted = readPersistedSlackRows(conv.id);
715
+ expect(persisted.length).toBe(2);
716
+ expect(persisted.find((p) => p.channelTs === "1234.0")?.content).toBe(
717
+ "parent already here",
718
+ );
719
+ expect(persisted.find((p) => p.channelTs === "1234.1")?.content).toBe(
720
+ "unseen earlier reply",
721
+ );
722
+ expect(persisted.find((p) => p.channelTs === "1234.5")).toBeUndefined();
723
+ });
724
+
725
+ test("multi-page delta backfill keeps the newest rows before the inbound mention", async () => {
726
+ const conv = createTestConversation();
727
+ const parentTs = "1699990000.000000";
728
+ const inboundTs = "1700000000.500000";
729
+ const ts = (n: number) => `1700000000.${String(n).padStart(6, "0")}`;
730
+
731
+ seedSlackRow(conv.id, parentTs, undefined, "parent already here");
732
+
733
+ backfillThreadPageMock.mockImplementation(async (...args) => {
734
+ const messages = await backfillThreadMock(...args);
735
+ const opts = args[2];
736
+ if (opts?.limit === 1) {
737
+ return { messages, hasMore: messages.length > 0 };
738
+ }
739
+ return { messages, hasMore: false };
740
+ });
741
+ backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
742
+ if (opts?.limit === 1) {
743
+ return [
744
+ makeBackfillMessage({
745
+ id: ts(100000),
746
+ text: "omitted earlier delta",
747
+ threadId: parentTs,
748
+ }),
749
+ ];
750
+ }
751
+ if (opts?.before === inboundTs && opts.after !== parentTs) {
752
+ return Array.from({ length: 50 }, (_, i) => {
753
+ const n = 499950 + i;
754
+ return makeBackfillMessage({
755
+ id: ts(n),
756
+ text: `newest delta ${n}`,
757
+ threadId: parentTs,
758
+ });
759
+ });
760
+ }
761
+ return [];
762
+ });
763
+
764
+ const result = await triggerSlackThreadBackfillIfNeeded({
765
+ conversationId: conv.id,
766
+ channelId: SLACK_CHANNEL_ID,
767
+ threadTs: parentTs,
768
+ excludeChannelTs: inboundTs,
769
+ });
770
+
771
+ expect(result.reason).toBe("thread_delta");
772
+ expect(result.omittedMiddle).toBe(true);
773
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBe(inboundTs);
774
+ expect(backfillThreadMock.mock.calls[0][2]?.after).not.toBe(parentTs);
775
+
776
+ const persisted = readPersistedSlackRows(conv.id);
777
+ expect(persisted.find((p) => p.channelTs === parentTs)?.content).toBe(
778
+ "parent already here",
779
+ );
780
+ expect(persisted.find((p) => p.channelTs === ts(100000))).toBeUndefined();
781
+ expect(persisted.find((p) => p.channelTs === ts(499999))?.content).toBe(
782
+ "newest delta 499999",
783
+ );
784
+ expect(persisted.find((p) => p.channelTs === inboundTs)).toBeUndefined();
785
+ });
786
+
787
+ test("file-bearing backfill renders a Slack file marker without binary hydration", async () => {
788
+ const conv = createTestConversation();
789
+
790
+ seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
791
+
792
+ backfillThreadMock.mockImplementation(async () => [
793
+ makeBackfillMessage({
794
+ id: "1234.1",
795
+ text: "uploaded the draft",
796
+ threadId: "1234.0",
797
+ sender: { id: "U_FILE", name: "File Sharer" },
798
+ metadata: {
799
+ slackFiles: [
800
+ {
801
+ id: "F-DRAFT",
802
+ name: "project-plan.pdf",
803
+ mimetype: "application/pdf",
804
+ },
805
+ ],
806
+ },
807
+ }),
808
+ ]);
809
+
810
+ await triggerSlackThreadBackfillIfNeeded({
811
+ conversationId: conv.id,
812
+ channelId: SLACK_CHANNEL_ID,
813
+ threadTs: "1234.0",
814
+ excludeChannelTs: "1234.2",
815
+ });
816
+
817
+ const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
818
+ loader: readMessageRowsByConversation,
819
+ trustClass: "guardian",
820
+ });
821
+
822
+ expect(context).not.toBeNull();
823
+ const rendered = flattenText(context!.messages);
824
+ expect(rendered).toContain("uploaded the draft");
825
+ expect(rendered).toContain(
826
+ "[attached file: project-plan.pdf, application/pdf]",
827
+ );
828
+ expect(rendered).not.toContain("F-DRAFT");
829
+ });
830
+
831
+ test("latest stored thread message at or after inbound ts skips backfill using parsed Slack timestamps", async () => {
832
+ const conv = createTestConversation();
833
+
834
+ seedSlackRow(conv.id, "1234.0", undefined, "parent");
835
+ seedSlackRow(conv.id, "1234.10", "1234.0", "newer stored reply");
836
+
837
+ await triggerSlackThreadBackfillIfNeeded({
838
+ conversationId: conv.id,
839
+ channelId: SLACK_CHANNEL_ID,
840
+ threadTs: "1234.0",
841
+ excludeChannelTs: "1234.2",
842
+ });
843
+
844
+ expect(backfillThreadMock).not.toHaveBeenCalled();
845
+ expect(readPersistedSlackRows(conv.id).length).toBe(2);
846
+ });
847
+
348
848
  test("idempotency cache: a second call inside the TTL window does not re-fetch", async () => {
349
849
  const conv = createTestConversation();
350
850
 
@@ -358,8 +858,8 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
358
858
  threadTs: "1234.0",
359
859
  });
360
860
 
361
- // Second call for the same conversation+thread — must short-circuit on
362
- // the in-memory cache without hitting backfillThread again.
861
+ // Second call for the same unbounded window — must short-circuit on the
862
+ // in-memory cache without hitting backfillThreadWindow again.
363
863
  await triggerSlackThreadBackfillIfNeeded({
364
864
  conversationId: conv.id,
365
865
  channelId: SLACK_CHANNEL_ID,
@@ -394,17 +894,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
394
894
  test("backfill returns duplicates that are already stored — only new rows are inserted", async () => {
395
895
  const conv = createTestConversation();
396
896
 
397
- // Pre-seed sibling 1234.1 so the backfill response includes one row that
398
- // already exists (and must not be re-inserted) plus two genuinely new
399
- // ones (parent 1234.0 and sibling 1234.2).
897
+ // Pre-seed parent and sibling 1234.1 so the bounded delta response
898
+ // includes one row that already exists (and must not be re-inserted)
899
+ // plus one genuinely new sibling.
900
+ seedSlackRow(conv.id, "1234.0", undefined, "parent");
400
901
  seedSlackRow(conv.id, "1234.1", "1234.0", "already here");
401
902
 
402
903
  backfillThreadMock.mockImplementation(async () => [
403
- makeBackfillMessage({
404
- id: "1234.0",
405
- text: "parent",
406
- threadId: undefined,
407
- }),
408
904
  makeBackfillMessage({
409
905
  id: "1234.1",
410
906
  text: "duplicate sibling — must be skipped",
@@ -421,8 +917,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
421
917
  conversationId: conv.id,
422
918
  channelId: SLACK_CHANNEL_ID,
423
919
  threadTs: "1234.0",
920
+ excludeChannelTs: "1234.3",
424
921
  });
425
922
 
923
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
924
+ expect(backfillThreadMock.mock.calls[0][2]?.after).toBe("1234.1");
925
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBe("1234.3");
926
+
426
927
  const persisted = readPersistedSlackRows(conv.id);
427
928
  expect(persisted.length).toBe(3);
428
929
 
@@ -453,7 +954,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
453
954
  expect(backfillThreadMock).toHaveBeenCalledTimes(1);
454
955
  expect(readPersistedSlackRows(conv.id).length).toBe(0);
455
956
 
456
- // Cache should now be populated for this conversation+thread, so an
957
+ // Cache should now be populated for this exact unbounded window, so an
457
958
  // immediate retry must not re-run the API call.
458
959
  await triggerSlackThreadBackfillIfNeeded({
459
960
  conversationId: conv.id,
@@ -463,6 +964,124 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
463
964
  expect(backfillThreadMock).toHaveBeenCalledTimes(1);
464
965
  });
465
966
 
967
+ test("TTL cache suppresses the same bounded window but not a newer upper-bound window", async () => {
968
+ const conv = createTestConversation();
969
+
970
+ backfillThreadMock.mockImplementation(async () => []);
971
+
972
+ await triggerSlackThreadBackfillIfNeeded({
973
+ conversationId: conv.id,
974
+ channelId: SLACK_CHANNEL_ID,
975
+ threadTs: "1234.0",
976
+ excludeChannelTs: "1234.5",
977
+ });
978
+ await triggerSlackThreadBackfillIfNeeded({
979
+ conversationId: conv.id,
980
+ channelId: SLACK_CHANNEL_ID,
981
+ threadTs: "1234.0",
982
+ excludeChannelTs: "1234.5",
983
+ });
984
+ await triggerSlackThreadBackfillIfNeeded({
985
+ conversationId: conv.id,
986
+ channelId: SLACK_CHANNEL_ID,
987
+ threadTs: "1234.0",
988
+ excludeChannelTs: "1234.6",
989
+ });
990
+
991
+ expect(
992
+ backfillThreadMock.mock.calls.some(
993
+ (call) => call[2]?.before === "1234.5",
994
+ ),
995
+ ).toBe(true);
996
+ expect(
997
+ backfillThreadMock.mock.calls.some(
998
+ (call) => call[2]?.before === "1234.6",
999
+ ),
1000
+ ).toBe(true);
1001
+ });
1002
+
1003
+ test("rapid consecutive replies can fetch a newer gap even when the prior inbound reply was only excluded", async () => {
1004
+ const conv = createTestConversation();
1005
+
1006
+ backfillThreadMock.mockImplementation(async (_channel, _thread, opts) => {
1007
+ if (opts?.limit === 25) {
1008
+ return [
1009
+ makeBackfillMessage({
1010
+ id: "1234.0",
1011
+ text: "parent",
1012
+ threadId: undefined,
1013
+ }),
1014
+ ];
1015
+ }
1016
+ if (opts?.before === "1234.5") {
1017
+ return [
1018
+ makeBackfillMessage({
1019
+ id: "1234.0",
1020
+ text: "parent",
1021
+ threadId: undefined,
1022
+ }),
1023
+ makeBackfillMessage({
1024
+ id: "1234.4",
1025
+ text: "reply before first live inbound",
1026
+ threadId: "1234.0",
1027
+ }),
1028
+ makeBackfillMessage({
1029
+ id: "1234.5",
1030
+ text: "first live inbound",
1031
+ threadId: "1234.0",
1032
+ }),
1033
+ ];
1034
+ }
1035
+ return [
1036
+ makeBackfillMessage({
1037
+ id: "1234.5",
1038
+ text: "first live inbound recovered by newer window",
1039
+ threadId: "1234.0",
1040
+ }),
1041
+ makeBackfillMessage({
1042
+ id: "1234.6",
1043
+ text: "second live inbound",
1044
+ threadId: "1234.0",
1045
+ }),
1046
+ ];
1047
+ });
1048
+
1049
+ await triggerSlackThreadBackfillIfNeeded({
1050
+ conversationId: conv.id,
1051
+ channelId: SLACK_CHANNEL_ID,
1052
+ threadTs: "1234.0",
1053
+ excludeChannelTs: "1234.5",
1054
+ });
1055
+ await triggerSlackThreadBackfillIfNeeded({
1056
+ conversationId: conv.id,
1057
+ channelId: SLACK_CHANNEL_ID,
1058
+ threadTs: "1234.0",
1059
+ excludeChannelTs: "1234.6",
1060
+ });
1061
+
1062
+ expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(3);
1063
+ expect(backfillThreadMock.mock.calls[0][2]?.after).toBeUndefined();
1064
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
1065
+ expect(
1066
+ backfillThreadMock.mock.calls.some(
1067
+ (call) => call[2]?.before === "1234.5",
1068
+ ),
1069
+ ).toBe(true);
1070
+ expect(
1071
+ backfillThreadMock.mock.calls.some(
1072
+ (call) => call[2]?.before === "1234.6",
1073
+ ),
1074
+ ).toBe(true);
1075
+
1076
+ const persisted = readPersistedSlackRows(conv.id);
1077
+ expect(persisted.map((p) => p.channelTs).sort()).toEqual([
1078
+ "1234.0",
1079
+ "1234.4",
1080
+ "1234.5",
1081
+ ]);
1082
+ expect(persisted.find((p) => p.channelTs === "1234.6")).toBeUndefined();
1083
+ });
1084
+
466
1085
  test("two distinct threads in the same conversation each trigger their own backfill", async () => {
467
1086
  const conv = createTestConversation();
468
1087
 
@@ -678,13 +1297,16 @@ function resetHttpState(): void {
678
1297
  _backfillTriggerCache.clear();
679
1298
  backfillThreadMock.mockReset();
680
1299
  backfillThreadMock.mockImplementation(async () => []);
1300
+ backfillDmMock.mockReset();
1301
+ backfillDmMock.mockImplementation(async () => []);
1302
+ setAdapterProcessMessage(undefined);
681
1303
  }
682
1304
 
683
- function seedHttpActiveMember(): void {
1305
+ function seedHttpActiveMember(chatId = HTTP_SLACK_CHANNEL_ID): void {
684
1306
  upsertContactChannel({
685
1307
  sourceChannel: "slack",
686
1308
  externalUserId: HTTP_SLACK_USER_ID,
687
- externalChatId: HTTP_SLACK_CHANNEL_ID,
1309
+ externalChatId: chatId,
688
1310
  status: "active",
689
1311
  policy: "allow",
690
1312
  displayName: HTTP_SLACK_DISPLAY_NAME,
@@ -727,6 +1349,93 @@ function buildThreadReplyRequest(
727
1349
  });
728
1350
  }
729
1351
 
1352
+ function buildSlackDmRequest(
1353
+ channelId: string,
1354
+ messageId: string,
1355
+ overrides: Record<string, unknown> = {},
1356
+ ): Request {
1357
+ httpMsgCounter++;
1358
+ const body: Record<string, unknown> = {
1359
+ sourceChannel: "slack",
1360
+ interface: "slack",
1361
+ conversationExternalId: channelId,
1362
+ externalMessageId: `${channelId}:${messageId}:${httpMsgCounter}`,
1363
+ content: "DM text",
1364
+ actorExternalId: HTTP_SLACK_USER_ID,
1365
+ actorDisplayName: HTTP_SLACK_DISPLAY_NAME,
1366
+ actorUsername: "charlie",
1367
+ replyCallbackUrl: "http://localhost:7830/deliver/slack",
1368
+ sourceMetadata: {
1369
+ messageId,
1370
+ chatType: "im",
1371
+ },
1372
+ ...overrides,
1373
+ };
1374
+
1375
+ return new Request("http://localhost:8080/channels/inbound", {
1376
+ method: "POST",
1377
+ headers: {
1378
+ "Content-Type": "application/json",
1379
+ "X-Gateway-Origin": TEST_BEARER_TOKEN,
1380
+ },
1381
+ body: JSON.stringify(body),
1382
+ });
1383
+ }
1384
+
1385
+ interface SlackInboundProcessOptions {
1386
+ slackRuntimeContextNotice?: string;
1387
+ slackInbound?: {
1388
+ channelId: string;
1389
+ channelTs: string;
1390
+ threadTs?: string;
1391
+ displayName?: string;
1392
+ };
1393
+ }
1394
+
1395
+ function persistSlackInboundFromProcessMessage(
1396
+ conversationId: string,
1397
+ content: string,
1398
+ options?: SlackInboundProcessOptions,
1399
+ ): string {
1400
+ const slackInbound = options?.slackInbound;
1401
+ return insertMessage(conversationId, "user", content, {
1402
+ ...(slackInbound
1403
+ ? {
1404
+ slackMeta: writeSlackMetadata({
1405
+ source: "slack",
1406
+ channelId: slackInbound.channelId,
1407
+ channelTs: slackInbound.channelTs,
1408
+ ...(slackInbound.threadTs
1409
+ ? { threadTs: slackInbound.threadTs }
1410
+ : {}),
1411
+ ...(slackInbound.displayName
1412
+ ? { displayName: slackInbound.displayName }
1413
+ : {}),
1414
+ eventKind: "message",
1415
+ }),
1416
+ }
1417
+ : {}),
1418
+ });
1419
+ }
1420
+
1421
+ const SLACK_CHANNEL_CAPS: ChannelCapabilities = {
1422
+ channel: "slack",
1423
+ dashboardCapable: false,
1424
+ supportsDynamicUi: false,
1425
+ supportsVoiceInput: false,
1426
+ chatType: "channel",
1427
+ };
1428
+
1429
+ function flattenText(messages: Message[]): string {
1430
+ return messages
1431
+ .flatMap((message) => message.content)
1432
+ .filter((block): block is { type: "text"; text: string } => {
1433
+ return block.type === "text";
1434
+ })
1435
+ .map((block) => block.text)
1436
+ .join("\n");
1437
+ }
1438
+
730
1439
  describe("handleChannelInbound — Slack thread backfill wiring", () => {
731
1440
  beforeEach(() => {
732
1441
  resetHttpState();
@@ -736,6 +1445,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
736
1445
 
737
1446
  afterEach(() => {
738
1447
  backfillThreadMock.mockReset();
1448
+ installDefaultThreadPageMock();
739
1449
  _backfillTriggerCache.clear();
740
1450
  });
741
1451
 
@@ -755,9 +1465,22 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
755
1465
  }),
756
1466
  ]);
757
1467
 
758
- const processMessage = async (): Promise<{ messageId: string }> => {
1468
+ let capturedHints: string[] | undefined;
1469
+ let capturedSlackNotice: string | undefined;
1470
+ const processMessage = async (
1471
+ _conversationId: string,
1472
+ _content: string,
1473
+ _attachmentIds?: string[],
1474
+ options?: {
1475
+ transport?: { hints?: string[] };
1476
+ slackRuntimeContextNotice?: string;
1477
+ },
1478
+ ): Promise<{ messageId: string }> => {
1479
+ capturedHints = options?.transport?.hints;
1480
+ capturedSlackNotice = options?.slackRuntimeContextNotice;
759
1481
  return { messageId: "agent-result-id" };
760
1482
  };
1483
+ setAdapterProcessMessage(processMessage);
761
1484
 
762
1485
  const req = buildThreadReplyRequest("1234.0", "1234.3");
763
1486
  const resp = await handleChannelInbound(
@@ -774,7 +1497,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
774
1497
  // void-promise has time to write to the DB before we assert.
775
1498
  await new Promise((resolve) => setTimeout(resolve, 100));
776
1499
 
777
- expect(backfillThreadMock).toHaveBeenCalledTimes(1);
1500
+ expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(2);
778
1501
  const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
779
1502
  expect(calledChannel).toBe(HTTP_SLACK_CHANNEL_ID);
780
1503
  expect(calledThread).toBe("1234.0");
@@ -800,9 +1523,190 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
800
1523
 
801
1524
  expect(channelTimestamps.has("1234.0")).toBe(true);
802
1525
  expect(channelTimestamps.has("1234.1")).toBe(true);
1526
+
1527
+ expect(
1528
+ capturedHints?.some((hint) => hint.includes("joined an existing thread")),
1529
+ ).not.toBe(true);
1530
+ expect(capturedSlackNotice).toContain("joined an existing thread");
1531
+ const contents = db.$client
1532
+ .prepare("SELECT content FROM messages")
1533
+ .all() as Array<{ content: string }>;
1534
+ expect(
1535
+ contents.some((row) => row.content.includes("Slack context note")),
1536
+ ).toBe(false);
803
1537
  });
804
1538
 
805
- test("second thread reply within the TTL window does not re-trigger backfill", async () => {
1539
+ test("late app mention sees unseen backfilled replies before the mention", async () => {
1540
+ let capturedTranscript = "";
1541
+ let parentTurnSeen = false;
1542
+ let resolveParentTurn: (() => void) | undefined;
1543
+ let secondTurnSeen = false;
1544
+ const parentTurnProcessed = new Promise<void>((resolve) => {
1545
+ resolveParentTurn = resolve;
1546
+ });
1547
+ let resolveSecondTurn: (() => void) | undefined;
1548
+ const secondTurnProcessed = new Promise<void>((resolve) => {
1549
+ resolveSecondTurn = resolve;
1550
+ });
1551
+
1552
+ const processMessage = async (
1553
+ conversationId: string,
1554
+ content: string,
1555
+ _attachmentIds?: string[],
1556
+ options?: SlackInboundProcessOptions,
1557
+ ): Promise<{ messageId: string }> => {
1558
+ const messageId = persistSlackInboundFromProcessMessage(
1559
+ conversationId,
1560
+ content,
1561
+ options,
1562
+ );
1563
+ if (options?.slackInbound?.channelTs === "1234.0") {
1564
+ parentTurnSeen = true;
1565
+ resolveParentTurn?.();
1566
+ }
1567
+ if (options?.slackInbound?.channelTs === "1234.5") {
1568
+ const context = loadSlackChronologicalContext(
1569
+ conversationId,
1570
+ SLACK_CHANNEL_CAPS,
1571
+ {
1572
+ loader: readMessageRowsByConversation,
1573
+ trustClass: "guardian",
1574
+ },
1575
+ );
1576
+ capturedTranscript = context ? flattenText(context.messages) : "";
1577
+ secondTurnSeen = true;
1578
+ resolveSecondTurn?.();
1579
+ }
1580
+ return { messageId };
1581
+ };
1582
+ setAdapterProcessMessage(processMessage);
1583
+
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",
1590
+ },
1591
+ }),
1592
+ processMessage,
1593
+ TEST_BEARER_TOKEN,
1594
+ );
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
+
1602
+ backfillThreadMock.mockReset();
1603
+ backfillThreadMock.mockImplementation(async () => [
1604
+ makeBackfillMessage({
1605
+ id: "1234.1",
1606
+ text: "unseen first reply",
1607
+ threadId: "1234.0",
1608
+ sender: { id: "U_ONE", name: "Reply One" },
1609
+ }),
1610
+ makeBackfillMessage({
1611
+ id: "1234.2",
1612
+ text: "unseen second reply",
1613
+ threadId: "1234.0",
1614
+ sender: { id: "U_TWO", name: "Reply Two" },
1615
+ }),
1616
+ makeBackfillMessage({
1617
+ id: "1234.5",
1618
+ text: "live app mention should not be duplicated by backfill",
1619
+ threadId: "1234.0",
1620
+ sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
1621
+ }),
1622
+ ]);
1623
+
1624
+ const mentionResp = await handleChannelInbound(
1625
+ buildThreadReplyRequest("1234.0", "1234.5", {
1626
+ content: "<@U_ASSISTANT> please answer with the missing context",
1627
+ sourceMetadata: {
1628
+ messageId: "1234.5",
1629
+ threadId: "1234.0",
1630
+ chatType: "channel",
1631
+ eventType: "app_mention",
1632
+ },
1633
+ }),
1634
+ processMessage,
1635
+ TEST_BEARER_TOKEN,
1636
+ );
1637
+ expect(mentionResp.status).toBe(200);
1638
+
1639
+ await Promise.race([
1640
+ secondTurnProcessed,
1641
+ new Promise((resolve) => setTimeout(resolve, 250)),
1642
+ ]);
1643
+
1644
+ expect(secondTurnSeen).toBe(true);
1645
+ expect(backfillThreadMock).toHaveBeenCalledTimes(1);
1646
+ expect(backfillThreadMock.mock.calls[0][2]?.after).toBe("1234.0");
1647
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBe("1234.5");
1648
+
1649
+ const parentIndex = capturedTranscript.indexOf("parent already stored");
1650
+ const firstReplyIndex = capturedTranscript.indexOf("unseen first reply");
1651
+ const secondReplyIndex = capturedTranscript.indexOf("unseen second reply");
1652
+ const mentionIndex = capturedTranscript.indexOf(
1653
+ "please answer with the missing context",
1654
+ );
1655
+
1656
+ expect(parentIndex).toBeGreaterThanOrEqual(0);
1657
+ expect(firstReplyIndex).toBeGreaterThan(parentIndex);
1658
+ expect(secondReplyIndex).toBeGreaterThan(firstReplyIndex);
1659
+ expect(mentionIndex).toBeGreaterThan(secondReplyIndex);
1660
+ expect(
1661
+ capturedTranscript.match(/live app mention should not be duplicated/g),
1662
+ ).toBeNull();
1663
+ });
1664
+
1665
+ test("cold-start Slack DMs still use backfillDm without thread backfill", async () => {
1666
+ const dmChannelId = "D0HTTPDM";
1667
+ seedHttpActiveMember(dmChannelId);
1668
+ backfillDmMock.mockImplementation(async () => [
1669
+ makeBackfillMessage({
1670
+ id: "1700000000.000100",
1671
+ conversationId: dmChannelId,
1672
+ text: "earlier DM context",
1673
+ sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
1674
+ }),
1675
+ ]);
1676
+
1677
+ const processMessage = async (
1678
+ conversationId: string,
1679
+ content: string,
1680
+ _attachmentIds?: string[],
1681
+ options?: SlackInboundProcessOptions,
1682
+ ): Promise<{ messageId: string }> => ({
1683
+ messageId: persistSlackInboundFromProcessMessage(
1684
+ conversationId,
1685
+ content,
1686
+ options,
1687
+ ),
1688
+ });
1689
+ setAdapterProcessMessage(processMessage);
1690
+
1691
+ const resp = await handleChannelInbound(
1692
+ buildSlackDmRequest(dmChannelId, "1700000000.000200"),
1693
+ processMessage,
1694
+ TEST_BEARER_TOKEN,
1695
+ );
1696
+
1697
+ expect(resp.status).toBe(200);
1698
+ await new Promise((resolve) => setTimeout(resolve, 100));
1699
+
1700
+ expect(backfillDmMock).toHaveBeenCalledTimes(1);
1701
+ expect(backfillDmMock.mock.calls[0][0]).toBe(dmChannelId);
1702
+ expect(backfillDmMock.mock.calls[0][1]).toMatchObject({
1703
+ limit: 50,
1704
+ before: "1700000000.000200",
1705
+ });
1706
+ expect(backfillThreadMock).not.toHaveBeenCalled();
1707
+ });
1708
+
1709
+ test("second thread reply within the TTL window can fetch a newer bounded gap", async () => {
806
1710
  backfillThreadMock.mockImplementation(async () => [
807
1711
  makeBackfillMessage({ id: "5678.0", text: "parent" }),
808
1712
  ]);
@@ -827,7 +1731,17 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
827
1731
  expect(r2.status).toBe(200);
828
1732
  await new Promise((resolve) => setTimeout(resolve, 100));
829
1733
 
830
- expect(backfillThreadMock).toHaveBeenCalledTimes(1);
1734
+ expect(backfillThreadMock.mock.calls[0][2]?.before).toBeUndefined();
1735
+ expect(
1736
+ backfillThreadMock.mock.calls.some(
1737
+ (call) => call[2]?.before === "5678.1",
1738
+ ),
1739
+ ).toBe(true);
1740
+ expect(
1741
+ backfillThreadMock.mock.calls.some(
1742
+ (call) => call[2]?.before === "5678.2",
1743
+ ),
1744
+ ).toBe(true);
831
1745
  });
832
1746
 
833
1747
  test("backfill error from the HTTP path does not crash the request", async () => {