@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Reproduction & regression tests for the thinking-block provider-switch bug.
3
+ *
4
+ * Phase 1 (real API): proves that replaying a thinking block with a tampered
5
+ * signature causes Anthropic to reject the request with a 400.
6
+ * Phase 2 (mocked): verifies the send-time filtering fix strips historical
7
+ * thinking blocks while preserving active tool-use continuation blocks.
8
+ */
9
+
10
+ import { describe, expect, test } from "bun:test";
11
+
12
+ import Anthropic from "@anthropic-ai/sdk";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Phase 1 — Real API reproduction
16
+ // ---------------------------------------------------------------------------
17
+
18
+ const apiKey = process.env.ANTHROPIC_API_KEY;
19
+
20
+ describe.skipIf(!apiKey)(
21
+ "Thinking block replay — real API reproduction",
22
+ () => {
23
+ test("Anthropic rejects a tampered thinking signature with 400", async () => {
24
+ const client = new Anthropic({ apiKey });
25
+
26
+ // Step 1: Get a real thinking block from the API
27
+ const initialResponse = await client.messages.create({
28
+ model: "claude-sonnet-4-6",
29
+ max_tokens: 4096,
30
+ thinking: { type: "enabled", budget_tokens: 1024 },
31
+ messages: [{ role: "user", content: "What is 2 + 2?" }],
32
+ });
33
+
34
+ const thinkingBlock = initialResponse.content.find(
35
+ (b) => b.type === "thinking",
36
+ ) as Anthropic.ThinkingBlock | undefined;
37
+
38
+ expect(thinkingBlock).toBeDefined();
39
+ expect(thinkingBlock!.signature).toBeTruthy();
40
+
41
+ // Step 2: Tamper with the signature to simulate a stale/cross-provider block
42
+ const tamperedSignature =
43
+ thinkingBlock!.signature.slice(0, -4) + "XXXX";
44
+
45
+ // Step 3: Replay the tampered block as historical context
46
+ const historicalAssistantContent: Anthropic.ContentBlockParam[] = [
47
+ {
48
+ type: "thinking",
49
+ thinking: thinkingBlock!.thinking,
50
+ signature: tamperedSignature,
51
+ },
52
+ { type: "text", text: "4" },
53
+ ];
54
+
55
+ // Step 4: Confirm Anthropic rejects with 400
56
+ try {
57
+ await client.messages.create({
58
+ model: "claude-sonnet-4-6",
59
+ max_tokens: 4096,
60
+ thinking: { type: "enabled", budget_tokens: 1024 },
61
+ messages: [
62
+ { role: "user", content: "What is 2 + 2?" },
63
+ { role: "assistant", content: historicalAssistantContent },
64
+ { role: "user", content: "And what is 3 + 3?" },
65
+ ],
66
+ });
67
+ expect.unreachable("API should have rejected the tampered signature");
68
+ } catch (err: unknown) {
69
+ const apiErr = err as { status?: number; message?: string };
70
+ expect(apiErr.status).toBe(400);
71
+ expect(apiErr.message).toContain("signature");
72
+ }
73
+ }, 30_000);
74
+
75
+ test("Anthropic accepts the request when thinking blocks are stripped from historical turns", async () => {
76
+ const client = new Anthropic({ apiKey });
77
+
78
+ // Step 1: Get a real thinking block
79
+ const initialResponse = await client.messages.create({
80
+ model: "claude-sonnet-4-6",
81
+ max_tokens: 4096,
82
+ thinking: { type: "enabled", budget_tokens: 1024 },
83
+ messages: [{ role: "user", content: "What is 2 + 2?" }],
84
+ });
85
+
86
+ const thinkingBlock = initialResponse.content.find(
87
+ (b) => b.type === "thinking",
88
+ ) as Anthropic.ThinkingBlock | undefined;
89
+
90
+ expect(thinkingBlock).toBeDefined();
91
+
92
+ // Step 2: Build history WITHOUT thinking blocks (the fix)
93
+ const cleanAssistantContent: Anthropic.ContentBlockParam[] = [
94
+ { type: "text", text: "4" },
95
+ ];
96
+
97
+ // Step 3: Confirm request succeeds
98
+ const followUp = await client.messages.create({
99
+ model: "claude-sonnet-4-6",
100
+ max_tokens: 4096,
101
+ thinking: { type: "enabled", budget_tokens: 1024 },
102
+ messages: [
103
+ { role: "user", content: "What is 2 + 2?" },
104
+ { role: "assistant", content: cleanAssistantContent },
105
+ { role: "user", content: "And what is 3 + 3?" },
106
+ ],
107
+ });
108
+
109
+ expect(followUp.content).toBeDefined();
110
+ expect(followUp.stop_reason).toBe("end_turn");
111
+ }, 30_000);
112
+ },
113
+ );
@@ -77,6 +77,14 @@ mock.module("../messaging/providers/slack/adapter.js", () => ({
77
77
  _account: string | undefined,
78
78
  fn: (token: string) => Promise<unknown>,
79
79
  ) => fn("test-slack-token"),
80
+ resolveSlackBotUserId: async (
81
+ _account: string | undefined,
82
+ botId: string,
83
+ ) => {
84
+ if (botId === "B_ASSISTANT") return "U_BOT";
85
+ if (botId === "B_OTHER") return "U_OTHER_BOT";
86
+ return null;
87
+ },
80
88
  }));
