@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
@@ -45,11 +45,13 @@ import { upsertBinding } from "../../memory/external-conversation-store.js";
45
45
  import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
46
46
  import {
47
47
  backfillDm,
48
- backfillThread,
48
+ backfillThreadWindowPage,
49
+ type SlackBackfillWindowPage,
49
50
  } from "../../messaging/providers/slack/backfill.js";
50
51
  import {
51
52
  mergeSlackMetadata,
52
53
  readSlackMetadata,
54
+ type SlackFileMetadata,
53
55
  type SlackMessageMetadata,
54
56
  writeSlackMetadata,
55
57
  } from "../../messaging/providers/slack/message-metadata.js";
@@ -71,7 +73,6 @@ import { handleGuardianActivationIntercept } from "./inbound-stages/guardian-act
71
73
  import { handleGuardianReplyIntercept } from "./inbound-stages/guardian-reply-intercept.js";
72
74
  import { runSecretIngressCheck } from "./inbound-stages/secret-ingress-check.js";
73
75
  import { tryTranscribeAudioAttachments } from "./inbound-stages/transcribe-audio.js";
74
- import { handleVerificationIntercept } from "./inbound-stages/verification-intercept.js";
75
76
  import type { RouteHandlerArgs } from "./types.js";
76
77
 
77
78
  const log = getLogger("runtime-http");
@@ -263,7 +264,7 @@ export async function handleChannelInbound({
263
264
  externalMessageId,
264
265
  });
265
266
  if (aclResult.earlyResponse) return aclResult.earlyResponse;
266
- const { resolvedMember, guardianVerifyCode } = aclResult;
267
+ const { resolvedMember } = aclResult;
267
268
 
268
269
  // ── Slack delete propagation ──
269
270
  // Slack message_deleted events are forwarded by the gateway with the
@@ -286,7 +287,7 @@ export async function handleChannelInbound({
286
287
  { conversationExternalId },
287
288
  "Slack message_deleted event missing sourceMetadata.messageId; ignoring",
288
289
  );
289
- return ({ accepted: true, deleted: false });
290
+ return { accepted: true, deleted: false };
290
291
  }
291
292
 
292
293
  // Look up the stored message via the existing channel-event lookup.
@@ -327,7 +328,7 @@ export async function handleChannelInbound({
327
328
  { conversationExternalId, deletedMessageTs },
328
329
  "No stored message found for Slack delete after retries; ignoring",
329
330
  );
330
- return ({ accepted: true, deleted: false });
331
+ return { accepted: true, deleted: false };
331
332
  }
332
333
 
333
334
  // Merge deletedAt into the existing slackMeta sub-key. If the row has
@@ -345,7 +346,7 @@ export async function handleChannelInbound({
345
346
  },
346
347
  "Stored Slack message has no metadata; skipping delete marker",
347
348
  );
348
- return ({ accepted: true, deleted: false });
349
+ return { accepted: true, deleted: false };
349
350
  }
350
351
 
351
352
  let parentMetadata: Record<string, unknown>;
@@ -365,7 +366,7 @@ export async function handleChannelInbound({
365
366
  },
366
367
  "Failed to parse stored metadata; skipping delete marker",
367
368
  );
368
- return ({ accepted: true, deleted: false });
369
+ return { accepted: true, deleted: false };
369
370
  }
370
371
 
371
372
  const existingSlackMeta =
@@ -382,7 +383,7 @@ export async function handleChannelInbound({
382
383
  },
383
384
  "Stored Slack message has no slackMeta; skipping delete marker",
384
385
  );
385
- return ({ accepted: true, deleted: false });
386
+ return { accepted: true, deleted: false };
386
387
  }
387
388
 
