@vellumai/assistant 0.8.3 → 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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -1,10 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
3
 
4
- import {
5
- invalidateConfigCache,
6
- loadRawConfig,
7
- } from "../config/loader.js";
4
+ import { invalidateConfigCache, loadRawConfig } from "../config/loader.js";
8
5
  import {
9
6
  classifySlash,
10
7
  resolveSlash,
@@ -42,6 +39,7 @@ describe("resolveSlash /commands interface-aware help", () => {
42
39
  expect(lines).toEqual([
43
40
  "/commands — List all available commands",
44
41
  "/compact — Force context compaction immediately",
42
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)",
45
43
  "/context — Show conversation context usage",
46
44
  "/model — List or switch inference profile",
47
45
  "/models — List all available models",
@@ -58,6 +56,7 @@ describe("resolveSlash /commands interface-aware help", () => {
58
56
  expect(lines).toEqual([
59
57
  "/commands — List all available commands",
60
58
  "/compact — Force context compaction immediately",
59
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)",
61
60
  "/context — Show conversation context usage",
62
61
  "/model — List or switch inference profile",
63
62
  "/models — List all available models",
@@ -74,6 +73,7 @@ describe("resolveSlash /commands interface-aware help", () => {
74
73
  expect(lines).toEqual([
75
74
  "/commands — List all available commands",
76
75
  "/compact — Force context compaction immediately",
76
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)",
77
77
  "/context — Show conversation context usage",
78
78
  "/model — List or switch inference profile",
79
79
  "/models — List all available models",
@@ -87,6 +87,7 @@ describe("resolveSlash /commands interface-aware help", () => {
87
87
  expect(lines).toEqual([
88
88
  "/commands — List all available commands",
89
89
  "/compact — Force context compaction immediately",
90
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)",
90
91
  "/context — Show conversation context usage",
91
92
  "/model — List or switch inference profile",
92
93
  "/models — List all available models",
@@ -99,6 +100,7 @@ describe("resolveSlash /commands interface-aware help", () => {
99
100
  expect(lines).toEqual([
100
101
  "/commands — List all available commands",
101
102
  "/compact — Force context compaction immediately",
103
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)",
102
104
  "/model — List or switch inference profile",
103
105
  "/models — List all available models",
104
106
  ]);
@@ -187,13 +189,38 @@ describe("resolveSlash /compact target override", () => {
187
189
  });
188
190
  });
189
191
 
192
+ describe("resolveSlash /clean", () => {
193
+ test("plain /clean resolves to kind=clean", async () => {
194
+ const result = await resolveSlash("/clean");
195
+ expect(result).toEqual({ kind: "clean" });
196
+ });
197
+
198
+ test("/clean tolerates surrounding whitespace", async () => {
199
+ const result = await resolveSlash(" /clean ");
200
+ expect(result).toEqual({ kind: "clean" });
201
+ });
202
+
203
+ test("/clean is case-insensitive", async () => {
204
+ const result = await resolveSlash("/CLEAN");
205
+ expect(result).toEqual({ kind: "clean" });
206
+ });
207
+
208
+ test("/clean rejects arguments with usage hint", async () => {
209
+ const result = await resolveSlash("/clean now");
210
+ expect(result.kind).toBe("unknown");
211
+ if (result.kind !== "unknown") throw new Error("expected unknown");
212
+ expect(result.message).toContain("/clean");
213
+ expect(result.message).toContain("does not take arguments");
214
+ });
215
+ });
216
+
190
217
  describe("classifySlash is a pure classifier matching resolveSlash kinds", () => {
191
218
  // Lookahead in `buildPassthroughBatch` must not run `resolveSlash`'s side
192
219
  // effects. The pure classifier is synchronous, takes no side-effecting
193
220
  // dependencies, and must agree with resolveSlash's `kind`.
194
221
  const cases: Array<{
195
222
  input: string;
196
- kind: "passthrough" | "compact" | "unknown";
223
+ kind: "passthrough" | "compact" | "clean" | "unknown";
197
224
  }> = [
198
225
  { input: "/models", kind: "unknown" },
199
226
  { input: "/context", kind: "unknown" },
@@ -204,6 +231,9 @@ describe("classifySlash is a pure classifier matching resolveSlash kinds", () =>
204
231
  { input: "/compact 30k", kind: "compact" },
205
232
  { input: "/compact 1.5M", kind: "compact" },
206
233
  { input: "/compact bogus", kind: "unknown" },
234
+ { input: "/clean", kind: "clean" },
235
+ { input: " /clean ", kind: "clean" },
236
+ { input: "/clean foo", kind: "unknown" },
207
237
  { input: "/model", kind: "unknown" },
208
238
  { input: "/model foo", kind: "unknown" },
209
239
  { input: "/opus", kind: "unknown" },
@@ -318,9 +348,7 @@ describe("resolveSlash /model — inference profile switcher", () => {
318
348
  const result = await resolveSlash("/model balanced");
319
349
  expect(result.kind).toBe("unknown");
320
350
  if (result.kind !== "unknown") throw new Error("expected unknown kind");
321
- expect(result.message).toBe(
322
- "Already using profile `balanced` (Balanced).",
323
- );
351
+ expect(result.message).toBe("Already using profile `balanced` (Balanced).");
324
352
  });
325
353
 
326
354
  test("`/model` with no profiles defined points at Settings", async () => {
@@ -28,7 +28,7 @@ mock.module("../memory/guardian-action-store.js", () => ({
28
28
 
29
29
  mock.module("../providers/registry.js", () => ({
30
30
  getProvider: () => ({ name: "mock-provider" }),
31
- initializeProviders: () => {},
31
+ initializeProviders: async () => {},
32
32
  }));
33
33
 
34
34
  mock.module("../config/loader.js", () => ({
@@ -24,7 +24,7 @@ mock.module("../memory/guardian-action-store.js", () => ({
24
24
 
25
25
  mock.module("../providers/registry.js", () => ({
26
26
  getProvider: () => ({ name: "mock-provider" }),
27
- initializeProviders: () => {},
27
+ initializeProviders: async () => {},
28
28
  }));
29
29
 
30
30
  mock.module("../config/loader.js", () => ({
@@ -48,7 +48,7 @@ mock.module("../memory/guardian-action-store.js", () => ({
48
48
 
49
49
  mock.module("../providers/registry.js", () => ({
50
50
  getProvider: () => ({ name: "mock-provider" }),
51
- initializeProviders: () => {},
51
+ initializeProviders: async () => {},
52
52
  }));
53
53
 
54
54
  // Controllable config mock — speed and feature flag behavior are test-specific.
@@ -84,6 +84,95 @@ describe("task_progress surface compatibility", () => {
84
84
  expect(sent).toHaveLength(0);
85
85
  });
86
86
 
87
+ test("allows Slack ui_show for task_progress card when dynamic UI is otherwise disabled", async () => {
88
+ const sent: ServerMessage[] = [];
89
+ const ctx = makeContext(sent, {
90
+ channel: "slack",
91
+ supportsDynamicUi: false,
92
+ });
93
+
94
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
95
+ surface_type: "card",
96
+ title: "Working",
97
+ template: "task_progress",
98
+ templateData: {
99
+ status: "in_progress",
100
+ steps: [{ label: "Start", status: "in_progress" }],
101
+ },
102
+ });
103
+
104
+ expect(result.isError).toBe(false);
105
+ const showMessage = sent.find(
106
+ (msg): msg is UiSurfaceShow => msg.type === "ui_surface_show",
107
+ );
108
+ expect(showMessage).toBeDefined();
109
+ if (!showMessage || showMessage.surfaceType !== "card") return;
110
+ expect((showMessage.data as CardSurfaceData).template).toBe(
111
+ "task_progress",
112
+ );
113
+ });
114
+
115
+ test("blocks Slack ui_show for non-task_progress card when dynamic UI is disabled", async () => {
116
+ const sent: ServerMessage[] = [];
117
+ const ctx = makeContext(sent, {
118
+ channel: "slack",
119
+ supportsDynamicUi: false,
120
+ });
121
+
122
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
123
+ surface_type: "card",
124
+ title: "Blocked",
125
+ data: { title: "Blocked", body: "not progress" },
126
+ });
127
+
128
+ expect(result.isError).toBe(true);
129
+ expect(result.content).toContain(
130
+ 'ui_show is unavailable on channel "slack"',
131
+ );
132
+ expect(sent).toHaveLength(0);
133
+ });
134
+
135
+ test("blocks Slack ui_show when normalized card data is not task_progress", async () => {
136
+ const sent: ServerMessage[] = [];
137
+ const ctx = makeContext(sent, {
138
+ channel: "slack",
139
+ supportsDynamicUi: false,
140
+ });
141
+
142
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
143
+ surface_type: "card",
144
+ title: "Blocked",
145
+ template: "task_progress",
146
+ data: { title: "Blocked", body: "not progress", template: "plain" },
147
+ });
148
+
149
+ expect(result.isError).toBe(true);
150
+ expect(result.content).toContain(
151
+ 'ui_show is unavailable on channel "slack"',
152
+ );
153
+ expect(sent).toHaveLength(0);
154
+ });
155
+
156
+ test("blocks Slack ui_show for non-card task_progress input when dynamic UI is disabled", async () => {
157
+ const sent: ServerMessage[] = [];
158
+ const ctx = makeContext(sent, {
159
+ channel: "slack",
160
+ supportsDynamicUi: false,
161
+ });
162
+
163
+ const result = await surfaceProxyResolver(ctx, "ui_show", {
164
+ surface_type: "dynamic_page",
165
+ title: "Blocked",
166
+ data: { template: "task_progress", html: "<p>Blocked</p>" },
167
+ });
168
+
169
+ expect(result.isError).toBe(true);
170
+ expect(result.content).toContain(
171
+ 'ui_show is unavailable on channel "slack"',
172
+ );
173
+ expect(sent).toHaveLength(0);
174
+ });
175
+
87
176
  test("ui_show maps legacy top-level task_progress fields into card data", async () => {
88
177
  const sent: ServerMessage[] = [];
89
178
  const ctx = makeContext(sent);
@@ -249,6 +338,137 @@ describe("task_progress surface compatibility", () => {
249
338
  expect(Array.isArray(templateData.steps)).toBe(true);
250
339
  });
251
340
 
341
+ test("allows Slack ui_update for stored task_progress card when dynamic UI is disabled", async () => {
342
+ const sent: ServerMessage[] = [];
343
+ const ctx = makeContext(sent, {
344
+ channel: "slack",
345
+ supportsDynamicUi: false,
346
+ });
347
+ ctx.surfaceState.set("surface-1", {
348
+ surfaceType: "card",
349
+ data: {
350
+ title: "Working",
351
+ body: "",
352
+ template: "task_progress",
353
+ templateData: { status: "in_progress", steps: [] },
354
+ } satisfies CardSurfaceData,
355
+ });
356
+
357
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
358
+ surface_id: "surface-1",
359
+ data: { status: "completed" },
360
+ });
361
+
362
+ expect(result.isError).toBe(false);
363
+ const updateMessage = sent.find(
364
+ (msg): msg is UiSurfaceUpdate => msg.type === "ui_surface_update",
365
+ );
366
+ expect(updateMessage).toBeDefined();
367
+ if (!updateMessage) return;
368
+ const templateData = (updateMessage.data as CardSurfaceData)
369
+ .templateData as Record<string, unknown>;
370
+ expect(templateData.status).toBe("completed");
371
+ });
372
+
373
+ test("blocks Slack ui_update when stored surface is not a task_progress card", async () => {
374
+ const sent: ServerMessage[] = [];
375
+ const ctx = makeContext(sent, {
376
+ channel: "slack",
377
+ supportsDynamicUi: false,
378
+ });
379
+ ctx.surfaceState.set("surface-1", {
380
+ surfaceType: "card",
381
+ data: { title: "Plain", body: "No progress" } satisfies CardSurfaceData,
382
+ });
383
+
384
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
385
+ surface_id: "surface-1",
386
+ data: { body: "still blocked" },
387
+ });
388
+
389
+ expect(result.isError).toBe(true);
390
+ expect(result.content).toContain(
391
+ 'ui_update is unavailable on channel "slack"',
392
+ );
393
+ expect(sent).toHaveLength(0);
394
+ });
395
+
396
+ test("blocks Slack ui_update that would convert a plain card to task_progress", async () => {
397
+ const sent: ServerMessage[] = [];
398
+ const ctx = makeContext(sent, {
399
+ channel: "slack",
400
+ supportsDynamicUi: false,
401
+ });
402
+ ctx.surfaceState.set("surface-1", {
403
+ surfaceType: "card",
404
+ data: { title: "Plain", body: "No progress" } satisfies CardSurfaceData,
405
+ });
406
+
407
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
408
+ surface_id: "surface-1",
409
+ data: {
410
+ template: "task_progress",
411
+ templateData: { status: "in_progress", steps: [] },
412
+ },
413
+ });
414
+
415
+ expect(result.isError).toBe(true);
416
+ expect(result.content).toContain(
417
+ 'ui_update is unavailable on channel "slack"',
418
+ );
419
+ expect(sent).toHaveLength(0);
420
+ });
421
+
422
+ test("blocks Slack ui_update that would change task_progress card template", async () => {
423
+ const sent: ServerMessage[] = [];
424
+ const ctx = makeContext(sent, {
425
+ channel: "slack",
426
+ supportsDynamicUi: false,
427
+ });
428
+ ctx.surfaceState.set("surface-1", {
429
+ surfaceType: "card",
430
+ data: {
431
+ title: "Working",
432
+ body: "",
433
+ template: "task_progress",
434
+ templateData: { status: "in_progress", steps: [] },
435
+ } satisfies CardSurfaceData,
436
+ });
437
+
438
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
439
+ surface_id: "surface-1",
440
+ data: { template: "plain", body: "now a plain card" },
441
+ });
442
+
443
+ expect(result.isError).toBe(true);
444
+ expect(result.content).toContain(
445
+ 'ui_update is unavailable on channel "slack"',
446
+ );
447
+ expect(sent).toHaveLength(0);
448
+ expect(
449
+ (ctx.surfaceState.get("surface-1")?.data as CardSurfaceData).template,
450
+ ).toBe("task_progress");
451
+ });
452
+
453
+ test("blocks Slack ui_update when the surface is not already stored", async () => {
454
+ const sent: ServerMessage[] = [];
455
+ const ctx = makeContext(sent, {
456
+ channel: "slack",
457
+ supportsDynamicUi: false,
458
+ });
459
+
460
+ const result = await surfaceProxyResolver(ctx, "ui_update", {
461
+ surface_id: "missing-surface",
462
+ data: { status: "completed" },
463
+ });
464
+
465
+ expect(result.isError).toBe(true);
466
+ expect(result.content).toContain(
467
+ 'ui_update is unavailable on channel "slack"',
468
+ );
469
+ expect(sent).toHaveLength(0);
470
+ });
471
+
252
472
  test("ui_show rejects new interactive surface when a non-dynamic_page pending surface exists", async () => {
253
473
  const sent: ServerMessage[] = [];
254
474
  const ctx = makeContext(sent);
@@ -18,7 +18,7 @@ mock.module("../util/logger.js", () => ({
18
18
 
19
19
  mock.module("../providers/registry.js", () => ({
20
20
  getProvider: () => ({ name: "mock-provider" }),
21
- initializeProviders: () => {},
21
+ initializeProviders: async () => {},
22
22
  }));
23
23
 
24
24
  mock.module("../config/loader.js", () => ({
@@ -27,7 +27,7 @@ mock.module("../memory/guardian-action-store.js", () => ({
27
27
 
28
28
  mock.module("../providers/registry.js", () => ({
29
29
  getProvider: () => ({ name: "mock-provider" }),
30
- initializeProviders: () => {},
30
+ initializeProviders: async () => {},
31
31
  }));
32
32
 
33
33
  mock.module("../config/loader.js", () => ({
@@ -141,6 +141,10 @@ mock.module("../memory/conversation-crud.js", () => ({
141
141
  deleteLastExchange: () => 0,
142
142
  getMessageById: () => null,
143
143
  getLastUserTimestampBefore: () => 0,
144
+ setLastNotifiedInferenceProfile: () => {},
145
+ getConversationOverrideProfileFromRow: () => undefined,
146
+ updateMessageMetadata: () => {},
147
+ clearStrippedInjectionMetadataForConversation: () => {},
144
148
  }));
145
149
 
146
150
  mock.module("../memory/conversation-queries.js", () => ({
@@ -25,7 +25,7 @@ mock.module("../memory/guardian-action-store.js", () => ({
25
25
 
26
26
  mock.module("../providers/registry.js", () => ({
27
27
  getProvider: () => ({ name: "mock-provider" }),
28
- initializeProviders: () => {},
28
+ initializeProviders: async () => {},
29
29
  }));
30
30
 
31
31
  mock.module("../config/loader.js", () => ({
@@ -138,6 +138,10 @@ mock.module("../memory/conversation-crud.js", () => ({
138
138
  deleteLastExchange: () => 0,
139
139
  getMessageById: () => null,
140
140
  getLastUserTimestampBefore: () => 0,
141
+ setLastNotifiedInferenceProfile: () => {},
142
+ getConversationOverrideProfileFromRow: () => undefined,
143
+ updateMessageMetadata: () => {},
144
+ clearStrippedInjectionMetadataForConversation: () => {},
141
145
  }));
142
146
 
143
147
  mock.module("../memory/conversation-queries.js", () => ({
@@ -209,6 +209,9 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
209
209
  "config/bundled-skills/media-processing/tools/analyze-keyframes.ts", // keyframe analysis tool API key lookup
210
210
  "providers/registry.ts", // provider registry API key lookup for initialization
211
211
  "providers/inference/resolve-auth.ts", // provider_connection auth resolver (api_key path reads vault, mirrors registry.ts)
212
+ "providers/inference/codex-token-refresh.ts", // Codex OAuth token refresh (reads/writes access_token, refresh_token, expires_at)
213
+ "cli/commands/inference-providers.ts", // ChatGPT subscription OAuth token storage
214
+ "runtime/routes/chatgpt-subscription-auth-routes.ts", // ChatGPT subscription daemon OAuth flow (stores tokens in CES)
212
215
  "providers/provider-availability.ts", // provider availability API key check
213
216
  "media/image-credentials.ts", // shared image-gen credential resolver (provider API key lookup)
214
217
  "memory/embedding-backend.ts", // embedding backend API key lookup
@@ -223,6 +226,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
223
226
  "daemon/daemon-skill-host.ts", // SkillHost secureKeys facet adapter (delegates to getProviderKeyAsync)
224
227
  "runtime/routes/credential-prompt-routes.ts", // Route for secure credential prompt (stores secret via setSecureKeyAsync)
225
228
  "runtime/routes/credential-routes.ts", // CLI credential management routes (CLI-migrated to IPC)
229
+ "runtime/routes/sanity-routes.ts", // Sanity connect/discover routes (reads stored api_token from credential store)
226
230
  "runtime/routes/platform-routes.ts", // CLI platform connect/disconnect/status routes (CLI-migrated to IPC)
227
231
  "ipc/skill-routes/providers.ts", // host.providers.secureKeys.getProviderKey IPC route (out-of-process SkillHost companion)
228
232
  "daemon/external-plugins-bootstrap.ts", // reads credentials at plugin init (manifest.requiresCredential) via the CES-mediated getSecureKeyAsync path
@@ -236,6 +240,8 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
236
240
  "runtime/routes/avatar-routes.ts", // avatar generate route reads platform_base_url from credential store
237
241
  "cli/commands/keys.ts", // CLI provider key management
238
242
  "cli/commands/oauth/connect.ts", // CLI OAuth connect stored-secret verification
243
+ "runtime/routes/chatgpt-subscription-auth-routes.ts", // ChatGPT subscription OAuth token storage
244
+ "runtime/routes/identity-routes.ts", // health/readyz endpoint checks CES connectivity via getCesClient
239
245
  ]);
240
246
 
241
247
  const thisDir = dirname(fileURLToPath(import.meta.url));
@@ -26,7 +26,16 @@ let mockCuClients: Array<{
26
26
  ];
27
27
 
28
28
  mock.module("../runtime/assistant-event-hub.js", () => ({
29
- broadcastMessage: (msg: unknown) => sentMessages.push(msg),
29
+ broadcastMessage: (msg: unknown) => {
30
+ // `interaction_resolved` envelopes are emitted by the
31
+ // pending-interactions tracker for every resolution. They are
32
+ // orthogonal to the surface-proxy wire messages these tests assert
33
+ // on, so swallow them here.
34
+ if ((msg as { type?: string } | null)?.type === "interaction_resolved") {
35
+ return;
36
+ }
37
+ sentMessages.push(msg);
38
+ },
30
39
  assistantEventHub: {
31
40
  getMostRecentClientByCapability: (cap: string) =>
32
41
  cap === "host_cu" && mockHasClient ? { id: "mock-client" } : null,
@@ -40,6 +40,20 @@ mock.module("../runtime/gateway-client.js", () => ({
40
40
  deliverChannelReply: async () => {},
41
41
  }));
42
42
 
43
+ mock.module("../messaging/providers/slack/adapter.js", () => ({
44
+ withSlackBotToken: async (
45
+ _account: string | undefined,
46
+ fn: (token: string) => Promise<unknown>,
47
+ ) => fn("test-slack-token"),
48
+ resolveSlackBotUserId: async (
49
+ _account: string | undefined,
50
+ botId: string,
51
+ ) => {
52
+ if (botId === "B_ASSISTANT") return "U_BOT";
53
+ return null;
54
+ },
55
+ }));
56
+
43
57
  import type { Message } from "../messaging/provider-types.js";
44
58
 
45
59
  // `backfillDm` is the only piece of the slack provider surface this test
@@ -59,6 +73,11 @@ mock.module("../messaging/providers/slack/backfill.js", () => ({
59
73
  backfillThread: () => backfillThreadMock(),
60
74
  }));
61
75
 
76
+ import {
77
+ loadRawConfig,
78
+ saveRawConfig,
79
+ setNestedValue,
80
+ } from "../config/loader.js";
62
81
  import { upsertContactChannel } from "../contacts/contacts-write.js";
63
82
  import { getDb } from "../memory/db-connection.js";
64
83
  import { initializeDb } from "../memory/db-init.js";
@@ -88,6 +107,13 @@ function resetState(): void {
88
107
  db.run("DELETE FROM contact_channels");
89
108
  db.run("DELETE FROM contacts");
90
109
  db.run("DELETE FROM external_conversation_bindings");
110
+ setConfiguredSlackBotUserId("U_BOT");
111
+ }
112
+
113
+ function setConfiguredSlackBotUserId(botUserId: string): void {
114
+ const raw = loadRawConfig();
115
+ setNestedValue(raw, "slack.botUserId", botUserId);
116
+ saveRawConfig(raw);
91
117
  }
92
118
 
93
119
  function seedActiveMember(): void {
@@ -494,6 +520,44 @@ describe("PR 23 — Slack DM cold-start backfill", () => {
494
520
  expect(botRow?.provenanceRequesterIdentifier).toBe("B_BOT");
495
521
  });
496
522
 
523
+ test("skips Slack assistant new-thread placeholder during DM backfill", async () => {
524
+ backfillDmMock.mockImplementation(async () => [
525
+ makeBackfilledMessage({
526
+ id: "1700000000.000001",
527
+ text: "New Assistant Thread",
528
+ sender: { id: "B_ASSISTANT", name: "Ada" },
529
+ metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
530
+ }),
531
+ makeBackfilledMessage({
532
+ id: "1700000000.000002",
533
+ text: "real bot context",
534
+ sender: { id: "B_ASSISTANT", name: "Ada" },
535
+ metadata: { isBot: true, slackBotId: "B_ASSISTANT" },
536
+ }),
537
+ makeBackfilledMessage({
538
+ id: "1700000000.000003",
539
+ text: "New Assistant Thread",
540
+ sender: { id: "B_OTHER", name: "Build Bot" },
541
+ metadata: { isBot: true, slackBotId: "B_OTHER" },
542
+ }),
543
+ ]);
544
+
545
+ await handleChannelInbound(
546
+ buildDmRequest("live new DM"),
547
+ noopProcessMessage,
548
+ TEST_BEARER_TOKEN,
549
+ );
550
+
551
+ const rows = readPersistedSlackRows();
552
+ expect(rows.map((row) => row.rawContent).sort()).toEqual([
553
+ "New Assistant Thread",
554
+ "real bot context",
555
+ ]);
556
+ expect(
557
+ rows.some((row) => row.slackMeta?.actorExternalUserId === "B_OTHER"),
558
+ ).toBe(true);
559
+ });
560
+
497
561
  test("backfill skips channelTs values already stored", async () => {
498
562
  // First DM: backfill returns three rows.
499
563
  backfillDmMock.mockImplementation(async () => [
@@ -179,6 +179,39 @@ describe("PR 16 — Slack DM persistence parity", () => {
179
179
  expect(slackMeta!.displayName).toBeUndefined();
180
180
  });
181
181
 
182
+ test("DM inbound persists Slack actor timezone metadata", async () => {
183
+ const ctx = createSlackTurnContext();
184
+ await persistQueuedMessageBody(
185
+ ctx,
186
+ "hello across timezones",
187
+ [],
188
+ "req-dm-timezone",
189
+ {
190
+ slackInbound: {
191
+ channelId: "D0123DM",
192
+ channelTs: "1700000000.777777",
193
+ displayName: "Alice",
194
+ actorTimezone: "America/New_York",
195
+ actorTimezoneLabel: "ET",
196
+ actorTimezoneOffsetSeconds: -18000,
197
+ timestampTimezone: "America/New_York",
198
+ timestampTimezoneLabel: "ET",
199
+ speakerTimezoneLabel: "ET",
200
+ },
201
+ },
202
+ undefined,
203
+ );
204
+
205
+ const slackMeta = lastPersistedSlackMeta();
206
+ expect(slackMeta).not.toBeNull();
207
+ expect(slackMeta!.actorTimezone).toBe("America/New_York");
208
+ expect(slackMeta!.actorTimezoneLabel).toBe("ET");
209
+ expect(slackMeta!.actorTimezoneOffsetSeconds).toBe(-18000);
210
+ expect(slackMeta!.timestampTimezone).toBe("America/New_York");
211
+ expect(slackMeta!.timestampTimezoneLabel).toBe("ET");
212
+ expect(slackMeta!.speakerTimezoneLabel).toBe("ET");
213
+ });
214
+
182
215
  test("DM and channel-message envelopes differ only by threadTs", async () => {
183
216
  // Capture the channel-thread case first.
184
217
  const ctx = createSlackTurnContext();