@vellumai/assistant 0.6.4 → 0.6.6

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 (1008) hide show
  1. package/.prettierignore +5 -0
  2. package/AGENTS.md +9 -1
  3. package/ARCHITECTURE.md +43 -49
  4. package/Dockerfile +17 -3
  5. package/README.md +3 -4
  6. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  7. package/bun.lock +8 -3
  8. package/docs/architecture/integrations.md +33 -59
  9. package/docs/architecture/memory.md +25 -30
  10. package/docs/architecture/security.md +19 -18
  11. package/docs/browser-use-architecture-phase2.md +63 -20
  12. package/docs/error-handling.md +111 -0
  13. package/docs/plugins.md +761 -0
  14. package/docs/skills.md +10 -10
  15. package/docs/stt-provider-onboarding.md +2 -1
  16. package/examples/plugins/echo/README.md +132 -0
  17. package/examples/plugins/echo/package.json +17 -0
  18. package/examples/plugins/echo/register.ts +187 -0
  19. package/knip.json +9 -2
  20. package/node_modules/@vellumai/ces-contracts/package.json +2 -1
  21. package/node_modules/@vellumai/ces-contracts/src/__tests__/trust-rules.test.ts +471 -0
  22. package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +398 -4
  23. package/node_modules/@vellumai/credential-storage/bun.lock +2 -2
  24. package/node_modules/@vellumai/credential-storage/package.json +2 -2
  25. package/node_modules/@vellumai/credential-storage/src/oauth-runtime.ts +20 -2
  26. package/node_modules/@vellumai/egress-proxy/bun.lock +2 -2
  27. package/node_modules/@vellumai/egress-proxy/package.json +2 -2
  28. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  29. package/openapi.yaml +334 -78
  30. package/package.json +6 -3
  31. package/scripts/generate-openapi.ts +50 -11
  32. package/src/__tests__/agent-loop-callsite-precedence.test.ts +318 -0
  33. package/src/__tests__/agent-loop-sentry-hygiene.test.ts +137 -0
  34. package/src/__tests__/agent-loop.test.ts +112 -1
  35. package/src/__tests__/anthropic-error-formatting.test.ts +98 -0
  36. package/src/__tests__/anthropic-provider.test.ts +171 -2
  37. package/src/__tests__/app-compiler.test.ts +57 -0
  38. package/src/__tests__/approval-cascade.test.ts +36 -10
  39. package/src/__tests__/approval-routes-http.test.ts +134 -10
  40. package/src/__tests__/assistant-attachments.test.ts +44 -0
  41. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -0
  42. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  43. package/src/__tests__/avatar-generator.test.ts +4 -2
  44. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  45. package/src/__tests__/browser-identifier-parity-guard.test.ts +53 -0
  46. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +23 -33
  47. package/src/__tests__/browser-skill-endstate.test.ts +51 -182
  48. package/src/__tests__/btw-routes.test.ts +47 -1
  49. package/src/__tests__/bundled-asset.test.ts +6 -6
  50. package/src/__tests__/call-controller.test.ts +1 -2
  51. package/src/__tests__/call-site-routing-provider.test.ts +214 -0
  52. package/src/__tests__/catalog-cache.test.ts +96 -4
  53. package/src/__tests__/channel-approval-routes.test.ts +4 -4
  54. package/src/__tests__/channel-reply-delivery.test.ts +300 -2
  55. package/src/__tests__/checker.test.ts +870 -655
  56. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  57. package/src/__tests__/cli-command-risk-guard.test.ts +30 -33
  58. package/src/__tests__/compaction-events.test.ts +501 -0
  59. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  60. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  61. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  62. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  63. package/src/__tests__/config-analysis.test.ts +11 -28
  64. package/src/__tests__/config-loader-backfill.test.ts +174 -0
  65. package/src/__tests__/config-loader-corrupt.test.ts +183 -0
  66. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +202 -0
  67. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  68. package/src/__tests__/config-schema-cmd.test.ts +11 -5
  69. package/src/__tests__/config-schema.test.ts +440 -114
  70. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  71. package/src/__tests__/config-watcher.test.ts +2 -2
  72. package/src/__tests__/contact-store-user-file.test.ts +72 -73
  73. package/src/__tests__/contacts-tools.test.ts +26 -0
  74. package/src/__tests__/contacts-write.test.ts +4 -4
  75. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  76. package/src/__tests__/context-token-estimator.test.ts +191 -1
  77. package/src/__tests__/context-window-manager.test.ts +883 -4
  78. package/src/__tests__/conversation-abort-tool-results.test.ts +32 -15
  79. package/src/__tests__/conversation-agent-loop-overflow.test.ts +86 -46
  80. package/src/__tests__/conversation-agent-loop.test.ts +435 -216
  81. package/src/__tests__/conversation-attachments.test.ts +1 -1
  82. package/src/__tests__/conversation-confirmation-signals.test.ts +36 -10
  83. package/src/__tests__/conversation-error.test.ts +37 -6
  84. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  85. package/src/__tests__/conversation-init.benchmark.test.ts +34 -12
  86. package/src/__tests__/conversation-lifecycle.test.ts +336 -0
  87. package/src/__tests__/conversation-load-history-repair.test.ts +27 -10
  88. package/src/__tests__/conversation-pairing.test.ts +174 -10
  89. package/src/__tests__/conversation-pre-run-repair.test.ts +32 -15
  90. package/src/__tests__/conversation-process-callsite.test.ts +309 -0
  91. package/src/__tests__/conversation-provider-retry-repair.test.ts +44 -21
  92. package/src/__tests__/conversation-queue.test.ts +68 -38
  93. package/src/__tests__/conversation-routes-disk-view.test.ts +36 -7
  94. package/src/__tests__/conversation-routes-slash-commands.test.ts +31 -3
  95. package/src/__tests__/conversation-runtime-assembly.test.ts +2877 -152
  96. package/src/__tests__/conversation-runtime-workspace.test.ts +35 -50
  97. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  98. package/src/__tests__/conversation-skill-tools.test.ts +12 -146
  99. package/src/__tests__/conversation-slash-queue.test.ts +39 -19
  100. package/src/__tests__/conversation-slash-unknown.test.ts +53 -16
  101. package/src/__tests__/conversation-speed-override.test.ts +36 -12
  102. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1035 -0
  103. package/src/__tests__/conversation-surfaces-standalone.test.ts +630 -0
  104. package/src/__tests__/conversation-title-service.test.ts +118 -2
  105. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  106. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +1 -1
  107. package/src/__tests__/conversation-unread-route.test.ts +2 -2
  108. package/src/__tests__/conversation-usage.test.ts +4 -2
  109. package/src/__tests__/conversation-workspace-cache-state.test.ts +33 -9
  110. package/src/__tests__/conversation-workspace-injection.test.ts +46 -15
  111. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +46 -15
  112. package/src/__tests__/credential-broker-browser-fill.test.ts +110 -0
  113. package/src/__tests__/credential-health-service.test.ts +78 -9
  114. package/src/__tests__/credential-security-invariants.test.ts +5 -2
  115. package/src/__tests__/credential-storage-oauth-compat.test.ts +18 -0
  116. package/src/__tests__/credential-storage-static-compat.test.ts +28 -0
  117. package/src/__tests__/credential-vault-unit.test.ts +135 -19
  118. package/src/__tests__/credentials-cli.test.ts +1 -9
  119. package/src/__tests__/cross-provider-web-search.test.ts +84 -0
  120. package/src/__tests__/daemon-server-persist-and-process-callsite.test.ts +92 -0
  121. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  122. package/src/__tests__/delete-propagation.test.ts +437 -0
  123. package/src/__tests__/dm-backfill.test.ts +417 -0
  124. package/src/__tests__/dm-persistence.test.ts +227 -0
  125. package/src/__tests__/edit-propagation.test.ts +280 -0
  126. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  127. package/src/__tests__/ephemeral-permissions.test.ts +93 -3
  128. package/src/__tests__/estimator-calibration-integration.test.ts +208 -0
  129. package/src/__tests__/estimator-calibration.test.ts +213 -0
  130. package/src/__tests__/extension-id-sync-guard.test.ts +29 -10
  131. package/src/__tests__/file-write-tool.test.ts +151 -1
  132. package/src/__tests__/filing-service.test.ts +255 -0
  133. package/src/__tests__/first-greeting.test.ts +247 -5
  134. package/src/__tests__/gemini-provider.test.ts +0 -3
  135. package/src/__tests__/guardian-grant-minting.test.ts +8 -0
  136. package/src/__tests__/headless-browser-interactions.test.ts +1 -1
  137. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  138. package/src/__tests__/heartbeat-service.test.ts +96 -15
  139. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  140. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  141. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  142. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  143. package/src/__tests__/host-shell-tool.test.ts +124 -18
  144. package/src/__tests__/http-user-message-parity.test.ts +29 -1
  145. package/src/__tests__/image-credentials.test.ts +137 -0
  146. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  147. package/src/__tests__/inbound-slack-persistence.test.ts +340 -0
  148. package/src/__tests__/injector-chain.test.ts +526 -0
  149. package/src/__tests__/intent-routing.test.ts +1 -66
  150. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  151. package/src/__tests__/llm-catalog-parity.test.ts +174 -0
  152. package/src/__tests__/llm-context-normalization.test.ts +121 -0
  153. package/src/__tests__/llm-resolver.test.ts +214 -0
  154. package/src/__tests__/llm-schema.test.ts +223 -0
  155. package/src/__tests__/managed-proxy-context.test.ts +6 -2
  156. package/src/__tests__/media-generate-image.test.ts +119 -13
  157. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  158. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  159. package/src/__tests__/messaging-skill-split.test.ts +3 -34
  160. package/src/__tests__/migration-import-from-url.test.ts +621 -0
  161. package/src/__tests__/model-intents.test.ts +11 -83
  162. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  163. package/src/__tests__/notification-decision-fallback.test.ts +0 -10
  164. package/src/__tests__/notification-decision-identity.test.ts +0 -9
  165. package/src/__tests__/notification-decision-recipient-context.test.ts +0 -9
  166. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  167. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  168. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  169. package/src/__tests__/oauth-cli.test.ts +14 -12
  170. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  171. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  172. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  173. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  174. package/src/__tests__/oauth-store.test.ts +46 -78
  175. package/src/__tests__/oauth2-gateway-transport.test.ts +8 -3
  176. package/src/__tests__/oauth2-refresh-retry.test.ts +279 -0
  177. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  178. package/src/__tests__/openai-image-service.test.ts +368 -0
  179. package/src/__tests__/openai-provider.test.ts +7 -0
  180. package/src/__tests__/openai-responses-provider.test.ts +396 -0
  181. package/src/__tests__/openrouter-provider-only.test.ts +135 -0
  182. package/src/__tests__/outbound-slack-persistence.test.ts +293 -0
  183. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  184. package/src/__tests__/permission-checker-host-gate.test.ts +1 -25
  185. package/src/__tests__/permission-mode.test.ts +16 -0
  186. package/src/__tests__/permission-types.test.ts +0 -1
  187. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  188. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  189. package/src/__tests__/persona-resolver.test.ts +13 -13
  190. package/src/__tests__/pipeline-runner.test.ts +565 -0
  191. package/src/__tests__/pkb-autoinject.test.ts +37 -1
  192. package/src/__tests__/platform-bash-auto-approve.test.ts +1 -1
  193. package/src/__tests__/platform.test.ts +5 -2
  194. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  195. package/src/__tests__/plugin-registry.test.ts +273 -0
  196. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  197. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  198. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  199. package/src/__tests__/plugin-types.test.ts +320 -0
  200. package/src/__tests__/pricing.test.ts +93 -14
  201. package/src/__tests__/profiler-routes.test.ts +1 -1
  202. package/src/__tests__/provider-commit-message-generator.test.ts +14 -84
  203. package/src/__tests__/provider-env-vars-scope.test.ts +52 -0
  204. package/src/__tests__/provider-error-scenarios.test.ts +135 -6
  205. package/src/__tests__/provider-managed-proxy-integration.test.ts +42 -11
  206. package/src/__tests__/provider-registry-ollama.test.ts +1 -2
  207. package/src/__tests__/proxy-approval-callback.test.ts +69 -9
  208. package/src/__tests__/reaction-persistence.test.ts +561 -0
  209. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  210. package/src/__tests__/registry.test.ts +0 -2
  211. package/src/__tests__/relay-server.test.ts +1 -1
  212. package/src/__tests__/require-fresh-approval.test.ts +1 -1
  213. package/src/__tests__/retry-openrouter-only-normalization.test.ts +136 -0
  214. package/src/__tests__/retry-thinking-tool-choice.test.ts +226 -0
  215. package/src/__tests__/risk-classifier-parity.test.ts +230 -0
  216. package/src/__tests__/sanitize-config-for-transfer.test.ts +78 -1
  217. package/src/__tests__/schedule-routes.test.ts +131 -1
  218. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  219. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  220. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  221. package/src/__tests__/secret-ingress-http.test.ts +28 -0
  222. package/src/__tests__/secret-prompter-channel-fallback.test.ts +125 -0
  223. package/src/__tests__/secret-routes-managed-proxy.test.ts +2 -3
  224. package/src/__tests__/secret-scanner-executor.test.ts +1 -1
  225. package/src/__tests__/send-endpoint-busy.test.ts +29 -1
  226. package/src/__tests__/server-history-render.test.ts +31 -0
  227. package/src/__tests__/shell-identity.test.ts +0 -134
  228. package/src/__tests__/shell-parser-property.test.ts +13 -13
  229. package/src/__tests__/skill-cache-store.test.ts +182 -0
  230. package/src/__tests__/skills.test.ts +19 -33
  231. package/src/__tests__/slack-app-setup-skill-regression.test.ts +3 -1
  232. package/src/__tests__/slack-skill.test.ts +3 -8
  233. package/src/__tests__/starter-bundle.test.ts +35 -0
  234. package/src/__tests__/subagent-call-site-routing.test.ts +280 -0
  235. package/src/__tests__/suggestion-routes.test.ts +259 -3
  236. package/src/__tests__/system-prompt.test.ts +22 -35
  237. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  238. package/src/__tests__/task-runner.test.ts +3 -1
  239. package/src/__tests__/task-scheduler.test.ts +3 -15
  240. package/src/__tests__/tcc-sandbox-deny.test.ts +198 -0
  241. package/src/__tests__/terminal-tools.test.ts +8 -0
  242. package/src/__tests__/test-preload.ts +11 -0
  243. package/src/__tests__/test-support/browser-skill-harness.ts +2 -52
  244. package/src/__tests__/thread-backfill.test.ts +941 -0
  245. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  246. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  247. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  248. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  249. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -8
  250. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -2
  251. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  252. package/src/__tests__/tool-executor.test.ts +201 -94
  253. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  254. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  255. package/src/__tests__/trust-store.test.ts +442 -109
  256. package/src/__tests__/update-bulletin-job.test.ts +389 -0
  257. package/src/__tests__/usage-cache-backfill-migration.test.ts +3 -1
  258. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  259. package/src/__tests__/verification-control-plane-policy.test.ts +1 -22
  260. package/src/__tests__/voice-session-bridge.test.ts +39 -0
  261. package/src/__tests__/volume-security-guard.test.ts +3 -2
  262. package/src/__tests__/web-search-history.test.ts +337 -0
  263. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +343 -0
  264. package/src/__tests__/workspace-migration-043-release-notes-latex-rendering.test.ts +202 -0
  265. package/src/__tests__/workspace-migration-045-release-notes-meet-avatar.test.ts +210 -0
  266. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  267. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  268. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  269. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  270. package/src/__tests__/workspace-migration-drop-user-md.test.ts +11 -11
  271. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  272. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +841 -0
  273. package/src/__tests__/workspace-policy.test.ts +22 -16
  274. package/src/acp/client-handler.ts +1 -2
  275. package/src/agent/loop.ts +545 -115
  276. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  277. package/src/approvals/guardian-request-resolvers.ts +80 -0
  278. package/src/avatar/resvg-lazy.test.ts +136 -0
  279. package/src/avatar/resvg-lazy.ts +82 -9
  280. package/src/avatar/traits-png-sync.ts +21 -1
  281. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  282. package/src/backup/backup-worker.ts +3 -15
  283. package/src/browser/__tests__/operations.test.ts +163 -0
  284. package/src/browser/identifiers.ts +51 -0
  285. package/src/browser/operations.ts +660 -0
  286. package/src/browser/types.ts +81 -0
  287. package/src/bundler/app-compiler.ts +84 -1
  288. package/src/calls/call-state.ts +2 -2
  289. package/src/calls/guardian-question-copy.ts +2 -2
  290. package/src/calls/telephony-stt-routing.ts +1 -1
  291. package/src/calls/voice-session-bridge.ts +1 -0
  292. package/src/channels/__tests__/types.test.ts +3 -3
  293. package/src/channels/types.ts +6 -4
  294. package/src/cli/AGENTS.md +1 -1
  295. package/src/cli/__tests__/notifications.test.ts +87 -211
  296. package/src/cli/commands/__tests__/attachment.test.ts +438 -0
  297. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  298. package/src/cli/commands/__tests__/browser.test.ts +554 -0
  299. package/src/cli/commands/__tests__/cache.test.ts +623 -0
  300. package/src/cli/commands/__tests__/email-list.test.ts +6 -0
  301. package/src/cli/commands/__tests__/email-send.test.ts +93 -1
  302. package/src/cli/commands/__tests__/image-generation.test.ts +886 -0
  303. package/src/cli/commands/__tests__/inference-send.test.ts +463 -0
  304. package/src/cli/commands/__tests__/stt-transcribe.test.ts +454 -0
  305. package/src/cli/commands/__tests__/task.test.ts +913 -0
  306. package/src/cli/commands/__tests__/tts-synthesize.test.ts +606 -0
  307. package/src/cli/commands/__tests__/ui-confirm.test.ts +650 -0
  308. package/src/cli/commands/__tests__/ui.test.ts +1215 -0
  309. package/src/cli/commands/__tests__/watchers.test.ts +716 -0
  310. package/src/cli/commands/attachment.ts +182 -0
  311. package/src/cli/commands/backup.ts +2 -2
  312. package/src/cli/commands/browser.ts +350 -0
  313. package/src/cli/commands/cache.ts +341 -0
  314. package/src/cli/commands/clients.ts +138 -0
  315. package/src/cli/commands/completions.ts +2 -12
  316. package/src/cli/commands/config.ts +6 -6
  317. package/src/cli/commands/conversations-import.ts +347 -0
  318. package/src/cli/commands/conversations.ts +69 -8
  319. package/src/cli/commands/email.ts +234 -194
  320. package/src/cli/commands/image-generation.ts +299 -0
  321. package/src/cli/commands/inference.ts +200 -0
  322. package/src/cli/commands/memory.ts +127 -17
  323. package/src/cli/commands/notifications.ts +68 -103
  324. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  325. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  326. package/src/cli/commands/oauth/connect.ts +2 -2
  327. package/src/cli/commands/oauth/providers.ts +176 -8
  328. package/src/cli/commands/oauth/status.ts +46 -36
  329. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  330. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -1
  331. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -1
  332. package/src/cli/commands/platform/__tests__/status.test.ts +0 -1
  333. package/src/cli/commands/skills.ts +3 -4
  334. package/src/cli/commands/stt.ts +339 -0
  335. package/src/cli/commands/task.ts +795 -0
  336. package/src/cli/commands/trust.ts +50 -19
  337. package/src/cli/commands/tts.ts +273 -0
  338. package/src/cli/commands/ui.ts +670 -0
  339. package/src/cli/commands/watchers.ts +509 -0
  340. package/src/cli/lib/daemon-credential-client.ts +0 -19
  341. package/src/cli/program.ts +39 -24
  342. package/src/cli.ts +0 -37
  343. package/src/config/__tests__/backup-schema.test.ts +7 -2
  344. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  345. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  346. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  347. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  348. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  349. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  350. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  351. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  352. package/src/config/bundled-skills/media-processing/services/reduce.ts +1 -1
  353. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  354. package/src/config/bundled-skills/messaging/TOOLS.json +4 -0
  355. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  356. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +20 -1
  357. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +15 -1
  358. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +21 -1
  359. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +69 -12
  360. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -8
  361. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  362. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  363. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  364. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  365. package/src/config/bundled-tool-registry.ts +0 -190
  366. package/src/config/env.ts +7 -2
  367. package/src/config/feature-flag-registry.json +42 -10
  368. package/src/config/llm-resolver.ts +128 -0
  369. package/src/config/loader.ts +194 -10
  370. package/src/config/raw-config-utils.ts +30 -2
  371. package/src/config/sanitize-for-transfer.ts +35 -0
  372. package/src/config/schema.ts +49 -41
  373. package/src/config/schemas/analysis.ts +3 -22
  374. package/src/config/schemas/backup.ts +1 -1
  375. package/src/config/schemas/calls.ts +0 -4
  376. package/src/config/schemas/conversations.ts +16 -0
  377. package/src/config/schemas/filing.ts +2 -7
  378. package/src/config/schemas/heartbeat.ts +0 -5
  379. package/src/config/schemas/inference.ts +3 -23
  380. package/src/config/schemas/llm.ts +317 -0
  381. package/src/config/schemas/memory-processing.ts +1 -9
  382. package/src/config/schemas/notifications.ts +4 -11
  383. package/src/config/schemas/platform.ts +3 -9
  384. package/src/config/schemas/security.ts +33 -0
  385. package/src/config/schemas/services.ts +9 -4
  386. package/src/config/schemas/stt.ts +1 -0
  387. package/src/config/schemas/tts.ts +64 -0
  388. package/src/config/schemas/updates.ts +1 -1
  389. package/src/config/schemas/workspace-git.ts +3 -40
  390. package/src/config/skill-state.ts +6 -2
  391. package/src/config/skills.ts +96 -7
  392. package/src/context/__tests__/compact-prompt.test.ts +63 -0
  393. package/src/context/__tests__/microcompact.test.ts +805 -0
  394. package/src/context/estimator-calibration.ts +136 -0
  395. package/src/context/microcompact.ts +443 -0
  396. package/src/context/prompts/compact.md +26 -0
  397. package/src/context/token-estimator.ts +61 -3
  398. package/src/context/tool-result-truncation.ts +3 -63
  399. package/src/context/window-manager.ts +417 -39
  400. package/src/credential-execution/approval-bridge.ts +0 -1
  401. package/src/credential-execution/executable-discovery.ts +19 -8
  402. package/src/credential-execution/process-manager.test.ts +109 -0
  403. package/src/credential-execution/process-manager.ts +65 -2
  404. package/src/credential-health/credential-health-service.ts +19 -6
  405. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  406. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  407. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  408. package/src/daemon/approval-generators.ts +29 -4
  409. package/src/daemon/assistant-attachments.ts +24 -13
  410. package/src/daemon/classifier.ts +2 -2
  411. package/src/daemon/config-watcher.ts +0 -3
  412. package/src/daemon/context-overflow-policy.ts +4 -13
  413. package/src/daemon/context-overflow-reducer.ts +4 -1
  414. package/src/daemon/conversation-agent-loop-handlers.ts +162 -34
  415. package/src/daemon/conversation-agent-loop.ts +1282 -599
  416. package/src/daemon/conversation-attachments.ts +2 -6
  417. package/src/daemon/conversation-error.ts +36 -1
  418. package/src/daemon/conversation-history.ts +10 -19
  419. package/src/daemon/conversation-lifecycle.ts +59 -17
  420. package/src/daemon/conversation-messaging.ts +73 -4
  421. package/src/daemon/conversation-notifiers.ts +2 -110
  422. package/src/daemon/conversation-process.ts +24 -11
  423. package/src/daemon/conversation-queue-manager.ts +3 -0
  424. package/src/daemon/conversation-runtime-assembly.ts +1063 -211
  425. package/src/daemon/conversation-slash.ts +2 -2
  426. package/src/daemon/conversation-surfaces.ts +389 -1
  427. package/src/daemon/conversation-tool-setup.ts +51 -9
  428. package/src/daemon/conversation-usage.ts +1 -1
  429. package/src/daemon/conversation.ts +197 -64
  430. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  431. package/src/daemon/external-skills-bootstrap.ts +41 -0
  432. package/src/daemon/first-greeting.ts +191 -14
  433. package/src/daemon/guardian-action-generators.ts +34 -14
  434. package/src/daemon/handlers/config-model.test.ts +86 -0
  435. package/src/daemon/handlers/config-model.ts +65 -12
  436. package/src/daemon/handlers/conversations.ts +9 -2
  437. package/src/daemon/handlers/shared.ts +39 -11
  438. package/src/daemon/handlers/skills.ts +7 -3
  439. package/src/daemon/handlers/slack-channel-oauth-install.ts +197 -0
  440. package/src/daemon/lifecycle.ts +109 -82
  441. package/src/daemon/message-types/computer-use.ts +2 -34
  442. package/src/daemon/message-types/conversations.ts +63 -0
  443. package/src/daemon/message-types/messages.ts +21 -1
  444. package/src/daemon/message-types/trust.ts +0 -2
  445. package/src/daemon/parse-actual-tokens-from-error.test.ts +57 -1
  446. package/src/daemon/parse-actual-tokens-from-error.ts +66 -0
  447. package/src/daemon/pkb-context-tracker.test.ts +169 -0
  448. package/src/daemon/pkb-context-tracker.ts +125 -0
  449. package/src/daemon/pkb-reminder-builder.test.ts +70 -0
  450. package/src/daemon/pkb-reminder-builder.ts +31 -0
  451. package/src/daemon/providers-setup.ts +6 -0
  452. package/src/daemon/server.ts +122 -12
  453. package/src/daemon/shutdown-handlers.ts +2 -12
  454. package/src/daemon/tool-side-effects.ts +14 -65
  455. package/src/daemon/web-search-history.ts +126 -0
  456. package/src/events/domain-events.ts +0 -1
  457. package/src/filing/filing-service.ts +9 -10
  458. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  459. package/src/heartbeat/heartbeat-service.ts +99 -28
  460. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  461. package/src/home/__tests__/feed-scheduler.test.ts +39 -11
  462. package/src/home/__tests__/rollup-producer.test.ts +44 -0
  463. package/src/home/assistant-feed-authoring.ts +4 -0
  464. package/src/home/emit-feed-event.ts +11 -0
  465. package/src/home/feed-scheduler.ts +20 -4
  466. package/src/home/feed-types.ts +97 -4
  467. package/src/home/relationship-state-writer.ts +2 -2
  468. package/src/home/rewrite-command-preview.ts +66 -0
  469. package/src/home/rollup-producer.ts +34 -5
  470. package/src/home/suggested-prompts.ts +101 -0
  471. package/src/ipc/__tests__/attachment-ipc.test.ts +213 -0
  472. package/src/ipc/__tests__/browser-ipc.test.ts +339 -0
  473. package/src/ipc/__tests__/cache-ipc.test.ts +266 -0
  474. package/src/ipc/__tests__/socket-path.test.ts +34 -0
  475. package/src/ipc/__tests__/task-ipc.test.ts +577 -0
  476. package/src/ipc/__tests__/ui-request-route.test.ts +495 -0
  477. package/src/ipc/__tests__/watcher-ipc.test.ts +295 -0
  478. package/src/ipc/cli-client.ts +2 -1
  479. package/src/ipc/cli-server.ts +26 -8
  480. package/src/ipc/gateway-client.ts +6 -3
  481. package/src/ipc/routes/attachment.ts +114 -0
  482. package/src/ipc/routes/browser-context.ts +63 -0
  483. package/src/ipc/routes/browser.ts +97 -0
  484. package/src/ipc/routes/cache.ts +96 -0
  485. package/src/ipc/routes/get-contact.ts +16 -0
  486. package/src/ipc/routes/index.ts +31 -1
  487. package/src/ipc/routes/list-clients.ts +31 -0
  488. package/src/ipc/routes/merge-contacts.ts +17 -0
  489. package/src/ipc/routes/notification.ts +133 -0
  490. package/src/ipc/routes/rename-conversation.ts +59 -0
  491. package/src/ipc/routes/search-contacts.ts +19 -0
  492. package/src/ipc/routes/task-queue.ts +226 -0
  493. package/src/ipc/routes/task.ts +173 -0
  494. package/src/ipc/routes/ui-request.ts +50 -0
  495. package/src/ipc/routes/upsert-contact.ts +25 -0
  496. package/src/ipc/routes/watcher.ts +203 -0
  497. package/src/ipc/socket-path.ts +76 -0
  498. package/src/media/app-icon-generator.ts +23 -46
  499. package/src/media/avatar-router.ts +26 -41
  500. package/src/media/gemini-image-service.ts +8 -41
  501. package/src/media/image-credentials.ts +73 -0
  502. package/src/media/image-service.ts +85 -0
  503. package/src/media/openai-image-service.ts +131 -0
  504. package/src/media/types.ts +46 -0
  505. package/src/memory/__tests__/conversation-analyze-job.test.ts +9 -8
  506. package/src/memory/__tests__/conversation-group-migration.test.ts +99 -0
  507. package/src/memory/admin.ts +18 -0
  508. package/src/memory/conversation-analyze-job.ts +14 -13
  509. package/src/memory/conversation-attention-store.ts +13 -6
  510. package/src/memory/conversation-crud.ts +133 -3
  511. package/src/memory/conversation-group-migration.ts +38 -6
  512. package/src/memory/conversation-queries.ts +57 -4
  513. package/src/memory/conversation-title-service.ts +32 -4
  514. package/src/memory/db-init.ts +10 -0
  515. package/src/memory/embedding-backend.ts +1 -1
  516. package/src/memory/embedding-gemini.test.ts +41 -2
  517. package/src/memory/embedding-gemini.ts +6 -1
  518. package/src/memory/graph/bootstrap.test.ts +282 -0
  519. package/src/memory/graph/bootstrap.ts +8 -5
  520. package/src/memory/graph/compaction.ts +299 -0
  521. package/src/memory/graph/consolidation.ts +4 -4
  522. package/src/memory/graph/conversation-graph-memory.ts +89 -29
  523. package/src/memory/graph/extraction.test.ts +272 -2
  524. package/src/memory/graph/extraction.ts +183 -53
  525. package/src/memory/graph/graph-search.test.ts +93 -0
  526. package/src/memory/graph/graph-search.ts +4 -1
  527. package/src/memory/graph/inspect.ts +2 -2
  528. package/src/memory/graph/narrative.ts +2 -2
  529. package/src/memory/graph/pattern-scan.ts +2 -2
  530. package/src/memory/graph/retriever.test.ts +459 -0
  531. package/src/memory/graph/retriever.ts +237 -48
  532. package/src/memory/graph/store.ts +41 -0
  533. package/src/memory/graph/tool-handlers.ts +27 -0
  534. package/src/memory/graph/tools.ts +6 -1
  535. package/src/memory/indexer.ts +5 -5
  536. package/src/memory/job-handlers/conversation-starters.ts +23 -20
  537. package/src/memory/job-handlers/summarization.ts +2 -2
  538. package/src/memory/job-utils.ts +7 -1
  539. package/src/memory/jobs/embed-pkb-file.test.ts +168 -0
  540. package/src/memory/jobs/embed-pkb-file.ts +54 -0
  541. package/src/memory/jobs-store.ts +44 -3
  542. package/src/memory/jobs-worker.ts +4 -0
  543. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  544. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +1 -1
  545. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  546. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +2 -2
  547. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +82 -0
  548. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  549. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  550. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  551. package/src/memory/migrations/index.ts +5 -0
  552. package/src/memory/pkb/pkb-index.test.ts +369 -0
  553. package/src/memory/pkb/pkb-index.ts +255 -0
  554. package/src/memory/pkb/pkb-reconcile.test.ts +252 -0
  555. package/src/memory/pkb/pkb-reconcile.ts +148 -0
  556. package/src/memory/pkb/pkb-search.test.ts +499 -0
  557. package/src/memory/pkb/pkb-search.ts +159 -0
  558. package/src/memory/pkb/types.ts +53 -0
  559. package/src/memory/qdrant-client.test.ts +60 -0
  560. package/src/memory/qdrant-client.ts +147 -1
  561. package/src/memory/schema/infrastructure.ts +1 -0
  562. package/src/memory/schema/oauth.ts +4 -1
  563. package/src/memory/slack-thread-store.ts +37 -0
  564. package/src/messaging/providers/gmail/adapter.ts +6 -16
  565. package/src/messaging/providers/gmail/client.ts +22 -0
  566. package/src/messaging/providers/gmail/types.ts +7 -0
  567. package/src/messaging/providers/slack/adapter.ts +14 -2
  568. package/src/messaging/providers/slack/backfill.test.ts +257 -0
  569. package/src/messaging/providers/slack/backfill.ts +101 -0
  570. package/src/messaging/providers/slack/message-metadata.test.ts +316 -0
  571. package/src/messaging/providers/slack/message-metadata.ts +123 -0
  572. package/src/messaging/providers/slack/render-transcript.test.ts +1421 -0
  573. package/src/messaging/providers/slack/render-transcript.ts +501 -0
  574. package/src/messaging/style-analyzer.ts +5 -2
  575. package/src/notifications/README.md +9 -5
  576. package/src/notifications/conversation-pairing.ts +78 -19
  577. package/src/notifications/copy-composer.ts +0 -5
  578. package/src/notifications/decision-engine.ts +3 -9
  579. package/src/notifications/emit-signal.ts +1 -1
  580. package/src/notifications/preference-extractor.ts +2 -6
  581. package/src/notifications/signal.ts +1 -2
  582. package/src/oauth/AGENTS.md +1 -1
  583. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  584. package/src/oauth/connect-orchestrator.ts +8 -34
  585. package/src/oauth/connect-types.ts +6 -10
  586. package/src/oauth/manual-token-connection.ts +23 -0
  587. package/src/oauth/oauth-store.ts +31 -14
  588. package/src/oauth/platform-connection.test.ts +47 -0
  589. package/src/oauth/platform-connection.ts +15 -5
  590. package/src/oauth/provider-serializer.ts +6 -1
  591. package/src/oauth/seed-providers.ts +56 -106
  592. package/src/outbound-proxy/http-forwarder.ts +9 -0
  593. package/src/permissions/approval-policy.test.ts +1223 -0
  594. package/src/permissions/approval-policy.ts +309 -0
  595. package/src/permissions/arg-parser.test.ts +161 -0
  596. package/src/permissions/arg-parser.ts +141 -0
  597. package/src/permissions/bash-risk-classifier.test.ts +1620 -0
  598. package/src/permissions/bash-risk-classifier.ts +950 -0
  599. package/src/permissions/checker.ts +348 -711
  600. package/src/permissions/command-registry.test.ts +774 -0
  601. package/src/permissions/command-registry.ts +1005 -0
  602. package/src/permissions/defaults.ts +28 -79
  603. package/src/permissions/file-risk-classifier.test.ts +535 -0
  604. package/src/permissions/file-risk-classifier.ts +274 -0
  605. package/src/permissions/gateway-threshold-reader.ts +196 -0
  606. package/src/permissions/prompter.ts +4 -0
  607. package/src/permissions/risk-types.ts +262 -0
  608. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  609. package/src/permissions/schedule-risk-classifier.ts +85 -0
  610. package/src/permissions/secret-prompter.ts +53 -2
  611. package/src/permissions/shell-identity.ts +2 -42
  612. package/src/permissions/skill-risk-classifier.test.ts +311 -0
  613. package/src/permissions/skill-risk-classifier.ts +214 -0
  614. package/src/permissions/trust-client.ts +52 -25
  615. package/src/permissions/trust-store-interface.ts +1 -6
  616. package/src/permissions/trust-store.ts +161 -62
  617. package/src/permissions/types.ts +25 -14
  618. package/src/permissions/web-risk-classifier.test.ts +170 -0
  619. package/src/permissions/web-risk-classifier.ts +89 -0
  620. package/src/permissions/workspace-policy.ts +9 -19
  621. package/src/platform/client.ts +19 -1
  622. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  623. package/src/plugins/defaults/compaction.ts +145 -0
  624. package/src/plugins/defaults/empty-response.ts +126 -0
  625. package/src/plugins/defaults/history-repair.ts +85 -0
  626. package/src/plugins/defaults/index.ts +116 -0
  627. package/src/plugins/defaults/injectors.ts +491 -0
  628. package/src/plugins/defaults/llm-call.ts +82 -0
  629. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  630. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  631. package/src/plugins/defaults/persistence.ts +129 -0
  632. package/src/plugins/defaults/title-generate.ts +95 -0
  633. package/src/plugins/defaults/token-estimate.ts +104 -0
  634. package/src/plugins/defaults/tool-error.ts +126 -0
  635. package/src/plugins/defaults/tool-execute.ts +89 -0
  636. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  637. package/src/plugins/pipeline.ts +316 -0
  638. package/src/plugins/plugin-skill-contributions.ts +292 -0
  639. package/src/plugins/registry.ts +241 -0
  640. package/src/plugins/types.ts +1134 -0
  641. package/src/plugins/user-loader.ts +177 -0
  642. package/src/prompts/persona-resolver.ts +3 -3
  643. package/src/prompts/system-prompt.ts +19 -20
  644. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  645. package/src/prompts/templates/SOUL.md +2 -2
  646. package/src/prompts/update-bulletin-job.ts +190 -0
  647. package/src/providers/__tests__/context-overflow-error.test.ts +328 -0
  648. package/src/providers/__tests__/provider-env-vars.test.ts +102 -0
  649. package/src/providers/__tests__/retry-callsite.test.ts +424 -0
  650. package/src/providers/anthropic/client.ts +183 -14
  651. package/src/providers/call-site-routing.ts +71 -0
  652. package/src/providers/gemini/client.ts +65 -2
  653. package/src/providers/managed-proxy/constants.ts +2 -1
  654. package/src/providers/model-catalog.ts +524 -33
  655. package/src/providers/model-intents.ts +4 -4
  656. package/src/providers/openai/chat-completions-provider.ts +57 -1
  657. package/src/providers/openai/responses-provider.ts +86 -9
  658. package/src/providers/openrouter/client.ts +80 -9
  659. package/src/providers/provider-env-vars.ts +56 -0
  660. package/src/providers/provider-send-message.ts +22 -5
  661. package/src/providers/ratelimit.ts +4 -0
  662. package/src/providers/registry.ts +19 -8
  663. package/src/providers/retry.ts +174 -39
  664. package/src/providers/speech-to-text/__tests__/resolve.test.ts +55 -0
  665. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  666. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  667. package/src/providers/speech-to-text/google-gemini-live-stream.ts +4 -4
  668. package/src/providers/speech-to-text/provider-catalog.ts +17 -0
  669. package/src/providers/speech-to-text/resolve.ts +7 -0
  670. package/src/providers/speech-to-text/xai-realtime.test.ts +646 -0
  671. package/src/providers/speech-to-text/xai-realtime.ts +821 -0
  672. package/src/providers/speech-to-text/xai.test.ts +155 -0
  673. package/src/providers/speech-to-text/xai.ts +97 -0
  674. package/src/providers/types.ts +93 -3
  675. package/src/runtime/AGENTS.md +27 -18
  676. package/src/runtime/__tests__/agent-wake.test.ts +43 -2
  677. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  678. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  679. package/src/runtime/__tests__/interactive-ui.test.ts +673 -0
  680. package/src/runtime/agent-wake.ts +63 -22
  681. package/src/runtime/auth/route-policy.ts +4 -0
  682. package/src/runtime/btw-sidechain.ts +13 -3
  683. package/src/runtime/channel-reply-delivery.ts +106 -2
  684. package/src/runtime/client-registry.ts +261 -0
  685. package/src/runtime/decision-token.ts +116 -0
  686. package/src/runtime/gateway-client.ts +2 -2
  687. package/src/runtime/http-router.ts +32 -0
  688. package/src/runtime/http-server.ts +129 -9
  689. package/src/runtime/http-types.ts +23 -3
  690. package/src/runtime/interactive-ui.ts +362 -0
  691. package/src/runtime/invite-instruction-generator.ts +2 -2
  692. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +176 -0
  693. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +390 -0
  694. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +221 -0
  695. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +1540 -0
  696. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +453 -0
  697. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +222 -0
  698. package/src/runtime/migrations/gcs-signed-url.ts +162 -0
  699. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  700. package/src/runtime/migrations/vbundle-importer.ts +154 -9
  701. package/src/runtime/migrations/vbundle-metadata-merge.ts +124 -0
  702. package/src/runtime/migrations/vbundle-streaming-importer.ts +2522 -0
  703. package/src/runtime/migrations/vbundle-streaming-validator.ts +244 -0
  704. package/src/runtime/migrations/vbundle-tar-stream.ts +217 -0
  705. package/src/runtime/migrations/vbundle-validator.ts +15 -6
  706. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +111 -0
  707. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +114 -75
  708. package/src/runtime/routes/__tests__/migration-vellum-metadata-reconcile.test.ts +246 -0
  709. package/src/runtime/routes/approval-prompt-ts-tracker.ts +78 -0
  710. package/src/runtime/routes/approval-routes.ts +29 -17
  711. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +9 -0
  712. package/src/runtime/routes/avatar-routes.ts +20 -4
  713. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  714. package/src/runtime/routes/btw-routes.ts +1 -4
  715. package/src/runtime/routes/conversation-management-routes.ts +20 -2
  716. package/src/runtime/routes/conversation-routes.ts +351 -138
  717. package/src/runtime/routes/debug-routes.ts +1 -1
  718. package/src/runtime/routes/diagnostics-routes.ts +6 -4
  719. package/src/runtime/routes/events-routes.ts +16 -0
  720. package/src/runtime/routes/guardian-approval-interception.ts +33 -3
  721. package/src/runtime/routes/guardian-approval-prompt.ts +13 -3
  722. package/src/runtime/routes/home-feed-routes.ts +120 -2
  723. package/src/runtime/routes/inbound-message-handler.ts +987 -2
  724. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +113 -2
  725. package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -3
  726. package/src/runtime/routes/inbound-stages/edit-intercept.ts +129 -6
  727. package/src/runtime/routes/integrations/slack/channel.ts +25 -3
  728. package/src/runtime/routes/llm-context-normalization.ts +23 -1
  729. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  730. package/src/runtime/routes/migration-routes.ts +720 -127
  731. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  732. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  733. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  734. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  735. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  736. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  737. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  738. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  739. package/src/runtime/routes/playground/deps.ts +56 -0
  740. package/src/runtime/routes/playground/force-compact.ts +73 -0
  741. package/src/runtime/routes/playground/guard.ts +37 -0
  742. package/src/runtime/routes/playground/index.ts +28 -0
  743. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  744. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  745. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  746. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  747. package/src/runtime/routes/playground/state.ts +78 -0
  748. package/src/runtime/routes/schedule-routes.ts +89 -8
  749. package/src/runtime/routes/settings-routes.ts +4 -2
  750. package/src/runtime/routes/trust-rules-routes.ts +30 -14
  751. package/src/runtime/routes/work-items-routes.test.ts +1 -1
  752. package/src/runtime/routes/work-items-routes.ts +3 -2
  753. package/src/runtime/services/__tests__/analyze-conversation.test.ts +25 -43
  754. package/src/runtime/services/analyze-conversation.ts +12 -16
  755. package/src/runtime/skill-route-registry.ts +97 -15
  756. package/src/schedule/run-script.ts +68 -0
  757. package/src/schedule/schedule-store.ts +7 -1
  758. package/src/schedule/scheduler.ts +56 -8
  759. package/src/security/__tests__/provider-key-env-fallback.test.ts +119 -0
  760. package/src/security/__tests__/untrusted-content.test.ts +109 -0
  761. package/src/security/oauth2.ts +98 -35
  762. package/src/security/secure-keys.ts +7 -8
  763. package/src/security/token-manager.ts +27 -13
  764. package/src/security/untrusted-content.ts +102 -0
  765. package/src/skills/catalog-cache.ts +35 -9
  766. package/src/skills/catalog-install.ts +31 -3
  767. package/src/skills/skill-cache-store.ts +97 -0
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +76 -0
  769. package/src/stt/daemon-batch-transcriber.ts +33 -0
  770. package/src/stt/stt-stream-session.ts +8 -1
  771. package/src/stt/types.ts +5 -1
  772. package/src/subagent/manager.ts +41 -13
  773. package/src/tasks/ephemeral-permissions.ts +9 -4
  774. package/src/telemetry/usage-telemetry-reporter.ts +27 -5
  775. package/src/tools/browser/__tests__/browser-status.test.ts +234 -2
  776. package/src/tools/browser/browser-execution.ts +150 -54
  777. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  778. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  779. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +22 -0
  780. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  781. package/src/tools/browser/cdp-client/factory.ts +15 -4
  782. package/src/tools/credentials/tool-policy.ts +39 -5
  783. package/src/tools/credentials/vault.ts +9 -4
  784. package/src/tools/executor.ts +129 -73
  785. package/src/tools/filesystem/write.ts +52 -0
  786. package/src/tools/host-terminal/host-shell.ts +45 -5
  787. package/src/tools/memory/register.test.ts +185 -0
  788. package/src/tools/memory/register.ts +3 -1
  789. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  790. package/src/tools/network/web-fetch.ts +20 -10
  791. package/src/tools/network/web-search.ts +19 -4
  792. package/src/tools/permission-checker.ts +116 -46
  793. package/src/tools/policy-context.ts +29 -8
  794. package/src/tools/registry.ts +195 -6
  795. package/src/tools/schedule/create.ts +23 -8
  796. package/src/tools/schedule/update.ts +3 -1
  797. package/src/tools/secret-detection-handler.ts +0 -51
  798. package/src/tools/side-effects.ts +0 -11
  799. package/src/tools/skills/execute.ts +2 -2
  800. package/src/tools/skills/sandbox-runner.ts +5 -2
  801. package/src/tools/system/avatar-generator.ts +6 -2
  802. package/src/tools/terminal/backends/native.ts +51 -2
  803. package/src/tools/terminal/safe-env.ts +3 -2
  804. package/src/tools/terminal/shell.ts +1 -0
  805. package/src/tools/tool-manifest.ts +6 -21
  806. package/src/tools/types.ts +40 -5
  807. package/src/tools/verification-control-plane-policy.ts +1 -1
  808. package/src/tts/__tests__/provider-adapters.test.ts +240 -13
  809. package/src/tts/provider-catalog.ts +18 -0
  810. package/src/tts/providers/index.ts +2 -0
  811. package/src/tts/providers/xai-provider.ts +224 -0
  812. package/src/tts/types.ts +46 -0
  813. package/src/types/tar-stream.d.ts +66 -0
  814. package/src/util/json.ts +17 -0
  815. package/src/util/platform.ts +9 -4
  816. package/src/util/pricing.ts +41 -8
  817. package/src/watcher/engine.ts +1 -1
  818. package/src/watcher/providers/google-calendar.ts +134 -8
  819. package/src/watcher/providers/outlook-calendar.ts +42 -2
  820. package/src/workspace/git-service.ts +23 -4
  821. package/src/workspace/migrations/006-services-config.ts +2 -4
  822. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  823. package/src/workspace/migrations/038-unify-llm-callsite-configs.ts +516 -0
  824. package/src/workspace/migrations/039-drop-legacy-llm-keys.ts +171 -0
  825. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +154 -0
  826. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +56 -0
  827. package/src/workspace/migrations/042-fix-backfill-google-gmail-settings-scope.ts +70 -0
  828. package/src/workspace/migrations/043-release-notes-latex-rendering.ts +75 -0
  829. package/src/workspace/migrations/044-bump-stale-provider-stream-timeout.ts +51 -0
  830. package/src/workspace/migrations/045-release-notes-meet-avatar.ts +130 -0
  831. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  832. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  833. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  834. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  835. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  836. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  837. package/src/workspace/migrations/AGENTS.md +1 -1
  838. package/src/workspace/migrations/registry.ts +28 -0
  839. package/src/workspace/provider-commit-message-generator.ts +19 -38
  840. package/tsconfig.json +1 -1
  841. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  842. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  843. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  844. package/src/__tests__/gmail-archive-fallback.test.ts +0 -193
  845. package/src/__tests__/gmail-archive-gate.test.ts +0 -246
  846. package/src/__tests__/gmail-preferences.test.ts +0 -117
  847. package/src/__tests__/hooks-blocking.test.ts +0 -178
  848. package/src/__tests__/hooks-cli.test.ts +0 -182
  849. package/src/__tests__/hooks-config.test.ts +0 -108
  850. package/src/__tests__/hooks-discovery.test.ts +0 -211
  851. package/src/__tests__/hooks-integration.test.ts +0 -196
  852. package/src/__tests__/hooks-manager.test.ts +0 -226
  853. package/src/__tests__/hooks-runner.test.ts +0 -175
  854. package/src/__tests__/hooks-settings.test.ts +0 -160
  855. package/src/__tests__/hooks-templates.test.ts +0 -169
  856. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  857. package/src/__tests__/hooks-watch.test.ts +0 -112
  858. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  859. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  860. package/src/__tests__/outlook-attachments.test.ts +0 -301
  861. package/src/__tests__/outlook-automation-tools.test.ts +0 -425
  862. package/src/__tests__/outlook-categories.test.ts +0 -212
  863. package/src/__tests__/outlook-compose-tools.test.ts +0 -325
  864. package/src/__tests__/outlook-declutter-tools.test.ts +0 -585
  865. package/src/__tests__/outlook-follow-up.test.ts +0 -196
  866. package/src/__tests__/outlook-trash.test.ts +0 -77
  867. package/src/__tests__/outlook-unsubscribe.test.ts +0 -279
  868. package/src/__tests__/send-notification-tool.test.ts +0 -83
  869. package/src/__tests__/update-bulletin-format.test.ts +0 -181
  870. package/src/__tests__/update-bulletin-state.test.ts +0 -135
  871. package/src/__tests__/update-bulletin.test.ts +0 -478
  872. package/src/__tests__/update-template-contract.test.ts +0 -29
  873. package/src/cli/commands/doctor.ts +0 -341
  874. package/src/cli/commands/shotgun.ts +0 -266
  875. package/src/config/bundled-skills/browser/SKILL.md +0 -88
  876. package/src/config/bundled-skills/browser/TOOLS.json +0 -516
  877. package/src/config/bundled-skills/browser/tools/browser-attach.ts +0 -12
  878. package/src/config/bundled-skills/browser/tools/browser-click.ts +0 -12
  879. package/src/config/bundled-skills/browser/tools/browser-close.ts +0 -12
  880. package/src/config/bundled-skills/browser/tools/browser-detach.ts +0 -12
  881. package/src/config/bundled-skills/browser/tools/browser-extract.ts +0 -12
  882. package/src/config/bundled-skills/browser/tools/browser-fill-credential.ts +0 -12
  883. package/src/config/bundled-skills/browser/tools/browser-hover.ts +0 -12
  884. package/src/config/bundled-skills/browser/tools/browser-navigate.ts +0 -12
  885. package/src/config/bundled-skills/browser/tools/browser-press-key.ts +0 -12
  886. package/src/config/bundled-skills/browser/tools/browser-screenshot.ts +0 -12
  887. package/src/config/bundled-skills/browser/tools/browser-scroll.ts +0 -12
  888. package/src/config/bundled-skills/browser/tools/browser-select-option.ts +0 -12
  889. package/src/config/bundled-skills/browser/tools/browser-snapshot.ts +0 -12
  890. package/src/config/bundled-skills/browser/tools/browser-status.ts +0 -12
  891. package/src/config/bundled-skills/browser/tools/browser-type.ts +0 -12
  892. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +0 -49
  893. package/src/config/bundled-skills/browser/tools/browser-wait-for.ts +0 -12
  894. package/src/config/bundled-skills/chatgpt-import/SKILL.md +0 -27
  895. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +0 -27
  896. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +0 -378
  897. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  898. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  899. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -66
  900. package/src/config/bundled-skills/gmail/SKILL.md +0 -221
  901. package/src/config/bundled-skills/gmail/TOOLS.json +0 -588
  902. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +0 -256
  903. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +0 -112
  904. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +0 -44
  905. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +0 -81
  906. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +0 -108
  907. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +0 -146
  908. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +0 -53
  909. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +0 -347
  910. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +0 -59
  911. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +0 -82
  912. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +0 -26
  913. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +0 -347
  914. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +0 -29
  915. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +0 -122
  916. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +0 -67
  917. package/src/config/bundled-skills/gmail/tools/scan-result-store.ts +0 -100
  918. package/src/config/bundled-skills/gmail/tools/shared.ts +0 -47
  919. package/src/config/bundled-skills/google-calendar/SKILL.md +0 -51
  920. package/src/config/bundled-skills/google-calendar/TOOLS.json +0 -226
  921. package/src/config/bundled-skills/google-calendar/calendar-client.ts +0 -223
  922. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +0 -27
  923. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +0 -48
  924. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +0 -19
  925. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +0 -36
  926. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +0 -58
  927. package/src/config/bundled-skills/google-calendar/tools/shared.ts +0 -17
  928. package/src/config/bundled-skills/google-calendar/types.ts +0 -97
  929. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  930. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  931. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  932. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  933. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  934. package/src/config/bundled-skills/outlook/SKILL.md +0 -196
  935. package/src/config/bundled-skills/outlook/TOOLS.json +0 -530
  936. package/src/config/bundled-skills/outlook/tools/outlook-attachments.ts +0 -85
  937. package/src/config/bundled-skills/outlook/tools/outlook-categories.ts +0 -77
  938. package/src/config/bundled-skills/outlook/tools/outlook-draft.ts +0 -84
  939. package/src/config/bundled-skills/outlook/tools/outlook-follow-up.ts +0 -94
  940. package/src/config/bundled-skills/outlook/tools/outlook-forward.ts +0 -49
  941. package/src/config/bundled-skills/outlook/tools/outlook-outreach-scan.ts +0 -237
  942. package/src/config/bundled-skills/outlook/tools/outlook-rules.ts +0 -161
  943. package/src/config/bundled-skills/outlook/tools/outlook-send-draft.ts +0 -32
  944. package/src/config/bundled-skills/outlook/tools/outlook-sender-digest.ts +0 -272
  945. package/src/config/bundled-skills/outlook/tools/outlook-trash.ts +0 -29
  946. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +0 -129
  947. package/src/config/bundled-skills/outlook/tools/outlook-vacation.ts +0 -87
  948. package/src/config/bundled-skills/outlook/tools/shared.ts +0 -20
  949. package/src/config/bundled-skills/outlook-calendar/SKILL.md +0 -51
  950. package/src/config/bundled-skills/outlook-calendar/TOOLS.json +0 -221
  951. package/src/config/bundled-skills/outlook-calendar/calendar-client.ts +0 -252
  952. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-check-availability.ts +0 -53
  953. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-create-event.ts +0 -74
  954. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-get-event.ts +0 -18
  955. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-list-events.ts +0 -46
  956. package/src/config/bundled-skills/outlook-calendar/tools/outlook-calendar-rsvp.ts +0 -36
  957. package/src/config/bundled-skills/outlook-calendar/tools/shared.ts +0 -17
  958. package/src/config/bundled-skills/outlook-calendar/types.ts +0 -120
  959. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  960. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  961. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  962. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  963. package/src/config/bundled-skills/slack/SKILL.md +0 -108
  964. package/src/config/bundled-skills/tasks/SKILL.md +0 -37
  965. package/src/config/bundled-skills/tasks/TOOLS.json +0 -353
  966. package/src/config/bundled-skills/tasks/icon.svg +0 -34
  967. package/src/config/bundled-skills/tasks/tools/task-delete.ts +0 -12
  968. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +0 -12
  969. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +0 -12
  970. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +0 -12
  971. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +0 -12
  972. package/src/config/bundled-skills/tasks/tools/task-list.ts +0 -12
  973. package/src/config/bundled-skills/tasks/tools/task-queue-run.ts +0 -12
  974. package/src/config/bundled-skills/tasks/tools/task-run.ts +0 -12
  975. package/src/config/bundled-skills/tasks/tools/task-save.ts +0 -12
  976. package/src/config/bundled-skills/watcher/SKILL.md +0 -31
  977. package/src/config/bundled-skills/watcher/TOOLS.json +0 -167
  978. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +0 -12
  979. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +0 -12
  980. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +0 -12
  981. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +0 -12
  982. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +0 -12
  983. package/src/daemon/context-overflow-approval.ts +0 -52
  984. package/src/daemon/watch-handler.ts +0 -399
  985. package/src/hooks/cli.ts +0 -253
  986. package/src/hooks/config.ts +0 -100
  987. package/src/hooks/discovery.ts +0 -135
  988. package/src/hooks/manager.ts +0 -179
  989. package/src/hooks/runner.ts +0 -117
  990. package/src/hooks/templates.ts +0 -77
  991. package/src/hooks/types.ts +0 -75
  992. package/src/oauth/scope-policy.ts +0 -89
  993. package/src/prompts/templates/UPDATES.md +0 -50
  994. package/src/prompts/update-bulletin-format.ts +0 -85
  995. package/src/prompts/update-bulletin-state.ts +0 -58
  996. package/src/prompts/update-bulletin-template-path.ts +0 -13
  997. package/src/prompts/update-bulletin.ts +0 -139
  998. package/src/runtime/gateway-internal-client.ts +0 -94
  999. package/src/runtime/routes/watch-routes.ts +0 -156
  1000. package/src/shared/provider-env-vars.ts +0 -19
  1001. package/src/signals/shotgun.ts +0 -203
  1002. package/src/tools/watch/screen-watch.ts +0 -144
  1003. package/src/tools/watch/watch-state.ts +0 -142
  1004. package/src/tools/watcher/create.ts +0 -86
  1005. package/src/tools/watcher/delete.ts +0 -36
  1006. package/src/tools/watcher/digest.ts +0 -54
  1007. package/src/tools/watcher/list.ts +0 -83
  1008. package/src/tools/watcher/update.ts +0 -71