388
389
  const updatedSlackMeta = mergeSlackMetadata(existingSlackMeta, {
@@ -404,11 +405,11 @@ export async function handleChannelInbound({
404
405
  "Marked Slack message as deleted",
405
406
  );
406
407
 
407
- return ({
408
+ return {
408
409
  accepted: true,
409
410
  deleted: true,
410
411
  messageId: original.messageId,
411
- });
412
+ };
412
413
  }
413
414
 
414
415
  if (hasAttachments) {
@@ -416,7 +417,9 @@ export async function handleChannelInbound({
416
417
  if (resolved.length !== attachmentIds.length) {
417
418
  const resolvedIds = new Set(resolved.map((a) => a.id));
418
419
  const missing = attachmentIds.filter((id) => !resolvedIds.has(id));
419
- throw new BadRequestError(`Attachment IDs not found: ${missing.join(", ")}`);
420
+ throw new BadRequestError(
421
+ `Attachment IDs not found: ${missing.join(", ")}`,
422
+ );
420
423
  }
421
424
  }
422
425
 
@@ -500,11 +503,11 @@ export async function handleChannelInbound({
500
503
  "Retry of pending verification reply failed; will retry on next duplicate",
501
504
  );
502
505
  }
503
- return ({
506
+ return {
504
507
  accepted: true,
505
508
  duplicate: true,
506
509
  eventId: result.eventId,
507
- });
510
+ };
508
511
  }
509
512
  }
510
513
 
@@ -541,10 +544,10 @@ export async function handleChannelInbound({
541
544
  // guardian approval reactions have no transcript representation.
542
545
  // 2. All other reactions (non-guardian, no pending approval, stale,
543
546
  // and any `reaction_removed:` event regardless of actor) fall
544
- // through to `persistSlackReactionAsMessage` so the chronological
545
- // renderer (PR 18) can surface them inline. Reactions never
546
- // trigger an agent response, so we short-circuit before
547
- // escalation and agent-loop dispatch in both cases.
547
+ // through to `persistSlackReactionAsMessage` so Slack transcript
548
+ // rendering can surface them inline. Reactions never trigger an
549
+ // agent response, so we short-circuit before escalation and
550
+ // agent-loop dispatch in both cases.
548
551
  if (isSlackReactionEvent(body)) {
549
552
  // Approval interception runs only for reactions (added) — `reaction_removed`
550
553
  // never expresses an approval intent, so un-reacting is left as a pure
@@ -586,12 +589,12 @@ export async function handleChannelInbound({
586
589
  // transcript line. All other interception outcomes (stale_ignored,
587
590
  // non-guardian, no pending approval) fall through to persistence.
588
591
  if (reactionApprovalResult.type === "guardian_decision_applied") {
589
- return ({
592
+ return {
590
593
  accepted: true,
591
594
  duplicate: false,
592
595
  eventId: result.eventId,
593
596
  approval: reactionApprovalResult.type,
594
- });
597
+ };
595
598
  }
596
599
  }
597
600
 
@@ -604,11 +607,11 @@ export async function handleChannelInbound({
604
607
  { conversationId: result.conversationId, eventId: result.eventId },
605
608
  "Skipping reaction persistence: missing sourceMetadata.messageId",
606
609
  );
607
- return ({
610
+ return {
608
611
  accepted: result.accepted,
609
612
  duplicate: result.duplicate,
610
613
  eventId: result.eventId,
611
- });
614
+ };
612
615
  }
613
616
 
614
617
  const threadTs =
@@ -634,11 +637,11 @@ export async function handleChannelInbound({
634
637
  );
635
638
  }
636
639
 
637
- return ({
640
+ return {
638
641
  accepted: result.accepted,
639
642
  duplicate: result.duplicate,
640
643
  eventId: result.eventId,
641
- });
644
+ };
642
645
  }
643
646
 
644
647
  // ── Ingress escalation ──
@@ -670,6 +673,7 @@ export async function handleChannelInbound({
670
673
  typeof hint === "string" && hint.trim().length > 0,
671
674
  )
672
675
  : [];
676
+ let slackRuntimeContextNotice: string | undefined;
673
677
 
674
678
  // Inject channel-scoped permission hints for Slack channel messages
675
679
  if (sourceChannel === "slack") {
@@ -734,24 +738,6 @@ export async function handleChannelInbound({
734
738
  });
735
739
  if (bootstrapResponse) return bootstrapResponse;
736
740
 
737
- // ── Guardian verification code intercept (deterministic) ──
738
- const verificationResponse = await handleVerificationIntercept({
739
- isDuplicate: result.duplicate,
740
- guardianVerifyCode,
741
- rawSenderId,
742
- canonicalSenderId,
743
- canonicalAssistantId,
744
- sourceChannel,
745
- conversationExternalId,
746
- conversationId: result.conversationId,
747
- eventId: result.eventId,
748
- replyCallbackUrl,
749
- assistantId,
750
- actorDisplayName: body.actorDisplayName,
751
- actorUsername: body.actorUsername,
752
- });
753
- if (verificationResponse) return verificationResponse;
754
-
755
741
  // Legacy voice guardian action interception removed — all guardian reply
756
742
  // routing now flows through the canonical router below (routeGuardianReply),
757
743
  // which handles request code matching, callback parsing, and NL classification
@@ -862,12 +848,12 @@ export async function handleChannelInbound({
862
848
  }
863
849
  }
864
850
 
865
- return ({
851
+ return {
866
852
  accepted: true,
867
853
  duplicate: false,
868
854
  eventId: result.eventId,
869
855
  approval: approvalResult.type,
870
- });
856
+ };
871
857
  }
872
858
 
873
859
  // When a callback payload was not handled by approval interception, it's
@@ -922,12 +908,12 @@ export async function handleChannelInbound({
922
908
  });
923
909
  }
924
910
 
925
- return ({
911
+ return {
926
912
  accepted: true,
927
913
  duplicate: false,
928
914
  eventId: result.eventId,
929
915
  approval: "stale_ignored",
930
- });
916
+ };
931
917
  }
932
918
  }
933
919
 
@@ -1032,25 +1018,27 @@ export async function handleChannelInbound({
1032
1018
  });
1033
1019
  }
1034
1020
 
1035
- // ── Thread-ancestor backfill ──
1036
- // When a Slack reply arrives for a thread the daemon never saw the
1037
- // parent of, fetch the thread's recent history from Slack and persist
1038
- // the missing messages so the chronological renderer (PR 18) has the
1039
- // full conversation. Awaited (mirrors the DM cold-start path above)
1040
- // so the agent loop dispatched immediately afterwards observes the
1041
- // backfilled parent without this, `loadSlackChronologicalMessages`
1042
- // can race the persist and miss thread context. Backfill is bounded
1043
- // (parent + ~50 messages) and the agent latency is dominated by the
1044
- // LLM call, so the added latency is negligible. Failures are
1045
- // swallowed inside the helper so they never block dispatch.
1021
+ // ── Thread gap/delta backfill ──
1022
+ // When a Slack thread reply arrives, compare the stored thread state
1023
+ // with the inbound message's ts and fetch only the bounded unseen
1024
+ // window. Initial late-join turns hydrate the earliest thread messages
1025
+ // plus a recent window adjacent to the inbound reply; later turns use
1026
+ // a delta window after the latest stored thread ts and before the
1027
+ // inbound ts. Awaited (mirrors the DM cold-start path above) so the
1028
+ // agent loop dispatched immediately afterwards observes hydrated
1029
+ // context. A late-join notice is added only to the current turn's
1030
+ // runtime context, not persisted as durable Slack metadata. Failures
1031
+ // are swallowed inside the helper so they never block dispatch.
1046
1032
  if (slackThreadTs) {
1047
- await triggerSlackThreadBackfillIfNeeded({
1033
+ const backfillResult = await triggerSlackThreadBackfillIfNeeded({
1048
1034
  conversationId: result.conversationId,
1049
1035
  channelId: conversationExternalId,
1050
1036
  threadTs: slackThreadTs,
1051
1037
  excludeChannelTs: slackInbound?.channelTs,
1052
1038
  account: slackAccount,
1053
1039
  });
1040
+ const lateJoinNotice = buildSlackLateJoinNotice(backfillResult);
1041
+ if (lateJoinNotice) slackRuntimeContextNotice = lateJoinNotice;
1054
1042
  }
1055
1043
 
1056
1044
  // Wrap non-guardian inbound content in external_content boundaries so
@@ -1078,6 +1066,7 @@ export async function handleChannelInbound({
1078
1066
  externalChatId: conversationExternalId,
1079
1067
  trustCtx,
1080
1068
  metadataHints,
1069
+ slackRuntimeContextNotice,
1081
1070
  metadataUxBrief,
1082
1071
  commandIntent,
1083
1072
  sourceLanguageCode,
@@ -1090,11 +1079,11 @@ export async function handleChannelInbound({
1090
1079
  }
1091
1080
  }
1092
1081
 
1093
- return ({
1082
+ return {
1094
1083
  accepted: result.accepted,
1095
1084
  duplicate: result.duplicate,
1096
1085
  eventId: result.eventId,
1097
- });
1086
+ };
1098
1087
  }
1099
1088
 
1100
1089
  /**
@@ -1193,8 +1182,8 @@ async function persistSlackReactionAsMessage(params: {
1193
1182
  },
1194
1183
  };
1195
1184
 
1196
- // Sentinel content — renderers (PR 18) read `slackMeta` to format the
1197
- // reaction line; the literal text is never displayed to the model.
1185
+ // Sentinel content — Slack transcript renderers read `slackMeta` to format
1186
+ // the reaction line; the literal text is never displayed to the model.
1198
1187
  const persisted = await addMessage(
1199
1188
  params.conversationId,
1200
1189
  "user",
@@ -1273,19 +1262,7 @@ function countSlackMetaMessages(conversationId: string): number {
1273
1262
  );
1274
1263
  if (candidates.length === 0) return count;
1275
1264
  for (const raw of candidates) {
1276
- let parent: Record<string, unknown> | null = null;
1277
- try {
1278
- const parsed = JSON.parse(raw) as unknown;
1279
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1280
- parent = parsed as Record<string, unknown>;
1281
- }
1282
- } catch {
1283
- continue;
1284
- }
1285
- if (!parent) continue;
1286
- const inner = parent.slackMeta;
1287
- if (typeof inner !== "string") continue;
1288
- if (readSlackMetadata(inner)) {
1265
+ if (readSlackMetadataFromMessageMetadata(raw)) {
1289
1266
  count++;
1290
1267
  if (count >= SLACK_DM_BACKFILL_WARM_THRESHOLD) return count;
1291
1268
  }
@@ -1296,44 +1273,110 @@ function countSlackMetaMessages(conversationId: string): number {
1296
1273
  return count;
1297
1274
  }
1298
1275
 
1276
+ function readSlackMetadataFromMessageMetadata(
1277
+ metadata: string | null | undefined,
1278
+ ): SlackMessageMetadata | null {
1279
+ if (!metadata) return null;
1280
+ let parent: Record<string, unknown> | null = null;
1281
+ try {
1282
+ const parsed = JSON.parse(metadata) as unknown;
1283
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1284
+ parent = parsed as Record<string, unknown>;
1285
+ }
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ if (!parent) return null;
1290
+ const raw = parent.slackMeta;
1291
+ if (typeof raw !== "string") return null;
1292
+ return readSlackMetadata(raw);
1293
+ }
1294
+
1299
1295
  /**
1300
1296
  * Build the set of `slackMeta.channelTs` values already stored on a
1301
- * conversation. Used by both DM cold-start backfill and thread-ancestor
1297
+ * conversation. Used by both DM cold-start backfill and thread gap/delta
1302
1298
  * backfill to dedupe rows so a partial prior backfill (or a single message
1303
1299
  * that was already persisted via the live ingress path) does not double-write.
1304
1300
  */
1305
1301
  function readStoredSlackChannelTs(conversationId: string): Set<string> {
1306
1302
  const seen = new Set<string>();
1307
1303
  for (const row of getMessages(conversationId)) {
1308
- if (!row.metadata) continue;
1309
- let parent: Record<string, unknown> | null = null;
1310
- try {
1311
- const parsed = JSON.parse(row.metadata) as unknown;
1312
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1313
- parent = parsed as Record<string, unknown>;
1314
- }
1315
- } catch {
1316
- continue;
1317
- }
1318
- if (!parent) continue;
1319
- const raw = parent.slackMeta;
1320
- if (typeof raw !== "string") continue;
1321
- const meta = readSlackMetadata(raw);
1304
+ const meta = readSlackMetadataFromMessageMetadata(row.metadata);
1322
1305
  // Only message rows represent stored Slack messages. Reaction rows carry
1323
1306
  // `channelTs` equal to the target message's ts, so including them would
1324
- // make a reaction on a thread parent wrongly short-circuit ancestor
1307
+ // make a reaction on a thread parent wrongly short-circuit thread
1325
1308
  // backfill (the parent itself may still be unseen).
1326
1309
  if (meta && meta.eventKind === "message") seen.add(meta.channelTs);
1327
1310
  }
1328
1311
  return seen;
1329
1312
  }
1330
1313
 
1314
+ interface ParsedSlackTimestamp {
1315
+ seconds: bigint;
1316
+ micros: bigint;
1317
+ }
1318
+
1319
+ function parseSlackTimestamp(
1320
+ ts: string | undefined,
1321
+ ): ParsedSlackTimestamp | null {
1322
+ if (!ts) return null;
1323
+ const match = /^(\d+)\.(\d{1,6})$/.exec(ts);
1324
+ if (!match) return null;
1325
+ const micros = BigInt(match[2]);
1326
+ if (micros > 999_999n) return null;
1327
+ return {
1328
+ seconds: BigInt(match[1]),
1329
+ micros,
1330
+ };
1331
+ }
1332
+
1333
+ function compareSlackTimestamps(left: string, right: string): number | null {
1334
+ const parsedLeft = parseSlackTimestamp(left);
1335
+ const parsedRight = parseSlackTimestamp(right);
1336
+ if (!parsedLeft || !parsedRight) return null;
1337
+ if (parsedLeft.seconds < parsedRight.seconds) return -1;
1338
+ if (parsedLeft.seconds > parsedRight.seconds) return 1;
1339
+ if (parsedLeft.micros < parsedRight.micros) return -1;
1340
+ if (parsedLeft.micros > parsedRight.micros) return 1;
1341
+ return 0;
1342
+ }
1343
+
1344
+ interface StoredSlackThreadState {
1345
+ storedChannelTs: Set<string>;
1346
+ latestStoredThreadTs: string | undefined;
1347
+ }
1348
+
1349
+ function readStoredSlackThreadState(
1350
+ conversationId: string,
1351
+ threadTs: string,
1352
+ ): StoredSlackThreadState {
1353
+ const storedChannelTs = new Set<string>();
1354
+ let latestStoredThreadTs: string | undefined;
1355
+
1356
+ for (const row of getMessages(conversationId)) {
1357
+ const meta = readSlackMetadataFromMessageMetadata(row.metadata);
1358
+ if (!meta || meta.eventKind !== "message") continue;
1359
+ if (meta.channelTs !== threadTs && meta.threadTs !== threadTs) continue;
1360
+
1361
+ storedChannelTs.add(meta.channelTs);
1362
+ if (!parseSlackTimestamp(meta.channelTs)) continue;
1363
+ if (
1364
+ latestStoredThreadTs === undefined ||
1365
+ compareSlackTimestamps(meta.channelTs, latestStoredThreadTs) === 1
1366
+ ) {
1367
+ latestStoredThreadTs = meta.channelTs;
1368
+ }
1369
+ }
1370
+
1371
+ return { storedChannelTs, latestStoredThreadTs };
1372
+ }
1373
+
1331
1374
  /**
1332
1375
  * Persist a single backfilled Slack message as a `messages` row with a
1333
1376
  * `slackMeta` envelope.
1334
1377
  *
1335
1378
  * Shared insertion point for any path that hydrates Slack history lazily
1336
- * (DM cold-start backfill, thread-ancestor backfill, etc.). Role is derived
1379
+ * (DM cold-start backfill, thread gap/delta backfill, etc.). Role is derived
1337
1380
  * from `message.metadata.isBot` — bot-authored rows map to `"assistant"` so
1338
1381
  * our own prior replies (and any other bot traffic) are not rehydrated as
1339
1382
  * user turns, which would otherwise corrupt speaker attribution and make
@@ -1347,6 +1390,7 @@ async function persistBackfilledSlackMessage(params: {
1347
1390
  message: ProviderMessage;
1348
1391
  }): Promise<void> {
1349
1392
  const { message } = params;
1393
+ const slackFiles = readSlackFilesFromProviderMetadata(message.metadata);
1350
1394
  const slackMeta: SlackMessageMetadata = {
1351
1395
  source: "slack",
1352
1396
  channelId: params.channelId,
@@ -1354,6 +1398,7 @@ async function persistBackfilledSlackMessage(params: {
1354
1398
  eventKind: "message",
1355
1399
  ...(message.threadId ? { threadTs: message.threadId } : {}),
1356
1400
  ...(message.sender?.name ? { displayName: message.sender.name } : {}),
1401
+ ...(slackFiles.length > 0 ? { slackFiles } : {}),
1357
1402
  };
1358
1403
  const role = message.metadata?.isBot === true ? "assistant" : "user";
1359
1404
  await addMessage(params.conversationId, role, message.text ?? "", {
@@ -1361,6 +1406,32 @@ async function persistBackfilledSlackMessage(params: {
1361
1406
  });
1362
1407
  }
1363
1408
 
1409
+ function readSlackFilesFromProviderMetadata(
1410
+ metadata: Record<string, unknown> | undefined,
1411
+ ): SlackFileMetadata[] {
1412
+ const raw = metadata?.slackFiles;
1413
+ if (!Array.isArray(raw)) return [];
1414
+ const files: SlackFileMetadata[] = [];
1415
+ for (const item of raw) {
1416
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
1417
+ continue;
1418
+ }
1419
+ const record = item as Record<string, unknown>;
1420
+ const name = typeof record.name === "string" ? record.name.trim() : "";
1421
+ if (!name) continue;
1422
+ files.push({
1423
+ ...(typeof record.id === "string" && record.id.length > 0
1424
+ ? { id: record.id }
1425
+ : {}),
1426
+ name,
1427
+ ...(typeof record.mimetype === "string" && record.mimetype.length > 0
1428
+ ? { mimetype: record.mimetype }
1429
+ : {}),
1430
+ });
1431
+ }
1432
+ return files;
1433
+ }
1434
+
1364
1435
  /**
1365
1436
  * In-memory map of in-flight DM cold-start backfills keyed by conversationId.
1366
1437
  * Concurrent inbound DMs to the same cold conversation share a single
@@ -1503,13 +1574,11 @@ async function runBackfillSlackDmIfCold(params: {
1503
1574
  // ---------------------------------------------------------------------------
1504
1575
 
1505
1576
  /**
1506
- * In-memory TTL cache keyed by `<conversationId>:<threadTs>`. Tracks recent
1507
- * thread-backfill triggers so a burst of replies inside the same Slack
1508
- * thread (e.g. a guardian rapidly typing several lines) does not re-fetch
1509
- * the same parent messages from Slack repeatedly. Entries naturally fall
1510
- * out after the TTL — if the thread is still active later, a fresh
1511
- * backfill becomes a cheap "are the parents already stored?" DB lookup
1512
- * that short-circuits before the Slack API is touched.
1577
+ * In-memory TTL cache keyed by
1578
+ * `<conversationId>:<threadTs>:<lowerBoundTs>:<upperBoundTs>`. Tracks recent
1579
+ * thread-backfill windows so repeated triggers for the same Slack gap do not
1580
+ * re-fetch identical rows while later replies in the same thread can still
1581
+ * request newer unseen windows.
1513
1582
  *
1514
1583
  * Exported only for tests; production callers should use
1515
1584
  * {@link triggerSlackThreadBackfillIfNeeded}.
@@ -1518,6 +1587,39 @@ export const _backfillTriggerCache = new Map<string, number>();
1518
1587
 
1519
1588
  const BACKFILL_TRIGGER_TTL_MS = 10 * 60 * 1000; // 10 minutes
1520
1589
  const BACKFILL_TRIGGER_CACHE_MAX = 1_000;
1590
+ const SLACK_THREAD_INITIAL_EARLY_LIMIT = 25;
1591
+ const SLACK_THREAD_INITIAL_RECENT_LIMIT = 50;
1592
+ const SLACK_THREAD_INITIAL_RECENT_MAX_PAGES = 5;
1593
+ const SLACK_THREAD_DELTA_LIMIT = 50;
1594
+ const SLACK_THREAD_UPPER_ADJACENT_MAX_ATTEMPTS = 5;
1595
+ const MICROS_PER_SECOND = 1_000_000n;
1596
+ const SLACK_UPPER_ADJACENT_EXPANDING_WINDOWS_MICROS = [
1597
+ 5n * 60n * MICROS_PER_SECOND,
1598
+ 60n * 60n * MICROS_PER_SECOND,
1599
+ 24n * 60n * 60n * MICROS_PER_SECOND,
1600
+ 7n * 24n * 60n * 60n * MICROS_PER_SECOND,
1601
+ 30n * 24n * 60n * 60n * MICROS_PER_SECOND,
1602
+ ];
1603
+ const SLACK_UPPER_ADJACENT_SHRINKING_WINDOWS_MICROS = [
1604
+ 60n * MICROS_PER_SECOND,
1605
+ 10n * MICROS_PER_SECOND,
1606
+ MICROS_PER_SECOND,
1607
+ 100_000n,
1608
+ 1_000n,
1609
+ ];
1610
+
1611
+ export interface SlackThreadBackfillResult {
1612
+ fetched: number;
1613
+ persisted: number;
1614
+ reason?: SlackBackfillReason;
1615
+ omittedMiddle: boolean;
1616
+ }
1617
+
1618
+ type SlackBackfillReason = "thread_late_join" | "thread_delta";
1619
+
1620
+ function emptySlackThreadBackfillResult(): SlackThreadBackfillResult {
1621
+ return { fetched: 0, persisted: 0, omittedMiddle: false };
1622
+ }
1521
1623
 
1522
1624
  function pruneBackfillCacheIfNeeded(): void {
1523
1625
  if (_backfillTriggerCache.size < BACKFILL_TRIGGER_CACHE_MAX) return;
@@ -1552,23 +1654,315 @@ function isBackfillRecentlyTriggered(cacheKey: string): boolean {
1552
1654
  return true;
1553
1655
  }
1554
1656
 
1657
+ interface SlackInitialThreadWindowsResult {
1658
+ messages: ProviderMessage[];
1659
+ omittedMiddle: boolean;
1660
+ }
1661
+
1662
+ interface SlackUpperAdjacentWindowResult {
1663
+ messages: ProviderMessage[];
1664
+ omittedEarlierContent: boolean;
1665
+ truncatedBeforeUpperBound: boolean;
1666
+ }
1667
+
1668
+ function slackPageHasMore(page: SlackBackfillWindowPage): boolean {
1669
+ return page.hasMore || page.nextCursor !== undefined;
1670
+ }
1671
+
1672
+ function minSlackMessageTs(messages: ProviderMessage[]): string | undefined {
1673
+ return sortSlackProviderMessages(messages)[0]?.id;
1674
+ }
1675
+
1676
+ function maxSlackMessageTs(messages: ProviderMessage[]): string | undefined {
1677
+ const sorted = sortSlackProviderMessages(messages);
1678
+ return sorted[sorted.length - 1]?.id;
1679
+ }
1680
+
1681
+ function slackTimestampToMicros(ts: string | undefined): bigint | null {
1682
+ const parsed = parseSlackTimestamp(ts);
1683
+ if (!parsed) return null;
1684
+ return parsed.seconds * MICROS_PER_SECOND + parsed.micros;
1685
+ }
1686
+
1687
+ function slackTimestampFromMicros(totalMicros: bigint): string | undefined {
1688
+ if (totalMicros < 0n) return undefined;
1689
+ const seconds = totalMicros / MICROS_PER_SECOND;
1690
+ const micros = totalMicros % MICROS_PER_SECOND;
1691
+ return `${seconds.toString()}.${micros.toString().padStart(6, "0")}`;
1692
+ }
1693
+
1694
+ function didInitialWindowsLeaveGap(params: {
1695
+ early: SlackBackfillWindowPage;
1696
+ recent: SlackBackfillWindowPage;
1697
+ recentScanTruncated: boolean;
1698
+ }): boolean {
1699
+ if (params.recentScanTruncated) return true;
1700
+ if (!slackPageHasMore(params.early)) return false;
1701
+ const earlyMax = maxSlackMessageTs(params.early.messages);
1702
+ const recentMin = minSlackMessageTs(params.recent.messages);
1703
+ if (!earlyMax || !recentMin) return false;
1704
+ const compared = compareSlackTimestamps(earlyMax, recentMin);
1705
+ return compared !== null && compared < 0;
1706
+ }
1707
+
1708
+ async function fetchSlackThreadUpperAdjacentWindow(params: {
1709
+ channelId: string;
1710
+ threadTs: string;
1711
+ upperBoundTs: string;
1712
+ lowerBoundTs?: string;
1713
+ limit: number;
1714
+ account?: string;
1715
+ maxAttempts?: number;
1716
+ }): Promise<SlackUpperAdjacentWindowResult> {
1717
+ // Slack returns bounded conversations.replies pages earliest-first. To keep
1718
+ // the context closest to the inbound mention, narrow by timestamp instead
1719
+ // of cursoring forward from the oldest page in the bounded range.
1720
+ const upperMicros = slackTimestampToMicros(params.upperBoundTs);
1721
+ if (upperMicros === null) {
1722
+ const page = await backfillThreadWindowPage(
1723
+ params.channelId,
1724
+ params.threadTs,
1725
+ {
1726
+ limit: params.limit,
1727
+ account: params.account,
1728
+ before: params.upperBoundTs,
1729
+ ...(params.lowerBoundTs !== undefined
1730
+ ? { after: params.lowerBoundTs }
1731
+ : {}),
1732
+ },
1733
+ );
1734
+ return {
1735
+ messages: page.messages,
1736
+ omittedEarlierContent: slackPageHasMore(page),
1737
+ truncatedBeforeUpperBound: slackPageHasMore(page),
1738
+ };
1739
+ }
1740
+
1741
+ const lowerMicros = slackTimestampToMicros(params.lowerBoundTs);
1742
+ const maxAttempts =
1743
+ params.maxAttempts ?? SLACK_THREAD_UPPER_ADJACENT_MAX_ATTEMPTS;
1744
+ let attempts = 0;
1745
+ let safePage: SlackBackfillWindowPage | undefined;
1746
+ let safeAfterTs: string | undefined;
1747
+ let truncatedBeforeUpperBound = false;
1748
+
1749
+ const fetchWindow = async (
1750
+ windowMicros: bigint,
1751
+ ): Promise<{
1752
+ page: SlackBackfillWindowPage;
1753
+ after?: string;
1754
+ reachedLowerBound: boolean;
1755
+ }> => {
1756
+ let candidateMicros = upperMicros - windowMicros;
1757
+ let reachedLowerBound = false;
1758
+ if (lowerMicros !== null && candidateMicros <= lowerMicros) {
1759
+ candidateMicros = lowerMicros;
1760
+ reachedLowerBound = true;
1761
+ }
1762
+ const after = reachedLowerBound
1763
+ ? params.lowerBoundTs
1764
+ : slackTimestampFromMicros(candidateMicros);
1765
+ const page = await backfillThreadWindowPage(
1766
+ params.channelId,
1767
+ params.threadTs,
1768
+ {
1769
+ limit: params.limit,
1770
+ account: params.account,
1771
+ before: params.upperBoundTs,
1772
+ ...(after !== undefined ? { after } : {}),
1773
+ },
1774
+ );
1775
+ attempts++;
1776
+ return { page, after, reachedLowerBound };
1777
+ };
1778
+
1779
+ const considerWindow = async (windowMicros: bigint): Promise<boolean> => {
1780
+ const { page, after, reachedLowerBound } = await fetchWindow(windowMicros);
1781
+ if (slackPageHasMore(page)) {
1782
+ truncatedBeforeUpperBound = true;
1783
+ return false;
1784
+ }
1785
+
1786
+ safePage = page;
1787
+ safeAfterTs = after;
1788
+ return page.messages.length < params.limit && !reachedLowerBound;
1789
+ };
1790
+
1791
+ for (const windowMicros of SLACK_UPPER_ADJACENT_EXPANDING_WINDOWS_MICROS) {
1792
+ if (attempts >= maxAttempts) break;
1793
+ const shouldExpand = await considerWindow(windowMicros);
1794
+ if (!shouldExpand) break;
1795
+ }
1796
+
1797
+ if (truncatedBeforeUpperBound && !safePage && attempts < maxAttempts) {
1798
+ for (const windowMicros of SLACK_UPPER_ADJACENT_SHRINKING_WINDOWS_MICROS) {
1799
+ if (attempts >= maxAttempts) break;
1800
+ await considerWindow(windowMicros);
1801
+ if (safePage) break;
1802
+ }
1803
+ }
1804
+
1805
+ if (!safePage) {
1806
+ const after = slackTimestampFromMicros(upperMicros - 2n);
1807
+ const page = await backfillThreadWindowPage(
1808
+ params.channelId,
1809
+ params.threadTs,
1810
+ {
1811
+ limit: params.limit,
1812
+ account: params.account,
1813
+ before: params.upperBoundTs,
1814
+ ...(after !== undefined ? { after } : {}),
1815
+ },
1816
+ );
1817
+ safePage = page;
1818
+ safeAfterTs = after;
1819
+ truncatedBeforeUpperBound =
1820
+ truncatedBeforeUpperBound || slackPageHasMore(page);
1821
+ }
1822
+ if (!safePage) {
1823
+ return {
1824
+ messages: [],
1825
+ omittedEarlierContent: true,
1826
+ truncatedBeforeUpperBound: true,
1827
+ };
1828
+ }
1829
+
1830
+ let omittedEarlierContent = truncatedBeforeUpperBound;
1831
+ if (
1832
+ !omittedEarlierContent &&
1833
+ params.lowerBoundTs !== undefined &&
1834
+ safeAfterTs !== undefined &&
1835
+ compareSlackTimestamps(params.lowerBoundTs, safeAfterTs) === -1
1836
+ ) {
1837
+ const coverageProbe = await backfillThreadWindowPage(
1838
+ params.channelId,
1839
+ params.threadTs,
1840
+ {
1841
+ limit: 1,
1842
+ account: params.account,
1843
+ after: params.lowerBoundTs,
1844
+ before: safeAfterTs,
1845
+ },
1846
+ );
1847
+ omittedEarlierContent =
1848
+ coverageProbe.messages.length > 0 || slackPageHasMore(coverageProbe);
1849
+ }
1850
+
1851
+ return {
1852
+ messages: safePage.messages,
1853
+ omittedEarlierContent,
1854
+ truncatedBeforeUpperBound,
1855
+ };
1856
+ }
1857
+
1858
+ async function fetchInitialSlackThreadWindows(params: {
1859
+ channelId: string;
1860
+ threadTs: string;
1861
+ upperBoundTs?: string;
1862
+ account?: string;
1863
+ }): Promise<SlackInitialThreadWindowsResult> {
1864
+ if (!params.upperBoundTs) {
1865
+ const early = await backfillThreadWindowPage(
1866
+ params.channelId,
1867
+ params.threadTs,
1868
+ {
1869
+ limit: SLACK_THREAD_INITIAL_EARLY_LIMIT,
1870
+ account: params.account,
1871
+ },
1872
+ );
1873
+ return {
1874
+ messages: sortSlackProviderMessages(
1875
+ dedupeSlackProviderMessages(early.messages),
1876
+ ),
1877
+ omittedMiddle: slackPageHasMore(early),
1878
+ };
1879
+ }
1880
+ const [early, recentResult] = await Promise.all([
1881
+ backfillThreadWindowPage(params.channelId, params.threadTs, {
1882
+ limit: SLACK_THREAD_INITIAL_EARLY_LIMIT,
1883
+ account: params.account,
1884
+ }),
1885
+ fetchSlackThreadUpperAdjacentWindow({
1886
+ channelId: params.channelId,
1887
+ threadTs: params.threadTs,
1888
+ account: params.account,
1889
+ upperBoundTs: params.upperBoundTs,
1890
+ limit: SLACK_THREAD_INITIAL_RECENT_LIMIT,
1891
+ maxAttempts: SLACK_THREAD_INITIAL_RECENT_MAX_PAGES,
1892
+ }),
1893
+ ]);
1894
+ const recent: SlackBackfillWindowPage = {
1895
+ messages: recentResult.messages,
1896
+ hasMore: recentResult.truncatedBeforeUpperBound,
1897
+ };
1898
+ return {
1899
+ messages: sortSlackProviderMessages(
1900
+ dedupeSlackProviderMessages([...early.messages, ...recent.messages]),
1901
+ ),
1902
+ omittedMiddle:
1903
+ recentResult.omittedEarlierContent ||
1904
+ didInitialWindowsLeaveGap({
1905
+ early,
1906
+ recent,
1907
+ recentScanTruncated: recentResult.truncatedBeforeUpperBound,
1908
+ }),
1909
+ };
1910
+ }
1911
+
1912
+ function dedupeSlackProviderMessages(
1913
+ messages: ProviderMessage[],
1914
+ ): ProviderMessage[] {
1915
+ const byTs = new Map<string, ProviderMessage>();
1916
+ for (const message of messages) {
1917
+ if (!message.id || byTs.has(message.id)) continue;
1918
+ byTs.set(message.id, message);
1919
+ }
1920
+ return [...byTs.values()];
1921
+ }
1922
+
1923
+ function sortSlackProviderMessages(
1924
+ messages: ProviderMessage[],
1925
+ ): ProviderMessage[] {
1926
+ return [...messages].sort((left, right) => {
1927
+ const compared = compareSlackTimestamps(left.id, right.id);
1928
+ if (compared !== null) return compared;
1929
+ return left.id.localeCompare(right.id);
1930
+ });
1931
+ }
1932
+
1933
+ function buildSlackLateJoinNotice(
1934
+ result: SlackThreadBackfillResult,
1935
+ ): string | null {
1936
+ if (result.reason !== "thread_late_join" || result.persisted === 0) {
1937
+ return null;
1938
+ }
1939
+ const omitted = result.omittedMiddle
1940
+ ? " Some middle thread messages were intentionally omitted from this turn's hydrated context to keep latency bounded."
1941
+ : "";
1942
+ return `Slack context note: this turn joined an existing thread. ${result.persisted} earlier thread message${result.persisted === 1 ? " was" : "s were"} backfilled before the current message.${omitted}`;
1943
+ }
1944
+
1555
1945
  /**
1556
- * Lazily backfill missing Slack thread ancestors for an inbound thread reply.
1946
+ * Lazily backfill Slack thread gaps for an inbound thread reply.
1557
1947
  *
1558
- * When a reply arrives for a thread the daemon has never seen (e.g. the bot
1559
- * was just added to the channel, or the parent message pre-dates the
1560
- * conversation), the daemon fetches the thread's recent history via
1561
- * {@link backfillThread}, persists each unseen message as a `messages` row
1562
- * with a `slackMeta` envelope, and skips duplicates whose `ts` already
1563
- * appears in the conversation.
1948
+ * When a reply arrives for a thread with unseen Slack history, the assistant
1949
+ * fetches bounded `conversations.replies` pages via
1950
+ * {@link backfillThreadWindowPage}, persists each unseen message as a
1951
+ * `messages` row with a `slackMeta` envelope, and skips duplicates whose `ts`
1952
+ * already appears in the conversation.
1564
1953
  *
1565
1954
  * Behavior contracts:
1566
- * - **No-op when the parent is already stored.** Looks up the conversation's
1567
- * messages and short-circuits if any row has `slackMeta.channelTs ===
1568
- * threadTs`. This keeps subsequent replies in the same thread cheap.
1569
- * - **TTL idempotency cache.** A 10-minute in-memory cache prevents bursts
1570
- * of replies in the same thread from re-running the DB lookup or the
1571
- * Slack API call.
1955
+ * - **Thread-state gap detection.** Looks up stored Slack message rows for
1956
+ * the same thread, excluding reactions, then fetches only the unseen
1957
+ * `(latestStoredThreadTs, excludeChannelTs)` window when the inbound Slack
1958
+ * timestamp is newer than local state.
1959
+ * - **Upper-bound windows.** Initial late-join backfill combines an early
1960
+ * thread page with a recent page adjacent to the inbound ts; delta backfill
1961
+ * fetches the page nearest the inbound upper bound so the current turn sees
1962
+ * the most relevant context while keeping latency bounded.
1963
+ * - **Exact-window TTL cache.** A 10-minute in-memory cache prevents repeated
1964
+ * fetches for the same exact lower/upper bounded window, without
1965
+ * suppressing later unseen windows in the same thread.
1572
1966
  * - **Failure-tolerant.** Any error (Slack API failure, DB error, malformed
1573
1967
  * payload) is logged at `warn` and swallowed — the inbound turn must
1574
1968
  * never block on backfill.
@@ -1583,65 +1977,106 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
1583
1977
  * `conversations.replies` returns it in the thread window. Necessary
1584
1978
  * because thread backfill runs concurrently with
1585
1979
  * `processChannelMessageInBackground`, so the inbound row may not yet be
1586
- * in the DB when `readStoredSlackChannelTs` snapshots the conversation.
1980
+ * in the DB when the thread-state scan snapshots the conversation.
1587
1981
  */
1588
1982
  excludeChannelTs?: string;
1589
1983
  /**
1590
1984
  * OAuth account identifier used to disambiguate which Slack workspace the
1591
1985
  * backfill should read from in multi-account setups. Passed through to
1592
- * `backfillThread` `resolveConnection`. Best-effort: if omitted, the
1593
- * resolver falls back to the default-active connection.
1986
+ * `backfillThreadWindowPage` page requests and then `resolveConnection`.
1987
+ * Best-effort: if omitted, the resolver falls back to the default-active
1988
+ * connection.
1594
1989
  */
1595
1990
  account?: string;
1596
- }): Promise<void> {
1991
+ }): Promise<SlackThreadBackfillResult> {
1597
1992
  const { conversationId, channelId, threadTs, excludeChannelTs, account } =
1598
1993
  params;
1599
- const cacheKey = `${conversationId}:${threadTs}`;
1600
1994
 
1601
1995
  try {
1602
- if (isBackfillRecentlyTriggered(cacheKey)) {
1603
- return;
1996
+ const upperBoundTs = parseSlackTimestamp(excludeChannelTs)
1997
+ ? excludeChannelTs
1998
+ : undefined;
1999
+ const threadState = readStoredSlackThreadState(conversationId, threadTs);
2000
+ const lowerBoundTs = threadState.latestStoredThreadTs;
2001
+
2002
+ // Pre-seed only after computing lowerBoundTs. The current inbound row
2003
+ // may not have reached the DB yet, and treating it as stored state would
2004
+ // hide the gap we need to fetch.
2005
+ if (excludeChannelTs) threadState.storedChannelTs.add(excludeChannelTs);
2006
+
2007
+ if (upperBoundTs && lowerBoundTs) {
2008
+ const lowerVsUpper = compareSlackTimestamps(lowerBoundTs, upperBoundTs);
2009
+ if (lowerVsUpper !== null && lowerVsUpper >= 0) {
2010
+ return emptySlackThreadBackfillResult();
2011
+ }
2012
+ } else if (!upperBoundTs && lowerBoundTs) {
2013
+ return emptySlackThreadBackfillResult();
1604
2014
  }
1605
2015
 
1606
- const storedChannelTs = readStoredSlackChannelTs(conversationId);
1607
- if (excludeChannelTs) storedChannelTs.add(excludeChannelTs);
1608
- if (storedChannelTs.has(threadTs)) {
1609
- // Parent is already in the conversation; mark the cache so a burst of
1610
- // replies in this thread does not redo the DB scan for each one.
1611
- _backfillTriggerCache.set(cacheKey, Date.now());
1612
- pruneBackfillCacheIfNeeded();
1613
- return;
2016
+ const cacheKey = `${conversationId}:${threadTs}:${
2017
+ lowerBoundTs ?? "none"
2018
+ }:${upperBoundTs ?? "unbounded"}`;
2019
+ if (isBackfillRecentlyTriggered(cacheKey)) {
2020
+ return emptySlackThreadBackfillResult();
1614
2021
  }
1615
2022
 
1616
2023
  // Mark the trigger before issuing the network call. Doing this first
1617
- // means a second concurrent reply in the same thread short-circuits
1618
- // immediately even while the first call is still awaiting the Slack
1619
- // API. The cost is a slightly larger window where a transient Slack
1620
- // failure suppresses a retry, which the next reply outside the TTL
1621
- // (or a daemon restart) will re-attempt anyway.
2024
+ // means a second concurrent request for the same window short-circuits
2025
+ // immediately even while the first call is still awaiting the Slack API.
2026
+ // The cost is a slightly larger window where a transient Slack failure
2027
+ // suppresses a retry, which the next reply outside the TTL (or a daemon
2028
+ // restart) will re-attempt anyway.
1622
2029
  _backfillTriggerCache.set(cacheKey, Date.now());
1623
2030
  pruneBackfillCacheIfNeeded();
1624
2031
 
1625
- const fetched = await backfillThread(channelId, threadTs, { account });
2032
+ const isInitialLateJoin =
2033
+ lowerBoundTs === undefined &&
2034
+ threadState.storedChannelTs.size === (excludeChannelTs ? 1 : 0);
2035
+ const reason: SlackBackfillReason = isInitialLateJoin
2036
+ ? "thread_late_join"
2037
+ : "thread_delta";
2038
+ let omittedMiddle = false;
2039
+ let fetched: ProviderMessage[];
2040
+ if (isInitialLateJoin) {
2041
+ const initial = await fetchInitialSlackThreadWindows({
2042
+ channelId,
2043
+ threadTs,
2044
+ upperBoundTs,
2045
+ account,
2046
+ });
2047
+ fetched = initial.messages;
2048
+ omittedMiddle = initial.omittedMiddle;
2049
+ } else {
2050
+ const window = await fetchSlackThreadUpperAdjacentWindow({
2051
+ channelId,
2052
+ threadTs,
2053
+ limit: SLACK_THREAD_DELTA_LIMIT,
2054
+ account,
2055
+ ...(lowerBoundTs !== undefined ? { lowerBoundTs } : {}),
2056
+ upperBoundTs: upperBoundTs ?? threadTs,
2057
+ });
2058
+ fetched = window.messages;
2059
+ omittedMiddle = window.omittedEarlierContent;
2060
+ }
1626
2061
  if (fetched.length === 0) {
1627
2062
  log.debug(
1628
2063
  { conversationId, channelId, threadTs },
1629
2064
  "Slack thread backfill returned no messages",
1630
2065
  );
1631
- return;
2066
+ return emptySlackThreadBackfillResult();
1632
2067
  }
1633
2068
 
1634
2069
  let persisted = 0;
1635
2070
  for (const message of fetched) {
1636
2071
  if (!message.id) continue;
1637
- if (storedChannelTs.has(message.id)) continue;
2072
+ if (threadState.storedChannelTs.has(message.id)) continue;
1638
2073
  try {
1639
2074
  await persistBackfilledSlackMessage({
1640
2075
  conversationId,
1641
2076
  channelId,
1642
2077
  message,
1643
2078
  });
1644
- storedChannelTs.add(message.id);
2079
+ threadState.storedChannelTs.add(message.id);
1645
2080
  persisted++;
1646
2081
  } catch (err) {
1647
2082
  log.warn(
@@ -1658,15 +2093,22 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
1658
2093
  threadTs,
1659
2094
  persisted,
1660
2095
  fetched: fetched.length,
2096
+ omittedMiddle,
1661
2097
  },
1662
- "Slack thread backfill persisted ancestor messages",
2098
+ "Slack thread backfill persisted thread messages",
1663
2099
  );
2100
+ return {
2101
+ fetched: fetched.length,
2102
+ persisted,
2103
+ reason,
2104
+ omittedMiddle,
2105
+ };
1664
2106
  } catch (err) {
1665
2107
  // `channel_not_found` almost always means the resolved connection is
1666
2108
  // pointing at the wrong Slack workspace (a real config bug), so log it
1667
2109
  // at ERROR to match backfill's rethrow contract. Other failures
1668
2110
  // (timeout, auth, ratelimited, …) stay at WARN — they're expected
1669
- // transient blips and dispatch proceeds without the ancestors.
2111
+ // transient blips and dispatch proceeds without the backfilled thread rows.
1670
2112
  const channelNotFound =
1671
2113
  err instanceof Error && /channel_not_found/i.test(err.message);
1672
2114
  const payload = { err, conversationId, channelId, threadTs, account };
@@ -1678,5 +2120,6 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
1678
2120
  } else {
1679
2121
  log.warn(payload, "Slack thread backfill failed; proceeding without it");
1680
2122
  }
2123
+ return emptySlackThreadBackfillResult();
1681
2124
  }
1682
2125
  }