81
89
 
82
90
  // ---------------------------------------------------------------------------
@@ -85,6 +93,11 @@ mock.module("../messaging/providers/slack/adapter.js", () => ({
85
93
 
86
94
  import { v4 as uuid } from "uuid";
87
95
 
96
+ import {
97
+ loadRawConfig,
98
+ saveRawConfig,
99
+ setNestedValue,
100
+ } from "../config/loader.js";
88
101
  import { upsertContactChannel } from "../contacts/contacts-write.js";
89
102
  import {
90
103
  type ChannelCapabilities,
@@ -97,6 +110,7 @@ import { recordInbound } from "../memory/delivery-crud.js";
97
110
  import type { Message as MessagingMessage } from "../messaging/provider-types.js";
98
111
  import * as slackBackfill from "../messaging/providers/slack/backfill.js";
99
112
  import {
113
+ buildSlackTimezoneMetadata,
100
114
  readSlackMetadata,
101
115
  writeSlackMetadata,
102
116
  } from "../messaging/providers/slack/message-metadata.js";
@@ -162,6 +176,20 @@ function resetState(): void {
162
176
  backfillDmMock.mockImplementation(async () => []);
163
177
  downloadSlackFileMock.mockReset();
164
178
  downloadSlackFileMock.mockResolvedValue(null);
179
+ setConfiguredSlackBotUserId("U_BOT");
180
+ setConfiguredUserTimezone(undefined);
181
+ }
182
+
183
+ function setConfiguredSlackBotUserId(botUserId: string): void {
184
+ const raw = loadRawConfig();
185
+ setNestedValue(raw, "slack.botUserId", botUserId);
186
+ saveRawConfig(raw);
187
+ }
188
+
189
+ function setConfiguredUserTimezone(userTimezone: string | undefined): void {
190
+ const raw = loadRawConfig();
191
+ setNestedValue(raw, "ui.userTimezone", userTimezone);
192
+ saveRawConfig(raw);
165
193
  }
166
194
 
167
195
  let convCounter = 0;
@@ -269,6 +297,12 @@ interface PersistedRow {
269
297
  threadTs: string | undefined;
270
298
  displayName: string | undefined;
271
299
  actorExternalUserId: string | undefined;
300
+ actorTimezone: string | undefined;
301
+ actorTimezoneLabel: string | undefined;
302
+ actorTimezoneOffsetSeconds: number | undefined;
303
+ timestampTimezone: string | undefined;
304
+ timestampTimezoneLabel: string | undefined;
305
+ speakerTimezoneLabel: string | undefined;
272
306
  slackFiles: Array<{ name: string; mimetype?: string }> | undefined;
273
307
  provenanceTrustClass: string | undefined;
274
308
  provenanceSourceChannel: string | undefined;
@@ -296,6 +330,12 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
296
330
  threadTs: undefined,
297
331
  displayName: undefined,
298
332
  actorExternalUserId: undefined,
333
+ actorTimezone: undefined,
334
+ actorTimezoneLabel: undefined,
335
+ actorTimezoneOffsetSeconds: undefined,
336
+ timestampTimezone: undefined,
337
+ timestampTimezoneLabel: undefined,
338
+ speakerTimezoneLabel: undefined,
299
339
  slackFiles: undefined,
300
340
  provenanceTrustClass: undefined,
301
341
  provenanceSourceChannel: undefined,
@@ -336,6 +376,12 @@ function readPersistedSlackRows(conversationId: string): PersistedRow[] {
336
376
  threadTs: slackMeta?.threadTs,
337
377
  displayName: slackMeta?.displayName,
338
378
  actorExternalUserId: slackMeta?.actorExternalUserId,
379
+ actorTimezone: slackMeta?.actorTimezone,
380
+ actorTimezoneLabel: slackMeta?.actorTimezoneLabel,
381
+ actorTimezoneOffsetSeconds: slackMeta?.actorTimezoneOffsetSeconds,
382
+ timestampTimezone: slackMeta?.timestampTimezone,
383
+ timestampTimezoneLabel: slackMeta?.timestampTimezoneLabel,
384
+ speakerTimezoneLabel: slackMeta?.speakerTimezoneLabel,
339
385
  slackFiles: slackMeta?.slackFiles?.map((file) => ({
340
386
  name: file.name,
341
387
  ...(file.mimetype ? { mimetype: file.mimetype } : {}),
@@ -978,7 +1024,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
978
1024
  expect(contextImage).toBeDefined();
979
1025
  });
980
1026
 
981
- test("backfilled bot Slack image files are persisted as user image history", async () => {
1027
+ test("backfilled assistant Slack image files are persisted as assistant image history", async () => {
982
1028
  const conv = createTestConversation();
983
1029
 
984
1030
  seedSlackRow(conv.id, "1234.0", undefined, "parent already here");
@@ -992,7 +1038,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
992
1038
  id: "1234.1",
993
1039
  text: "bot posted a diagram",
994
1040
  threadId: "1234.0",
995
- sender: { id: "B_IMAGE", name: "Build Bot" },
1041
+ sender: { id: "U_BOT", name: "Douglas" },
996
1042
  metadata: {
997
1043
  isBot: true,
998
1044
  slackFiles: [
@@ -1020,7 +1066,7 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
1020
1066
  row.content.includes("bot posted a diagram"),
1021
1067
  );
1022
1068
  expect(botRow).toBeDefined();
1023
- expect(botRow?.role).toBe("user");
1069
+ expect(botRow?.role).toBe("assistant");
1024
1070
  const blocks = JSON.parse(botRow!.content) as Message["content"];
1025
1071
  const textBlock = blocks.find(
1026
1072
  (block): block is Extract<Message["content"][number], { type: "text" }> =>
@@ -1042,6 +1088,27 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
1042
1088
  });
1043
1089
 
1044
1090
  expect(context).not.toBeNull();
1091
+ const contextBotMessage = context!.messages.find(
1092
+ (message) =>
1093
+ message.role === "assistant" &&
1094
+ message.content.some(
1095
+ (block) =>
1096
+ block.type === "text" &&
1097
+ block.text.includes("bot posted a diagram"),
1098
+ ),
1099
+ );
1100
+ expect(contextBotMessage).toBeDefined();
1101
+ expect(
1102
+ contextBotMessage!.content
1103
+ .filter(
1104
+ (
1105
+ block,
1106
+ ): block is Extract<Message["content"][number], { type: "text" }> =>
1107
+ block.type === "text",
1108
+ )
1109
+ .map((block) => block.text)
1110
+ .join("\n"),
1111
+ ).not.toContain("<external_content");
1045
1112
  const contextImage = context!.messages
1046
1113
  .flatMap((message) => message.content)
1047
1114
  .find((block) => block.type === "image");
@@ -1082,6 +1149,62 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
1082
1149
  expect(persisted.provenanceRequesterIdentifier).toBe("U_ANITA");
1083
1150
  });
1084
1151
 
1152
+ test("backfilled Slack timezone metadata derives timestamp and speaker fields", async () => {
1153
+ const conv = createTestConversation();
1154
+ setConfiguredUserTimezone("America/Denver");
1155
+
1156
+ backfillThreadMock.mockImplementation(async () => [
1157
+ makeBackfillMessage({
1158
+ id: "1234.0",
1159
+ text: "non-guardian context",
1160
+ threadId: undefined,
1161
+ sender: { id: "U_ANITA", name: "Anita" },
1162
+ metadata: {
1163
+ actorTimezone: "America/New_York",
1164
+ actorTimezoneLabel: "ET",
1165
+ actorTimezoneOffsetSeconds: -18000,
1166
+ },
1167
+ }),
1168
+ makeBackfillMessage({
1169
+ id: "1234.1",
1170
+ text: "trusted context",
1171
+ threadId: "1234.0",
1172
+ sender: { id: "U_GUARDIAN", name: "Guardian User" },
1173
+ metadata: {
1174
+ actorTimezone: "America/Denver",
1175
+ actorTimezoneLabel: "MT",
1176
+ actorTimezoneOffsetSeconds: -25200,
1177
+ },
1178
+ }),
1179
+ ]);
1180
+
1181
+ await triggerSlackThreadBackfillIfNeeded({
1182
+ conversationId: conv.id,
1183
+ channelId: SLACK_CHANNEL_ID,
1184
+ threadTs: "1234.0",
1185
+ guardianExternalUserId: "U_GUARDIAN",
1186
+ });
1187
+
1188
+ const persisted = readPersistedSlackRows(conv.id);
1189
+ const nonGuardian = persisted.find((p) => p.channelTs === "1234.0");
1190
+ expect(nonGuardian).toBeDefined();
1191
+ expect(nonGuardian?.actorTimezone).toBe("America/New_York");
1192
+ expect(nonGuardian?.actorTimezoneLabel).toBe("ET");
1193
+ expect(nonGuardian?.actorTimezoneOffsetSeconds).toBe(-18000);
1194
+ expect(nonGuardian?.timestampTimezone).toBe("America/Denver");
1195
+ expect(nonGuardian?.timestampTimezoneLabel).toBe("MT");
1196
+ expect(nonGuardian?.speakerTimezoneLabel).toBe("ET");
1197
+
1198
+ const guardian = persisted.find((p) => p.channelTs === "1234.1");
1199
+ expect(guardian).toBeDefined();
1200
+ expect(guardian?.actorTimezone).toBe("America/Denver");
1201
+ expect(guardian?.actorTimezoneLabel).toBe("MT");
1202
+ expect(guardian?.actorTimezoneOffsetSeconds).toBe(-25200);
1203
+ expect(guardian?.timestampTimezone).toBe("America/Denver");
1204
+ expect(guardian?.timestampTimezoneLabel).toBe("MT");
1205
+ expect(guardian?.speakerTimezoneLabel).toBeUndefined();
1206
+ });
1207
+
1085
1208
  test("backfilled guardian-authored text is persisted raw with guardian provenance", async () => {
1086
1209
  const conv = createTestConversation();
1087
1210
 
@@ -1115,12 +1238,13 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
1115
1238
  expect(persisted.provenanceRequesterIdentifier).toBe("U_GUARDIAN");
1116
1239
  });
1117
1240
 
1118
- test("backfilled bot-authored text is persisted raw as user history", async () => {
1241
+ test("backfilled assistant-authored text is persisted raw as assistant history", async () => {
1119
1242
  const conv = createTestConversation();
1243
+ setConfiguredUserTimezone("America/Denver");
1120
1244
 
1121
1245
  backfillThreadMock.mockImplementation(async () => [
1122
1246
  makeBackfillMessage({
1123
- id: "1234.0",
1247
+ id: "1772681880.000000",
1124
1248
  text: "earlier assistant reply",
1125
1249
  threadId: undefined,
1126
1250
  sender: { id: "U_BOT", name: "Douglas" },
@@ -1131,20 +1255,150 @@ describe("triggerSlackThreadBackfillIfNeeded — gap detection and persistence",
1131
1255
  await triggerSlackThreadBackfillIfNeeded({
1132
1256
  conversationId: conv.id,
1133
1257
  channelId: SLACK_CHANNEL_ID,
1134
- threadTs: "1234.0",
1258
+ threadTs: "1772681880.000000",
1135
1259
  });
1136
1260
 
1137
1261
  const [persisted] = readPersistedSlackRows(conv.id).filter(
1138
- (p) => p.channelTs === "1234.0",
1262
+ (p) => p.channelTs === "1772681880.000000",
1139
1263
  );
1140
1264
  expect(persisted).toBeDefined();
1141
- expect(persisted.role).toBe("user");
1265
+ expect(persisted.role).toBe("assistant");
1142
1266
  expect(persisted.rawContent).toBe("earlier assistant reply");
1143
1267
  expect(persisted.rawContent).not.toContain("<external_content");
1144
1268
  expect(persisted.actorExternalUserId).toBe("U_BOT");
1269
+ expect(persisted.timestampTimezone).toBe("America/Denver");
1270
+ expect(persisted.timestampTimezoneLabel).toBe("MT");
1271
+ expect(persisted.speakerTimezoneLabel).toBeUndefined();
1145
1272
  expect(persisted.provenanceTrustClass).toBe("unknown");
1146
1273
  expect(persisted.provenanceSourceChannel).toBe("slack");
1147
1274
  expect(persisted.provenanceRequesterIdentifier).toBe("U_BOT");
1275
+
1276
+ const context = loadSlackChronologicalContext(conv.id, SLACK_CHANNEL_CAPS, {
1277
+ loader: readMessageRowsByConversation,
1278
+ trustClass: "guardian",
1279
+ });
1280
+ expect(context).not.toBeNull();
1281
+ expect(context!.messages).toEqual([
1282
+ {
1283
+ role: "assistant",
1284
+ content: [
1285
+ {
1286
+ type: "text",
1287
+ text: "[mar 4 2026 8:38 PM MT assistant] earlier assistant reply",
1288
+ },
1289
+ ],
1290
+ },
1291
+ ]);
1292
+ });
1293
+
1294
+ test("backfilled bot-id-only assistant text resolves to assistant history", async () => {
1295
+ const conv = createTestConversation();
1296
+
1297
+ backfillThreadMock.mockImplementation(async () => [
1298
+ makeBackfillMessage({
1299
+ id: "1234.0",
1300
+ text: "earlier assistant reply from bot id",
1301
+ threadId: undefined,
1302
+ sender: { id: "B_ASSISTANT", name: "Douglas" },
1303
+ metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
1304
+ }),
1305
+ ]);
1306
+
1307
+ await triggerSlackThreadBackfillIfNeeded({
1308
+ conversationId: conv.id,
1309
+ channelId: SLACK_CHANNEL_ID,
1310
+ threadTs: "1234.0",
1311
+ });
1312
+
1313
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1314
+ (p) => p.channelTs === "1234.0",
1315
+ );
1316
+ expect(persisted).toBeDefined();
1317
+ expect(persisted.role).toBe("assistant");
1318
+ expect(persisted.rawContent).toBe("earlier assistant reply from bot id");
1319
+ expect(persisted.actorExternalUserId).toBe("B_ASSISTANT");
1320
+ expect(persisted.provenanceRequesterIdentifier).toBe("B_ASSISTANT");
1321
+ });
1322
+
1323
+ test("backfilled third-party bot-authored text stays user history", async () => {
1324
+ const conv = createTestConversation();
1325
+
1326
+ backfillThreadMock.mockImplementation(async () => [
1327
+ makeBackfillMessage({
1328
+ id: "1234.0",
1329
+ text: "deployment bot status update",
1330
+ threadId: undefined,
1331
+ sender: { id: "U_OTHER_BOT", name: "Deploy Bot" },
1332
+ metadata: { isBot: true, slackBotId: "B_OTHER" },
1333
+ }),
1334
+ ]);
1335
+
1336
+ await triggerSlackThreadBackfillIfNeeded({
1337
+ conversationId: conv.id,
1338
+ channelId: SLACK_CHANNEL_ID,
1339
+ threadTs: "1234.0",
1340
+ });
1341
+
1342
+ const [persisted] = readPersistedSlackRows(conv.id).filter(
1343
+ (p) => p.channelTs === "1234.0",
1344
+ );
1345
+ expect(persisted).toBeDefined();
1346
+ expect(persisted.role).toBe("user");
1347
+ expect(persisted.rawContent).toBe("deployment bot status update");
1348
+ expect(persisted.actorExternalUserId).toBe("U_OTHER_BOT");
1349
+ expect(persisted.provenanceTrustClass).toBe("unknown");
1350
+ expect(persisted.provenanceSourceChannel).toBe("slack");
1351
+ expect(persisted.provenanceRequesterIdentifier).toBe("U_OTHER_BOT");
1352
+ });
1353
+
1354
+ test("skips Slack assistant new-thread placeholder during backfill", async () => {
1355
+ const conv = createTestConversation();
1356
+
1357
+ backfillThreadMock.mockImplementation(async () => [
1358
+ makeBackfillMessage({
1359
+ id: "1234.0",
1360
+ text: "New Assistant Thread",
1361
+ threadId: undefined,
1362
+ sender: { id: "B_ASSISTANT", name: "Ada" },
1363
+ metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
1364
+ }),
1365
+ makeBackfillMessage({
1366
+ id: "1234.1",
1367
+ text: "real bot context",
1368
+ threadId: "1234.0",
1369
+ sender: { id: "B_ASSISTANT", name: "Ada" },
1370
+ metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
1371
+ }),
1372
+ makeBackfillMessage({
1373
+ id: "1234.2",
1374
+ text: "New Assistant Thread",
1375
+ threadId: undefined,
1376
+ sender: { id: "B_OTHER", name: "Build Bot" },
1377
+ metadata: { isBot: true, slackBotId: "B_OTHER" },
1378
+ }),
1379
+ ]);
1380
+
1381
+ const result = await triggerSlackThreadBackfillIfNeeded({
1382
+ conversationId: conv.id,
1383
+ channelId: SLACK_CHANNEL_ID,
1384
+ threadTs: "1234.0",
1385
+ });
1386
+
1387
+ expect(result.fetched).toBe(3);
1388
+ expect(result.persisted).toBe(2);
1389
+ const rows = readPersistedSlackRows(conv.id);
1390
+ expect(rows.map((row) => row.rawContent).sort()).toEqual([
1391
+ "New Assistant Thread",
1392
+ "real bot context",
1393
+ ]);
1394
+ const assistantRow = rows.find(
1395
+ (row) => row.rawContent === "real bot context",
1396
+ );
1397
+ expect(assistantRow?.role).toBe("assistant");
1398
+ expect(assistantRow?.actorExternalUserId).toBe("B_ASSISTANT");
1399
+ expect(rows.some((row) => row.actorExternalUserId === "B_OTHER")).toBe(
1400
+ true,
1401
+ );
1148
1402
  });
1149
1403
 
1150
1404
  test("backfilled non-bot message with empty text is persisted unwrapped", async () => {
@@ -1775,13 +2029,25 @@ function buildSlackDmRequest(
1775
2029
 
1776
2030
  interface SlackInboundProcessOptions {
1777
2031
  displayContent?: string;
1778
- slackRuntimeContextNotice?: string;
2032
+ transport?: {
2033
+ channelId?: string;
2034
+ hints?: string[];
2035
+ uxBrief?: string;
2036
+ chatType?: string;
2037
+ clientTimezone?: string;
2038
+ };
1779
2039
  slackInbound?: {
1780
2040
  channelId: string;
1781
2041
  channelTs: string;
1782
2042
  threadTs?: string;
1783
2043
  displayName?: string;
1784
2044
  actorExternalUserId?: string;
2045
+ actorTimezone?: string;
2046
+ actorTimezoneLabel?: string;
2047
+ actorTimezoneOffsetSeconds?: number;
2048
+ timestampTimezone?: string;
2049
+ timestampTimezoneLabel?: string;
2050
+ speakerTimezoneLabel?: string;
1785
2051
  };
1786
2052
  }
1787
2053
 
@@ -1808,6 +2074,15 @@ function persistSlackInboundFromProcessMessage(
1808
2074
  ...(slackInbound.actorExternalUserId
1809
2075
  ? { actorExternalUserId: slackInbound.actorExternalUserId }
1810
2076
  : {}),
2077
+ ...buildSlackTimezoneMetadata({
2078
+ actorTimezone: slackInbound.actorTimezone,
2079
+ actorTimezoneLabel: slackInbound.actorTimezoneLabel,
2080
+ actorTimezoneOffsetSeconds:
2081
+ slackInbound.actorTimezoneOffsetSeconds,
2082
+ timestampTimezone: slackInbound.timestampTimezone,
2083
+ timestampTimezoneLabel: slackInbound.timestampTimezoneLabel,
2084
+ speakerTimezoneLabel: slackInbound.speakerTimezoneLabel,
2085
+ }),
1811
2086
  eventKind: "message",
1812
2087
  }),
1813
2088
  }
@@ -1934,18 +2209,15 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
1934
2209
  ]);
1935
2210
 
1936
2211
  let capturedHints: string[] | undefined;
1937
- let capturedSlackNotice: string | undefined;
1938
2212
  const processMessage = async (
1939
2213
  _conversationId: string,
1940
2214
  _content: string,
1941
2215
  _attachmentIds?: string[],
1942
2216
  options?: {
1943
2217
  transport?: { hints?: string[] };
1944
- slackRuntimeContextNotice?: string;
1945
2218
  },
1946
2219
  ): Promise<{ messageId: string }> => {
1947
2220
  capturedHints = options?.transport?.hints;
1948
- capturedSlackNotice = options?.slackRuntimeContextNotice;
1949
2221
  return { messageId: "agent-result-id" };
1950
2222
  };
1951
2223
  setAdapterProcessMessage(processMessage);
@@ -1992,16 +2264,7 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
1992
2264
  expect(channelTimestamps.has("1234.0")).toBe(true);
1993
2265
  expect(channelTimestamps.has("1234.1")).toBe(true);
1994
2266
 
1995
- expect(
1996
- capturedHints?.some((hint) => hint.includes("joined an existing thread")),
1997
- ).not.toBe(true);
1998
- expect(capturedSlackNotice).toContain("joined an existing thread");
1999
- const contents = db.$client
2000
- .prepare("SELECT content FROM messages")
2001
- .all() as Array<{ content: string }>;
2002
- expect(
2003
- contents.some((row) => row.content.includes("Slack context note")),
2004
- ).toBe(false);
2267
+ expect(capturedHints ?? []).toEqual([]);
2005
2268
  });
2006
2269
 
2007
2270
  test("live Slack non-guardian passes raw displayContent while wrapping model content", async () => {
@@ -2009,6 +2272,14 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
2009
2272
  const captured = await handleAndCaptureLiveSlackProcessMessage(
2010
2273
  buildSlackChannelRequest("1700000000.000300", {
2011
2274
  content: rawContent,
2275
+ clientTimezone: "America/Los_Angeles",
2276
+ sourceMetadata: {
2277
+ messageId: "1700000000.000300",
2278
+ chatType: "channel",
2279
+ timezone: "America/New_York",
2280
+ timezoneLabel: "Eastern Time",
2281
+ timezoneOffsetSeconds: -18000,
2282
+ },
2012
2283
  }),
2013
2284
  );
2014
2285
 
@@ -2020,10 +2291,27 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
2020
2291
  expect(captured.options?.slackInbound?.actorExternalUserId).toBe(
2021
2292
  HTTP_SLACK_USER_ID,
2022
2293
  );
2294
+ expect(captured.options?.transport?.clientTimezone).toBe(
2295
+ "America/Los_Angeles",
2296
+ );
2297
+ expect(captured.options?.slackInbound?.actorTimezone).toBe(
2298
+ "America/New_York",
2299
+ );
2300
+ expect(captured.options?.slackInbound?.timestampTimezone).toBe(
2301
+ "America/Los_Angeles",
2302
+ );
2303
+ expect(captured.options?.slackInbound?.timestampTimezoneLabel).toBe("PT");
2304
+ expect(captured.options?.slackInbound?.speakerTimezoneLabel).toBe(
2305
+ "Eastern Time",
2306
+ );
2023
2307
 
2024
2308
  const persisted = readMessagesByConversation(captured.conversationId);
2025
2309
  expect(persisted).toHaveLength(1);
2026
2310
  expect(persisted[0].content).toBe(rawContent);
2311
+ const [persistedSlackRow] = readPersistedSlackRows(captured.conversationId);
2312
+ expect(persistedSlackRow?.timestampTimezone).toBe("America/Los_Angeles");
2313
+ expect(persistedSlackRow?.timestampTimezoneLabel).toBe("PT");
2314
+ expect(persistedSlackRow?.speakerTimezoneLabel).toBe("Eastern Time");
2027
2315
  });
2028
2316
 
2029
2317
  test("live Slack attachment-only passes empty raw displayContent for persistence", async () => {
@@ -2236,6 +2524,66 @@ describe("handleChannelInbound — Slack thread backfill wiring", () => {
2236
2524
  expect(backfillThreadMock).not.toHaveBeenCalled();
2237
2525
  });
2238
2526
 
2527
+ test("threaded Slack DMs use thread backfill instead of whole-DM backfill", async () => {
2528
+ const dmChannelId = "D0HTTPAPPTHREAD";
2529
+ const threadTs = "1700000000.000100";
2530
+ const inboundTs = "1700000000.000300";
2531
+ seedHttpActiveMember(dmChannelId);
2532
+ backfillDmMock.mockImplementation(async () => {
2533
+ throw new Error("whole-DM backfill should not run for threaded DMs");
2534
+ });
2535
+ backfillThreadMock.mockImplementation(async () => [
2536
+ makeBackfillMessage({
2537
+ id: threadTs,
2538
+ conversationId: dmChannelId,
2539
+ text: "app DM thread root",
2540
+ sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
2541
+ }),
2542
+ makeBackfillMessage({
2543
+ id: "1700000000.000200",
2544
+ conversationId: dmChannelId,
2545
+ text: "app DM thread context",
2546
+ threadId: threadTs,
2547
+ sender: { id: HTTP_SLACK_USER_ID, name: HTTP_SLACK_DISPLAY_NAME },
2548
+ }),
2549
+ ]);
2550
+
2551
+ const processMessage = async (
2552
+ conversationId: string,
2553
+ content: string,
2554
+ _attachmentIds?: string[],
2555
+ options?: SlackInboundProcessOptions,
2556
+ ): Promise<{ messageId: string }> => ({
2557
+ messageId: persistSlackInboundFromProcessMessage(
2558
+ conversationId,
2559
+ content,
2560
+ options,
2561
+ ),
2562
+ });
2563
+ setAdapterProcessMessage(processMessage);
2564
+
2565
+ const resp = await handleChannelInbound(
2566
+ buildSlackDmRequest(dmChannelId, inboundTs, {
2567
+ sourceMetadata: {
2568
+ messageId: inboundTs,
2569
+ threadId: threadTs,
2570
+ chatType: "im",
2571
+ },
2572
+ }),
2573
+ processMessage,
2574
+ TEST_BEARER_TOKEN,
2575
+ );
2576
+
2577
+ expect(resp.status).toBe(200);
2578
+ await new Promise((resolve) => setTimeout(resolve, 100));
2579
+
2580
+ expect(backfillDmMock).not.toHaveBeenCalled();
2581
+ expect(backfillThreadMock.mock.calls.length).toBeGreaterThanOrEqual(1);
2582
+ const [calledChannel, calledThread] = backfillThreadMock.mock.calls[0];
2583
+ expect(calledChannel).toBe(dmChannelId);
2584
+ expect(calledThread).toBe(threadTs);
2585
+ });
2586
+
2239
2587
  test("second thread reply within the TTL window can fetch a newer bounded gap", async () => {
2240
2588
  backfillThreadMock.mockImplementation(async () => [
2241
2589
  makeBackfillMessage({ id: "5678.0", text: "parent" }),