@@ -19,10 +19,29 @@ import {
19
19
  recordConversationSeenSignal,
20
20
  type SignalType,
21
21
  } from "../../memory/conversation-attention-store.js";
22
+ import {
23
+ addMessage,
24
+ getMessageById,
25
+ getMessages,
26
+ selectSlackMetaCandidateMetadata,
27
+ updateMessageMetadata,
28
+ } from "../../memory/conversation-crud.js";
22
29
  import * as deliveryChannels from "../../memory/delivery-channels.js";
23
30
  import * as deliveryCrud from "../../memory/delivery-crud.js";
24
31
  import * as deliveryStatus from "../../memory/delivery-status.js";
25
32
  import * as externalConversationStore from "../../memory/external-conversation-store.js";
33
+ import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
34
+ import {
35
+ backfillDm,
36
+ backfillThread,
37
+ } from "../../messaging/providers/slack/backfill.js";
38
+ import {
39
+ mergeSlackMetadata,
40
+ readSlackMetadata,
41
+ type SlackMessageMetadata,
42
+ writeSlackMetadata,
43
+ } from "../../messaging/providers/slack/message-metadata.js";
44
+ import { wrapUntrustedContent } from "../../security/untrusted-content.js";
26
45
  import { canonicalizeInboundIdentity } from "../../util/canonicalize-identity.js";
27
46
  import { getLogger } from "../../util/logger.js";
28
47
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
@@ -52,6 +71,28 @@ import { handleVerificationIntercept } from "./inbound-stages/verification-inter
52
71
 
53
72
  const log = getLogger("runtime-http");
54
73
 
74
+ // Delete-lookup retry configuration. Delete webhooks can race ahead of
75
+ // the inbound handler's `linkMessage` call when the original message's
76
+ // agent loop is still running. Retrying buys time for the link to land
77
+ // before we drop the deletion signal. Mirrors the edit-intercept path's
78
+ // EDIT_LOOKUP_RETRIES / EDIT_LOOKUP_DELAY_MS constants.
79
+ let deleteLookupRetries = 5;
80
+ let deleteLookupDelayMs = 2000;
81
+
82
+ /**
83
+ * Test-only override for the delete-lookup retry timings. Used by
84
+ * tests that exercise the "no such message" path without waiting
85
+ * through the full production backoff. Not exported from any barrel
86
+ * file — only the test file imports it directly.
87
+ */
88
+ export function _setDeleteLookupConfigForTests(
89
+ retries: number,
90
+ delayMs: number,
91
+ ): void {
92
+ deleteLookupRetries = retries;
93
+ deleteLookupDelayMs = delayMs;
94
+ }
95
+
55
96
  export async function handleChannelInbound(
56
97
  req: Request,
57
98
  processMessage?: MessageProcessor,
@@ -238,6 +279,152 @@ export async function handleChannelInbound(
238
279
  if (aclResult.earlyResponse) return aclResult.earlyResponse;
239
280
  const { resolvedMember, guardianVerifyCode } = aclResult;
240
281
 
282
+ // ── Slack delete propagation ──
283
+ // Slack message_deleted events are forwarded by the gateway with the
284
+ // sentinel `callbackData = "message_deleted"` and `sourceMetadata.messageId`
285
+ // set to the original (deleted) message's ts. Short-circuit the rest of
286
+ // the pipeline: the agent loop should not run for delete notifications,
287
+ // and routing the event through approval / agent paths would be incorrect.
288
+ // We mark the stored row as deleted in slackMeta but leave `content`
289
+ // untouched for audit purposes — rendering elides based on the deletedAt
290
+ // marker. Gated behind ingress ACL so non-members cannot drive deletes
291
+ // (matches the edit-intercept policy).
292
+ if (sourceChannel === "slack" && body.callbackData === "message_deleted") {
293
+ const deletedMessageTs =
294
+ typeof sourceMetadata?.messageId === "string"
295
+ ? sourceMetadata.messageId
296
+ : undefined;
297
+
298
+ if (!deletedMessageTs) {
299
+ log.debug(
300
+ { conversationExternalId },
301
+ "Slack message_deleted event missing sourceMetadata.messageId; ignoring",
302
+ );
303
+ return Response.json({ accepted: true, deleted: false });
304
+ }
305
+
306
+ // Look up the stored message via the existing channel-event lookup.
307
+ // The original message's externalMessageId may differ from its ts
308
+ // (Slack populates client_msg_id when present), so we join via the
309
+ // sourceMessageId column which records the ts explicitly.
310
+ //
311
+ // Retry with backoff mirrors the edit-intercept path: delete webhooks
312
+ // can race ahead of `linkMessage` when the original message's agent
313
+ // loop is still running. Without retries a delete that arrives in
314
+ // that window is silently dropped and the deletion signal is lost.
315
+ let original: { messageId: string; conversationId: string } | null = null;
316
+ for (let attempt = 0; attempt <= deleteLookupRetries; attempt++) {
317
+ original = deliveryCrud.findMessageBySourceId(
318
+ sourceChannel,
319
+ conversationExternalId,
320
+ deletedMessageTs,
321
+ );
322
+ if (original) break;
323
+ if (attempt < deleteLookupRetries) {
324
+ log.info(
325
+ {
326
+ conversationExternalId,
327
+ deletedMessageTs,
328
+ attempt: attempt + 1,
329
+ maxAttempts: deleteLookupRetries,
330
+ },
331
+ "Original message not linked yet, retrying delete lookup",
332
+ );
333
+ await new Promise((resolve) =>
334
+ setTimeout(resolve, deleteLookupDelayMs),
335
+ );
336
+ }
337
+ }
338
+
339
+ if (!original) {
340
+ log.debug(
341
+ { conversationExternalId, deletedMessageTs },
342
+ "No stored message found for Slack delete after retries; ignoring",
343
+ );
344
+ return Response.json({ accepted: true, deleted: false });
345
+ }
346
+
347
+ // Merge deletedAt into the existing slackMeta sub-key. If the row has
348
+ // no slackMeta (legacy pre-upgrade row), skip — the renderer's flat
349
+ // fallback ignores deletedAt for those rows anyway, and synthesizing
350
+ // a partial slackMeta here would produce metadata that fails
351
+ // readSlackMetadata validation.
352
+ const row = getMessageById(original.messageId);
353
+ if (!row?.metadata) {
354
+ log.debug(
355
+ {
356
+ conversationExternalId,
357
+ deletedMessageTs,
358
+ messageId: original.messageId,
359
+ },
360
+ "Stored Slack message has no metadata; skipping delete marker",
361
+ );
362
+ return Response.json({ accepted: true, deleted: false });
363
+ }
364
+
365
+ let parentMetadata: Record<string, unknown>;
366
+ try {
367
+ const parsed = JSON.parse(row.metadata) as unknown;
368
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
369
+ parentMetadata = parsed as Record<string, unknown>;
370
+ } else {
371
+ parentMetadata = {};
372
+ }
373
+ } catch {
374
+ log.debug(
375
+ {
376
+ conversationExternalId,
377
+ deletedMessageTs,
378
+ messageId: original.messageId,
379
+ },
380
+ "Failed to parse stored metadata; skipping delete marker",
381
+ );
382
+ return Response.json({ accepted: true, deleted: false });
383
+ }
384
+
385
+ const existingSlackMeta =
386
+ typeof parentMetadata.slackMeta === "string"
387
+ ? parentMetadata.slackMeta
388
+ : null;
389
+
390
+ if (!existingSlackMeta) {
391
+ log.debug(
392
+ {
393
+ conversationExternalId,
394
+ deletedMessageTs,
395
+ messageId: original.messageId,
396
+ },
397
+ "Stored Slack message has no slackMeta; skipping delete marker",
398
+ );
399
+ return Response.json({ accepted: true, deleted: false });
400
+ }
401
+
402
+ const updatedSlackMeta = mergeSlackMetadata(existingSlackMeta, {
403
+ deletedAt: Date.now(),
404
+ });
405
+
406
+ // updateMessageMetadata performs a shallow merge over the parent
407
+ // metadata, replacing only `slackMeta` and leaving sibling keys
408
+ // (channel, interface, provenance, etc.) untouched. Content column
409
+ // is intentionally not updated.
410
+ updateMessageMetadata(original.messageId, { slackMeta: updatedSlackMeta });
411
+
412
+ log.info(
413
+ {
414
+ conversationExternalId,
415
+ deletedMessageTs,
416
+ messageId: original.messageId,
417
+ },
418
+ "Marked Slack message as deleted",
419
+ );
420
+
421
+ return Response.json({
422
+ accepted: true,
423
+ deleted: true,
424
+ messageId: original.messageId,
425
+ });
426
+ }
427
+
241
428
  if (hasAttachments) {
242
429
  const resolved = attachmentsStore.getAttachmentsByIds(attachmentIds);
243
430
  if (resolved.length !== attachmentIds.length) {
@@ -369,6 +556,119 @@ export async function handleChannelInbound(
369
556
  });
370
557
  }
371
558
 
559
+ // ── Slack reaction handling ──
560
+ // Reactions arrive as regular `SlackInboundEvent`s with `callbackData`
561
+ // prefixed `reaction:` (added) or `reaction_removed:` (removed).
562
+ //
563
+ // Two paths from here:
564
+ // 1. Guardian approval-by-reaction. A `reaction:` (added) event from
565
+ // the guardian on an active approval prompt is consumed by
566
+ // `handleApprovalInterception` to apply the decision. In that case
567
+ // we do NOT persist the reaction as a transcript line — resolved
568
+ // guardian approval reactions have no transcript representation.
569
+ // 2. All other reactions (non-guardian, no pending approval, stale,
570
+ // and any `reaction_removed:` event regardless of actor) fall
571
+ // through to `persistSlackReactionAsMessage` so the chronological
572
+ // renderer (PR 18) can surface them inline. Reactions never
573
+ // trigger an agent response, so we short-circuit before
574
+ // escalation and agent-loop dispatch in both cases.
575
+ if (isSlackReactionEvent(body)) {
576
+ // Approval interception runs only for reactions (added) — `reaction_removed`
577
+ // never expresses an approval intent, so un-reacting is left as a pure
578
+ // transcript signal. Gated by the same `replyCallbackUrl && !duplicate`
579
+ // preconditions used by the standard approval interception call below.
580
+ const isReactionAdded = body.callbackData?.startsWith("reaction:") === true;
581
+ if (isReactionAdded && replyCallbackUrl && !result.duplicate) {
582
+ const trustCtxForReaction: TrustContext = resolveTrustContext({
583
+ assistantId: canonicalAssistantId,
584
+ sourceChannel,
585
+ conversationExternalId,
586
+ actorExternalId: rawSenderId,
587
+ actorUsername: body.actorUsername,
588
+ actorDisplayName: body.actorDisplayName,
589
+ });
590
+
591
+ const approvalMessageTs =
592
+ typeof sourceMetadata?.messageId === "string"
593
+ ? sourceMetadata.messageId
594
+ : undefined;
595
+
596
+ const reactionApprovalResult = await handleApprovalInterception({
597
+ conversationId: result.conversationId,
598
+ callbackData: body.callbackData,
599
+ content: trimmedContent,
600
+ conversationExternalId,
601
+ sourceChannel,
602
+ actorExternalId: canonicalSenderId ?? rawSenderId,
603
+ replyCallbackUrl,
604
+ bearerToken: mintBearerToken(),
605
+ trustCtx: trustCtxForReaction,
606
+ assistantId: canonicalAssistantId,
607
+ approvalCopyGenerator,
608
+ approvalConversationGenerator,
609
+ approvalMessageTs,
610
+ });
611
+
612
+ // A real guardian decision was applied — short-circuit and skip the
613
+ // reaction-persistence path so we do not double-record it as a
614
+ // transcript line. All other interception outcomes (stale_ignored,
615
+ // non-guardian, no pending approval) fall through to persistence.
616
+ if (reactionApprovalResult.type === "guardian_decision_applied") {
617
+ return Response.json({
618
+ accepted: true,
619
+ duplicate: false,
620
+ eventId: result.eventId,
621
+ approval: reactionApprovalResult.type,
622
+ });
623
+ }
624
+ }
625
+
626
+ const reactedMessageTs =
627
+ typeof sourceMetadata?.messageId === "string"
628
+ ? sourceMetadata.messageId
629
+ : undefined;
630
+ if (!reactedMessageTs) {
631
+ log.debug(
632
+ { conversationId: result.conversationId, eventId: result.eventId },
633
+ "Skipping reaction persistence: missing sourceMetadata.messageId",
634
+ );
635
+ return Response.json({
636
+ accepted: result.accepted,
637
+ duplicate: result.duplicate,
638
+ eventId: result.eventId,
639
+ });
640
+ }
641
+
642
+ const threadTs =
643
+ typeof sourceMetadata?.threadId === "string"
644
+ ? sourceMetadata.threadId
645
+ : undefined;
646
+
647
+ try {
648
+ await persistSlackReactionAsMessage({
649
+ conversationId: result.conversationId,
650
+ conversationExternalId,
651
+ eventId: result.eventId,
652
+ callbackData: body.callbackData!,
653
+ actorDisplayName: body.actorDisplayName,
654
+ threadTs,
655
+ reactedMessageTs,
656
+ duplicate: result.duplicate,
657
+ });
658
+ } catch (err) {
659
+ log.error(
660
+ { err, conversationId: result.conversationId, eventId: result.eventId },
661
+ "Failed to persist Slack reaction event",
662
+ );
663
+ }
664
+
665
+ return Response.json({
666
+ accepted: result.accepted,
667
+ duplicate: result.duplicate,
668
+ eventId: result.eventId,
669
+ });
670
+ }
671
+
372
672
  // ── Ingress escalation ──
373
673
  const escalationResponse = handleEscalationIntercept({
374
674
  resolvedMember,
@@ -606,7 +906,13 @@ export async function handleChannelInbound(
606
906
  // of whether content/attachments are present — callback payloads always
607
907
  // have non-empty content (normalize.ts sets message.content to cbq.data),
608
908
  // so checking for empty content alone would miss stale callbacks.
609
- if (hasCallbackData) {
909
+ //
910
+ // Reaction events (`reaction:` / `reaction_removed:`) are persisted by
911
+ // the earlier `isSlackReactionEvent` branch and never reach here; guard
912
+ // explicitly so a future refactor can't let a reaction ts drive a
913
+ // "This approval request has been resolved." edit that would clobber
914
+ // the user's reacted-to message.
915
+ if (hasCallbackData && !isSlackReactionEvent(body)) {
610
916
  // Record seen signal even for stale callbacks — the user still interacted
611
917
  if (sourceChannel === "telegram" || sourceChannel === "slack") {
612
918
  try {
@@ -698,6 +1004,100 @@ export async function handleChannelInbound(
698
1004
  heartbeatService?.resetTimer();
699
1005
  }
700
1006
 
1007
+ // Slack inbound metadata captured for thread-aware persistence. The
1008
+ // gateway forwards `thread_ts` under `sourceMetadata.threadId` and the
1009
+ // message's own ts under `sourceMetadata.messageId`. Persistence turns
1010
+ // this into a `slackMeta` sub-object in the row's metadata column so
1011
+ // the chronological renderer can reconstruct thread structure without
1012
+ // re-fetching from Slack.
1013
+ const slackThreadTs =
1014
+ sourceChannel === "slack" &&
1015
+ typeof sourceMetadata?.threadId === "string"
1016
+ ? sourceMetadata.threadId
1017
+ : undefined;
1018
+ const slackInbound =
1019
+ sourceChannel === "slack"
1020
+ ? {
1021
+ channelId: conversationExternalId,
1022
+ channelTs: sourceMessageId ?? externalMessageId,
1023
+ ...(slackThreadTs ? { threadTs: slackThreadTs } : {}),
1024
+ ...((body.actorDisplayName ?? body.actorUsername)
1025
+ ? {
1026
+ displayName: body.actorDisplayName ?? body.actorUsername!,
1027
+ }
1028
+ : {}),
1029
+ }
1030
+ : undefined;
1031
+
1032
+ // Account identifier threaded into backfill so `resolveConnection()`
1033
+ // can pick the right workspace in multi-account setups. Best-effort:
1034
+ // the gateway forwards `sourceMetadata.account` when it knows which
1035
+ // Slack workspace the event came from; when absent, both helpers
1036
+ // fall back to the default-active connection.
1037
+ const slackAccount =
1038
+ sourceChannel === "slack" &&
1039
+ typeof sourceMetadata?.account === "string" &&
1040
+ sourceMetadata.account.length > 0
1041
+ ? sourceMetadata.account
1042
+ : undefined;
1043
+
1044
+ // ── DM cold-start backfill ──
1045
+ // First time a Slack DM lands in a conversation that has fewer than
1046
+ // SLACK_DM_BACKFILL_WARM_THRESHOLD stored slackMeta messages, fetch a
1047
+ // window of recent history so the agent sees prior context. One-shot:
1048
+ // once persistence warms up past the threshold, subsequent DMs no
1049
+ // longer trigger backfill. Failures are non-fatal — the new message
1050
+ // proceeds without backfilled history.
1051
+ if (sourceChannel === "slack" && sourceChatType === "im") {
1052
+ // Exclude the just-arrived webhook message from the history window —
1053
+ // the normal inbound persistence path writes it separately, so
1054
+ // including it here would produce duplicate user turns. Only pass a
1055
+ // bound when we actually have a Slack ts (`<secs>.<micros>`): the
1056
+ // fallback path writes `externalMessageId` into `channelTs`, but that
1057
+ // identifier is not guaranteed to be a Slack ts, and Slack's
1058
+ // `conversations.history` rejects anything that isn't a ts string.
1059
+ const boundingTs = isSlackTs(sourceMessageId)
1060
+ ? sourceMessageId
1061
+ : undefined;
1062
+ await tryBackfillSlackDmIfCold({
1063
+ conversationId: result.conversationId,
1064
+ channelId: conversationExternalId,
1065
+ account: slackAccount,
1066
+ latestTs: boundingTs,
1067
+ });
1068
+ }
1069
+
1070
+ // ── Thread-ancestor backfill ──
1071
+ // When a Slack reply arrives for a thread the daemon never saw the
1072
+ // parent of, fetch the thread's recent history from Slack and persist
1073
+ // the missing messages so the chronological renderer (PR 18) has the
1074
+ // full conversation. Awaited (mirrors the DM cold-start path above)
1075
+ // so the agent loop dispatched immediately afterwards observes the
1076
+ // backfilled parent — without this, `loadSlackChronologicalMessages`
1077
+ // can race the persist and miss thread context. Backfill is bounded
1078
+ // (parent + ~50 messages) and the agent latency is dominated by the
1079
+ // LLM call, so the added latency is negligible. Failures are
1080
+ // swallowed inside the helper so they never block dispatch.
1081
+ if (slackThreadTs) {
1082
+ await triggerSlackThreadBackfillIfNeeded({
1083
+ conversationId: result.conversationId,
1084
+ channelId: conversationExternalId,
1085
+ threadTs: slackThreadTs,
1086
+ excludeChannelTs: slackInbound?.channelTs,
1087
+ account: slackAccount,
1088
+ });
1089
+ }
1090
+
1091
+ // Wrap non-guardian inbound content in external_content boundaries so
1092
+ // the model can distinguish external channel messages from instructions.
1093
+ const contentForProcessing =
1094
+ trustCtx.trustClass !== "guardian"
1095
+ ? wrapUntrustedContent(trimmedContent, {
1096
+ source: "webhook",
1097
+ sourceDetail: trustCtx.requesterIdentifier,
1098
+ })
1099
+ : trimmedContent;
1100
+
701
1101
  // Fire-and-forget: process the message and deliver the reply in the background.
702
1102
  // The HTTP response returns immediately so the gateway webhook is not blocked.
703
1103
  // The onEvent callback in processMessage registers pending interactions, and
@@ -706,7 +1106,7 @@ export async function handleChannelInbound(
706
1106
  processMessage,
707
1107
  conversationId: result.conversationId,
708
1108
  eventId: result.eventId,
709
- content: trimmedContent,
1109
+ content: contentForProcessing,
710
1110
  attachmentIds: hasAttachments ? attachmentIds : undefined,
711
1111
  sourceChannel,
712
1112
  sourceInterface,
@@ -721,6 +1121,7 @@ export async function handleChannelInbound(
721
1121
  assistantId: canonicalAssistantId,
722
1122
  approvalCopyGenerator,
723
1123
  chatType: sourceChatType,
1124
+ slackInbound,
724
1125
  });
725
1126
  }
726
1127
  }
@@ -731,3 +1132,587 @@ export async function handleChannelInbound(
731
1132
  eventId: result.eventId,
732
1133
  });
733
1134
  }
1135
+
1136
+ /**
1137
+ * Detect a Slack reaction event by inspecting the inbound payload's
1138
+ * `callbackData` prefix. The gateway encodes reactions as a unified
1139
+ * `SlackInboundEvent` with `callbackData` of the form
1140
+ * `reaction:<emoji>` (added) or `reaction_removed:<emoji>` (removed) —
1141
+ * see `gateway/src/slack/normalize.ts`. This helper centralizes that
1142
+ * convention so the daemon can route reactions to a dedicated persistence
1143
+ * branch instead of the agent-response pipeline.
1144
+ */
1145
+ export function isSlackReactionEvent(body: {
1146
+ sourceChannel?: string;
1147
+ callbackData?: string;
1148
+ }): boolean {
1149
+ if (body.sourceChannel !== "slack") return false;
1150
+ const cb = body.callbackData;
1151
+ if (typeof cb !== "string") return false;
1152
+ return cb.startsWith("reaction:") || cb.startsWith("reaction_removed:");
1153
+ }
1154
+
1155
+ /**
1156
+ * Parse a reaction `callbackData` string into its op (added/removed) and
1157
+ * emoji name. Returns `null` when the input is not a reaction prefix or
1158
+ * when the emoji portion is empty.
1159
+ */
1160
+ export function parseSlackReactionCallbackData(
1161
+ callbackData: string,
1162
+ ): { op: "added" | "removed"; emoji: string } | null {
1163
+ let op: "added" | "removed";
1164
+ let emoji: string;
1165
+ if (callbackData.startsWith("reaction_removed:")) {
1166
+ op = "removed";
1167
+ emoji = callbackData.slice("reaction_removed:".length);
1168
+ } else if (callbackData.startsWith("reaction:")) {
1169
+ op = "added";
1170
+ emoji = callbackData.slice("reaction:".length);
1171
+ } else {
1172
+ return null;
1173
+ }
1174
+ if (emoji.length === 0) return null;
1175
+ return { op, emoji };
1176
+ }
1177
+
1178
+ /**
1179
+ * Persist a Slack reaction event as a `messages` row with `slackMeta`
1180
+ * envelope so the renderer can surface it inline in the chronological
1181
+ * transcript. Reactions do not trigger an agent response — the row is
1182
+ * written and the inbound event is linked, but the agent loop is not
1183
+ * dispatched.
1184
+ *
1185
+ * The caller is expected to have run `recordInbound` already so that
1186
+ * deduplication and conversation resolution have happened. Duplicate
1187
+ * inbound events are skipped here to keep persistence idempotent.
1188
+ */
1189
+ async function persistSlackReactionAsMessage(params: {
1190
+ conversationId: string;
1191
+ conversationExternalId: string;
1192
+ eventId: string;
1193
+ callbackData: string;
1194
+ actorDisplayName?: string;
1195
+ threadTs?: string;
1196
+ reactedMessageTs: string;
1197
+ duplicate: boolean;
1198
+ }): Promise<void> {
1199
+ if (params.duplicate) return;
1200
+
1201
+ const parsed = parseSlackReactionCallbackData(params.callbackData);
1202
+ if (!parsed) {
1203
+ log.debug(
1204
+ {
1205
+ conversationId: params.conversationId,
1206
+ callbackData: params.callbackData,
1207
+ },
1208
+ "Skipping reaction persistence: unparseable callbackData",
1209
+ );
1210
+ return;
1211
+ }
1212
+
1213
+ const slackMeta: SlackMessageMetadata = {
1214
+ source: "slack",
1215
+ channelId: params.conversationExternalId,
1216
+ channelTs: params.reactedMessageTs,
1217
+ eventKind: "reaction",
1218
+ ...(params.threadTs ? { threadTs: params.threadTs } : {}),
1219
+ ...(params.actorDisplayName
1220
+ ? { displayName: params.actorDisplayName }
1221
+ : {}),
1222
+ reaction: {
1223
+ emoji: parsed.emoji,
1224
+ targetChannelTs: params.reactedMessageTs,
1225
+ op: parsed.op,
1226
+ ...(params.actorDisplayName
1227
+ ? { actorDisplayName: params.actorDisplayName }
1228
+ : {}),
1229
+ },
1230
+ };
1231
+
1232
+ // Sentinel content — renderers (PR 18) read `slackMeta` to format the
1233
+ // reaction line; the literal text is never displayed to the model.
1234
+ const persisted = await addMessage(
1235
+ params.conversationId,
1236
+ "user",
1237
+ "[reaction]",
1238
+ { slackMeta: writeSlackMetadata(slackMeta) },
1239
+ { skipIndexing: true },
1240
+ );
1241
+ deliveryCrud.linkMessage(params.eventId, persisted.id);
1242
+ deliveryStatus.markProcessed(params.eventId);
1243
+ }
1244
+
1245
+ /**
1246
+ * Threshold of stored Slack-tagged messages below which a conversation is
1247
+ * considered "cold" and eligible for one-shot backfill. The number is
1248
+ * deliberately small but greater than 1 so a single sentinel row (e.g. a
1249
+ * stale reaction) does not disqualify a conversation that has no real
1250
+ * message history yet.
1251
+ */
1252
+ const SLACK_DM_BACKFILL_WARM_THRESHOLD = 3;
1253
+
1254
+ /**
1255
+ * Shape-check for a Slack `ts` value. Slack IDs messages by `<seconds>.<micros>`
1256
+ * strings (e.g. `"1700000000.000100"`). The daemon also stores an
1257
+ * `externalMessageId` derived from the gateway's dedupe key which follows a
1258
+ * different format, so any path that feeds a ts to Slack's API
1259
+ * (`conversations.history`'s `latest`, etc.) must shape-check first — Slack
1260
+ * rejects non-ts arguments with `invalid_arguments`, and passing a malformed
1261
+ * bound silently disables the intended history window.
1262
+ */
1263
+ function isSlackTs(value: string | null | undefined): value is string {
1264
+ return typeof value === "string" && /^\d+\.\d+$/.test(value);
1265
+ }
1266
+
1267
+ /**
1268
+ * Batch size used when pulling candidate rows from SQL. A bare
1269
+ * `LIKE '%"slackMeta"%'` match can include rows whose metadata JSON is
1270
+ * malformed or carries the literal under an unrelated key, so we fetch in
1271
+ * batches and re-validate each candidate with Zod. The threshold is tiny
1272
+ * (see `SLACK_DM_BACKFILL_WARM_THRESHOLD`), so a 10× batch is a trivial
1273
+ * scan while letting a handful of bad rows not starve the count.
1274
+ */
1275
+ const SLACK_DM_CANDIDATE_BATCH_SIZE = SLACK_DM_BACKFILL_WARM_THRESHOLD * 10;
1276
+
1277
+ /**
1278
+ * Absolute cap on candidate rows inspected per webhook to classify a DM as
1279
+ * warm. If this many substring matches have been examined without reaching
1280
+ * the valid-row threshold, treat the conversation as cold — a scan this
1281
+ * deep already dominates the critical-path budget and the cold-start
1282
+ * backfill path is itself idempotent against re-runs.
1283
+ */
1284
+ const SLACK_DM_CANDIDATE_MAX_SCAN = SLACK_DM_BACKFILL_WARM_THRESHOLD * 20;
1285
+
1286
+ /**
1287
+ * Count messages in a conversation whose `metadata` carries a well-formed
1288
+ * `slackMeta` envelope, capped at the warm threshold. SQL prefilters with
1289
+ * `LIKE` + `LIMIT`/`OFFSET` so warm DM conversations never scan the full
1290
+ * table on the webhook critical path, and each candidate is re-validated
1291
+ * through `readSlackMetadata` — a bare substring match would otherwise
1292
+ * wrongly count rows whose metadata is truncated, parses but fails schema
1293
+ * validation, or happens to contain the literal `"slackMeta"` under an
1294
+ * unrelated key. Pulls candidates in batches, continuing until either the
1295
+ * threshold of *valid* rows is reached or the per-call scan cap is hit, so
1296
+ * a cluster of malformed rows at the head of the scan cannot starve the
1297
+ * count and misclassify a warm conversation as cold.
1298
+ */
1299
+ function countSlackMetaMessages(conversationId: string): number {
1300
+ let count = 0;
1301
+ let offset = 0;
1302
+ while (offset < SLACK_DM_CANDIDATE_MAX_SCAN) {
1303
+ const remaining = SLACK_DM_CANDIDATE_MAX_SCAN - offset;
1304
+ const batchLimit = Math.min(SLACK_DM_CANDIDATE_BATCH_SIZE, remaining);
1305
+ const candidates = selectSlackMetaCandidateMetadata(
1306
+ conversationId,
1307
+ batchLimit,
1308
+ offset,
1309
+ );
1310
+ if (candidates.length === 0) return count;
1311
+ for (const raw of candidates) {
1312
+ let parent: Record<string, unknown> | null = null;
1313
+ try {
1314
+ const parsed = JSON.parse(raw) as unknown;
1315
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1316
+ parent = parsed as Record<string, unknown>;
1317
+ }
1318
+ } catch {
1319
+ continue;
1320
+ }
1321
+ if (!parent) continue;
1322
+ const inner = parent.slackMeta;
1323
+ if (typeof inner !== "string") continue;
1324
+ if (readSlackMetadata(inner)) {
1325
+ count++;
1326
+ if (count >= SLACK_DM_BACKFILL_WARM_THRESHOLD) return count;
1327
+ }
1328
+ }
1329
+ if (candidates.length < batchLimit) return count;
1330
+ offset += candidates.length;
1331
+ }
1332
+ return count;
1333
+ }
1334
+
1335
+ /**
1336
+ * Build the set of `slackMeta.channelTs` values already stored on a
1337
+ * conversation. Used by both DM cold-start backfill and thread-ancestor
1338
+ * backfill to dedupe rows so a partial prior backfill (or a single message
1339
+ * that was already persisted via the live ingress path) does not double-write.
1340
+ */
1341
+ function readStoredSlackChannelTs(conversationId: string): Set<string> {
1342
+ const seen = new Set<string>();
1343
+ for (const row of getMessages(conversationId)) {
1344
+ if (!row.metadata) continue;
1345
+ let parent: Record<string, unknown> | null = null;
1346
+ try {
1347
+ const parsed = JSON.parse(row.metadata) as unknown;
1348
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1349
+ parent = parsed as Record<string, unknown>;
1350
+ }
1351
+ } catch {
1352
+ continue;
1353
+ }
1354
+ if (!parent) continue;
1355
+ const raw = parent.slackMeta;
1356
+ if (typeof raw !== "string") continue;
1357
+ const meta = readSlackMetadata(raw);
1358
+ // Only message rows represent stored Slack messages. Reaction rows carry
1359
+ // `channelTs` equal to the target message's ts, so including them would
1360
+ // make a reaction on a thread parent wrongly short-circuit ancestor
1361
+ // backfill (the parent itself may still be unseen).
1362
+ if (meta && meta.eventKind === "message") seen.add(meta.channelTs);
1363
+ }
1364
+ return seen;
1365
+ }
1366
+
1367
+ /**
1368
+ * Persist a single backfilled Slack message as a `messages` row with a
1369
+ * `slackMeta` envelope.
1370
+ *
1371
+ * Shared insertion point for any path that hydrates Slack history lazily
1372
+ * (DM cold-start backfill, thread-ancestor backfill, etc.). Role is derived
1373
+ * from `message.metadata.isBot` — bot-authored rows map to `"assistant"` so
1374
+ * our own prior replies (and any other bot traffic) are not rehydrated as
1375
+ * user turns, which would otherwise corrupt speaker attribution and make
1376
+ * the assistant treat its own outputs as new user input on later turns.
1377
+ * Caller is responsible for dedup checks before invoking; this helper
1378
+ * performs no idempotency check itself.
1379
+ */
1380
+ async function persistBackfilledSlackMessage(params: {
1381
+ conversationId: string;
1382
+ channelId: string;
1383
+ message: ProviderMessage;
1384
+ }): Promise<void> {
1385
+ const { message } = params;
1386
+ const slackMeta: SlackMessageMetadata = {
1387
+ source: "slack",
1388
+ channelId: params.channelId,
1389
+ channelTs: message.id,
1390
+ eventKind: "message",
1391
+ ...(message.threadId ? { threadTs: message.threadId } : {}),
1392
+ ...(message.sender?.name ? { displayName: message.sender.name } : {}),
1393
+ };
1394
+ const role = message.metadata?.isBot === true ? "assistant" : "user";
1395
+ await addMessage(params.conversationId, role, message.text ?? "", {
1396
+ slackMeta: writeSlackMetadata(slackMeta),
1397
+ });
1398
+ }
1399
+
1400
+ /**
1401
+ * In-memory map of in-flight DM cold-start backfills keyed by conversationId.
1402
+ * Concurrent inbound DMs to the same cold conversation share a single
1403
+ * backfill promise instead of each issuing their own Slack history fetch and
1404
+ * write — without this, two near-simultaneous DMs would both observe a cold
1405
+ * count, both fetch the same history window, and both insert duplicate rows
1406
+ * (channelTs lives inside a JSON metadata blob, so the DB has no uniqueness
1407
+ * constraint to fall back on).
1408
+ */
1409
+ const _dmBackfillInFlight = new Map<string, Promise<void>>();
1410
+
1411
+ /**
1412
+ * One-shot DM cold-start backfill. When a Slack DM lands in a conversation
1413
+ * with fewer than `SLACK_DM_BACKFILL_WARM_THRESHOLD` stored Slack-tagged
1414
+ * messages, fetch a window of recent history via `backfillDm` and persist
1415
+ * each returned message with a `slackMeta` envelope. Already-stored
1416
+ * messages (matched by `slackMeta.channelTs`) are skipped to keep the
1417
+ * operation idempotent across retries.
1418
+ *
1419
+ * Failure semantics: any error is logged at WARN and swallowed. The caller
1420
+ * proceeds with only the new message.
1421
+ */
1422
+ async function tryBackfillSlackDmIfCold(params: {
1423
+ conversationId: string;
1424
+ channelId: string;
1425
+ account?: string;
1426
+ latestTs?: string;
1427
+ }): Promise<void> {
1428
+ const existing = _dmBackfillInFlight.get(params.conversationId);
1429
+ if (existing) {
1430
+ await existing;
1431
+ return;
1432
+ }
1433
+ const promise = runBackfillSlackDmIfCold(params).finally(() => {
1434
+ _dmBackfillInFlight.delete(params.conversationId);
1435
+ });
1436
+ _dmBackfillInFlight.set(params.conversationId, promise);
1437
+ await promise;
1438
+ }
1439
+
1440
+ async function runBackfillSlackDmIfCold(params: {
1441
+ conversationId: string;
1442
+ channelId: string;
1443
+ account?: string;
1444
+ latestTs?: string;
1445
+ }): Promise<void> {
1446
+ try {
1447
+ const storedCount = countSlackMetaMessages(params.conversationId);
1448
+ if (storedCount >= SLACK_DM_BACKFILL_WARM_THRESHOLD) {
1449
+ return;
1450
+ }
1451
+
1452
+ // Pass the webhook message's ts as `before` (Slack's `latest`,
1453
+ // exclusive) so history never contains the message that's about to be
1454
+ // persisted by the live inbound path. Without this bound the just-arrived
1455
+ // DM would be written twice — once here and once via normal persistence —
1456
+ // producing duplicate user turns.
1457
+ const fetched = await backfillDm(params.channelId, {
1458
+ limit: 50,
1459
+ account: params.account,
1460
+ before: params.latestTs,
1461
+ });
1462
+ if (fetched.length === 0) {
1463
+ log.debug(
1464
+ { conversationId: params.conversationId, channelId: params.channelId },
1465
+ "DM backfill returned no messages",
1466
+ );
1467
+ return;
1468
+ }
1469
+
1470
+ const seen = readStoredSlackChannelTs(params.conversationId);
1471
+ let written = 0;
1472
+ // Slack's conversation.history returns most-recent first. Reverse so
1473
+ // rows insert in chronological order, giving stable createdAt ordering
1474
+ // and a transcript that reads correctly when the renderer joins on
1475
+ // monotonic createdAt.
1476
+ const ordered = [...fetched].reverse();
1477
+ for (const message of ordered) {
1478
+ if (seen.has(message.id)) continue;
1479
+ try {
1480
+ await persistBackfilledSlackMessage({
1481
+ conversationId: params.conversationId,
1482
+ channelId: params.channelId,
1483
+ message,
1484
+ });
1485
+ seen.add(message.id);
1486
+ written++;
1487
+ } catch (perRowErr) {
1488
+ log.warn(
1489
+ {
1490
+ err: perRowErr,
1491
+ conversationId: params.conversationId,
1492
+ channelId: params.channelId,
1493
+ channelTs: message.id,
1494
+ },
1495
+ "Failed to persist backfilled DM row; continuing",
1496
+ );
1497
+ }
1498
+ }
1499
+
1500
+ log.info(
1501
+ {
1502
+ conversationId: params.conversationId,
1503
+ channelId: params.channelId,
1504
+ fetched: fetched.length,
1505
+ written,
1506
+ },
1507
+ "DM cold-start backfill complete",
1508
+ );
1509
+ } catch (err) {
1510
+ // `channel_not_found` almost always means the resolved connection is
1511
+ // pointing at the wrong Slack workspace (a real config bug), so log it
1512
+ // at ERROR to match backfill's rethrow contract. Other failures
1513
+ // (timeout, auth, ratelimited, …) stay at WARN — they're expected
1514
+ // transient blips and the caller proceeds without backfilled history.
1515
+ const channelNotFound =
1516
+ err instanceof Error && /channel_not_found/i.test(err.message);
1517
+ const payload = {
1518
+ err,
1519
+ conversationId: params.conversationId,
1520
+ channelId: params.channelId,
1521
+ account: params.account,
1522
+ };
1523
+ if (channelNotFound) {
1524
+ log.error(
1525
+ payload,
1526
+ "DM cold-start backfill hit channel_not_found — connection likely points at the wrong Slack workspace",
1527
+ );
1528
+ } else {
1529
+ log.warn(
1530
+ payload,
1531
+ "DM cold-start backfill failed; proceeding without history",
1532
+ );
1533
+ }
1534
+ }
1535
+ }
1536
+
1537
+ // ---------------------------------------------------------------------------
1538
+ // Slack thread backfill on gap detection
1539
+ // ---------------------------------------------------------------------------
1540
+
1541
+ /**
1542
+ * In-memory TTL cache keyed by `<conversationId>:<threadTs>`. Tracks recent
1543
+ * thread-backfill triggers so a burst of replies inside the same Slack
1544
+ * thread (e.g. a guardian rapidly typing several lines) does not re-fetch
1545
+ * the same parent messages from Slack repeatedly. Entries naturally fall
1546
+ * out after the TTL — if the thread is still active later, a fresh
1547
+ * backfill becomes a cheap "are the parents already stored?" DB lookup
1548
+ * that short-circuits before the Slack API is touched.
1549
+ *
1550
+ * Exported only for tests; production callers should use
1551
+ * {@link triggerSlackThreadBackfillIfNeeded}.
1552
+ */
1553
+ export const _backfillTriggerCache = new Map<string, number>();
1554
+
1555
+ const BACKFILL_TRIGGER_TTL_MS = 10 * 60 * 1000; // 10 minutes
1556
+ const BACKFILL_TRIGGER_CACHE_MAX = 1_000;
1557
+
1558
+ function pruneBackfillCacheIfNeeded(): void {
1559
+ if (_backfillTriggerCache.size < BACKFILL_TRIGGER_CACHE_MAX) return;
1560
+ const now = Date.now();
1561
+ for (const [key, ts] of _backfillTriggerCache) {
1562
+ if (now - ts >= BACKFILL_TRIGGER_TTL_MS) {
1563
+ _backfillTriggerCache.delete(key);
1564
+ }
1565
+ }
1566
+ // If still over the cap after TTL sweep, drop the oldest entries (LRU-ish).
1567
+ if (_backfillTriggerCache.size >= BACKFILL_TRIGGER_CACHE_MAX) {
1568
+ const entries = [..._backfillTriggerCache.entries()].sort(
1569
+ (a, b) => a[1] - b[1],
1570
+ );
1571
+ const toRemove = entries.slice(
1572
+ 0,
1573
+ entries.length - BACKFILL_TRIGGER_CACHE_MAX + 1,
1574
+ );
1575
+ for (const [key] of toRemove) {
1576
+ _backfillTriggerCache.delete(key);
1577
+ }
1578
+ }
1579
+ }
1580
+
1581
+ function isBackfillRecentlyTriggered(cacheKey: string): boolean {
1582
+ const ts = _backfillTriggerCache.get(cacheKey);
1583
+ if (ts === undefined) return false;
1584
+ if (Date.now() - ts >= BACKFILL_TRIGGER_TTL_MS) {
1585
+ _backfillTriggerCache.delete(cacheKey);
1586
+ return false;
1587
+ }
1588
+ return true;
1589
+ }
1590
+
1591
+ /**
1592
+ * Lazily backfill missing Slack thread ancestors for an inbound thread reply.
1593
+ *
1594
+ * When a reply arrives for a thread the daemon has never seen (e.g. the bot
1595
+ * was just added to the channel, or the parent message pre-dates the
1596
+ * conversation), the daemon fetches the thread's recent history via
1597
+ * {@link backfillThread}, persists each unseen message as a `messages` row
1598
+ * with a `slackMeta` envelope, and skips duplicates whose `ts` already
1599
+ * appears in the conversation.
1600
+ *
1601
+ * Behavior contracts:
1602
+ * - **No-op when the parent is already stored.** Looks up the conversation's
1603
+ * messages and short-circuits if any row has `slackMeta.channelTs ===
1604
+ * threadTs`. This keeps subsequent replies in the same thread cheap.
1605
+ * - **TTL idempotency cache.** A 10-minute in-memory cache prevents bursts
1606
+ * of replies in the same thread from re-running the DB lookup or the
1607
+ * Slack API call.
1608
+ * - **Failure-tolerant.** Any error (Slack API failure, DB error, malformed
1609
+ * payload) is logged at `warn` and swallowed — the inbound turn must
1610
+ * never block on backfill.
1611
+ */
1612
+ export async function triggerSlackThreadBackfillIfNeeded(params: {
1613
+ conversationId: string;
1614
+ channelId: string;
1615
+ threadTs: string;
1616
+ /**
1617
+ * The inbound message's own `channelTs`. Pre-seeded into the dedup set so
1618
+ * this helper does not re-persist the just-received message when Slack's
1619
+ * `conversations.replies` returns it in the thread window. Necessary
1620
+ * because thread backfill runs concurrently with
1621
+ * `processChannelMessageInBackground`, so the inbound row may not yet be
1622
+ * in the DB when `readStoredSlackChannelTs` snapshots the conversation.
1623
+ */
1624
+ excludeChannelTs?: string;
1625
+ /**
1626
+ * OAuth account identifier used to disambiguate which Slack workspace the
1627
+ * backfill should read from in multi-account setups. Passed through to
1628
+ * `backfillThread` → `resolveConnection`. Best-effort: if omitted, the
1629
+ * resolver falls back to the default-active connection.
1630
+ */
1631
+ account?: string;
1632
+ }): Promise<void> {
1633
+ const { conversationId, channelId, threadTs, excludeChannelTs, account } =
1634
+ params;
1635
+ const cacheKey = `${conversationId}:${threadTs}`;
1636
+
1637
+ try {
1638
+ if (isBackfillRecentlyTriggered(cacheKey)) {
1639
+ return;
1640
+ }
1641
+
1642
+ const storedChannelTs = readStoredSlackChannelTs(conversationId);
1643
+ if (excludeChannelTs) storedChannelTs.add(excludeChannelTs);
1644
+ if (storedChannelTs.has(threadTs)) {
1645
+ // Parent is already in the conversation; mark the cache so a burst of
1646
+ // replies in this thread does not redo the DB scan for each one.
1647
+ _backfillTriggerCache.set(cacheKey, Date.now());
1648
+ pruneBackfillCacheIfNeeded();
1649
+ return;
1650
+ }
1651
+
1652
+ // Mark the trigger before issuing the network call. Doing this first
1653
+ // means a second concurrent reply in the same thread short-circuits
1654
+ // immediately even while the first call is still awaiting the Slack
1655
+ // API. The cost is a slightly larger window where a transient Slack
1656
+ // failure suppresses a retry, which the next reply outside the TTL
1657
+ // (or a daemon restart) will re-attempt anyway.
1658
+ _backfillTriggerCache.set(cacheKey, Date.now());
1659
+ pruneBackfillCacheIfNeeded();
1660
+
1661
+ const fetched = await backfillThread(channelId, threadTs, { account });
1662
+ if (fetched.length === 0) {
1663
+ log.debug(
1664
+ { conversationId, channelId, threadTs },
1665
+ "Slack thread backfill returned no messages",
1666
+ );
1667
+ return;
1668
+ }
1669
+
1670
+ let persisted = 0;
1671
+ for (const message of fetched) {
1672
+ if (!message.id) continue;
1673
+ if (storedChannelTs.has(message.id)) continue;
1674
+ try {
1675
+ await persistBackfilledSlackMessage({
1676
+ conversationId,
1677
+ channelId,
1678
+ message,
1679
+ });
1680
+ storedChannelTs.add(message.id);
1681
+ persisted++;
1682
+ } catch (err) {
1683
+ log.warn(
1684
+ { err, conversationId, channelId, threadTs, channelTs: message.id },
1685
+ "Failed to persist backfilled Slack thread message",
1686
+ );
1687
+ }
1688
+ }
1689
+
1690
+ log.info(
1691
+ {
1692
+ conversationId,
1693
+ channelId,
1694
+ threadTs,
1695
+ persisted,
1696
+ fetched: fetched.length,
1697
+ },
1698
+ "Slack thread backfill persisted ancestor messages",
1699
+ );
1700
+ } catch (err) {
1701
+ // `channel_not_found` almost always means the resolved connection is
1702
+ // pointing at the wrong Slack workspace (a real config bug), so log it
1703
+ // at ERROR to match backfill's rethrow contract. Other failures
1704
+ // (timeout, auth, ratelimited, …) stay at WARN — they're expected
1705
+ // transient blips and dispatch proceeds without the ancestors.
1706
+ const channelNotFound =
1707
+ err instanceof Error && /channel_not_found/i.test(err.message);
1708
+ const payload = { err, conversationId, channelId, threadTs, account };
1709
+ if (channelNotFound) {
1710
+ log.error(
1711
+ payload,
1712
+ "Slack thread backfill hit channel_not_found — connection likely points at the wrong Slack workspace",
1713
+ );
1714
+ } else {
1715
+ log.warn(payload, "Slack thread backfill failed; proceeding without it");
1716
+ }
1717
+ }
1718
+ }