@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
@@ -5,6 +5,8 @@
5
5
  * implements the MessagingProvider interface.
6
6
  */
7
7
 
8
+ import { createHash } from "node:crypto";
9
+
8
10
  import {
9
11
  buildSlackUserLabelMap,
10
12
  renderSlackTextForModel,
@@ -35,10 +37,32 @@ import type {
35
37
  SlackConversation,
36
38
  SlackMessage,
37
39
  SlackSearchMatch,
40
+ SlackUser,
38
41
  } from "./types.js";
39
42
 
40
- // Cache user display names to avoid repeated API calls within a session
41
- const userNameCache = new Map<string, string>();
43
+ interface NormalizedSlackUserInfo {
44
+ displayName: string;
45
+ timezone?: string;
46
+ timezoneLabel?: string;
47
+ timezoneOffsetSeconds?: number;
48
+ }
49
+
50
+ interface SlackUserInfoLookupResult {
51
+ info: NormalizedSlackUserInfo;
52
+ cacheable: boolean;
53
+ }
54
+
55
+ const PERMANENT_USER_INFO_SLACK_ERRORS = new Set([
56
+ "account_inactive",
57
+ "ekm_access_denied",
58
+ "missing_scope",
59
+ "not_allowed_token_type",
60
+ "user_not_found",
61
+ "user_not_visible",
62
+ ]);
63
+
64
+ // Cache normalized Slack user facts to avoid repeated API calls within a session.
65
+ const userInfoCache = new Map<string, Promise<SlackUserInfoLookupResult>>();
42
66
 
43
67
  /**
44
68
  * Cached auth resolved during resolveConnection(), split by direction.
@@ -58,6 +82,7 @@ const userNameCache = new Map<string, string>();
58
82
  */
59
83
  let _cachedSlackWriteAuth: OAuthConnection | string | null = null;
60
84
  let _cachedSlackReadAuth: OAuthConnection | string | null = null;
85
+ const botUserIdByBotIdCache = new Map<string, string>();
61
86
 
62
87
  /**
63
88
  * Get the Slack auth value to pass to Slack client functions.
@@ -123,6 +148,32 @@ export async function withSlackBotToken<T>(
123
148
  return auth.withToken(fn);
124
149
  }
125
150
 
151
+ export async function resolveSlackBotUserId(
152
+ account: string | undefined,
153
+ botId: string,
154
+ ): Promise<string | null> {
155
+ const trimmedBotId = botId.trim();
156
+ if (!trimmedBotId) return null;
157
+
158
+ const cacheKey = account ? `${account}:${trimmedBotId}` : null;
159
+ if (cacheKey && botUserIdByBotIdCache.has(cacheKey)) {
160
+ return botUserIdByBotIdCache.get(cacheKey) ?? null;
161
+ }
162
+
163
+ const resolvedUserId = await withSlackBotToken(account, async (token) => {
164
+ const resp = await slack.botsInfo(token, trimmedBotId);
165
+ const userId = resp.bot.user_id?.trim();
166
+ return userId && userId.length > 0 ? userId : null;
167
+ });
168
+ if (resolvedUserId) {
169
+ if (cacheKey) {
170
+ botUserIdByBotIdCache.set(cacheKey, resolvedUserId);
171
+ }
172
+ return resolvedUserId;
173
+ }
174
+ return null;
175
+ }
176
+
126
177
  /**
127
178
  * Run a read-path Slack call, falling back to the bot token if the cached
128
179
  * user token is rejected with an auth error. On fallback, the read cache is
@@ -159,20 +210,46 @@ async function resolveUserName(
159
210
  auth: OAuthConnection | string,
160
211
  userId: string,
161
212
  ): Promise<string> {
162
- if (!userId) return "unknown";
163
- const cached = userNameCache.get(userId);
164
- if (cached) return cached;
213
+ return (await resolveUserInfo(auth, userId)).displayName;
214
+ }
215
+
216
+ async function resolveUserInfo(
217
+ auth: OAuthConnection | string,
218
+ userId: string,
219
+ ): Promise<NormalizedSlackUserInfo> {
220
+ if (!userId) return { displayName: "unknown" };
221
+ const cacheKey = slackUserInfoCacheKey(auth, userId);
222
+ const cached = userInfoCache.get(cacheKey);
223
+ if (cached) return (await cached).info;
224
+
225
+ const resolved = resolveUserInfoUncached(auth, userId).then(
226
+ (result) => {
227
+ if (!result.cacheable) {
228
+ userInfoCache.delete(cacheKey);
229
+ }
230
+ return result;
231
+ },
232
+ (err) => {
233
+ userInfoCache.delete(cacheKey);
234
+ throw err;
235
+ },
236
+ );
237
+ userInfoCache.set(cacheKey, resolved);
238
+ return (await resolved).info;
239
+ }
165
240
 
166
- // Check contacts DB for a persistent cache hit
241
+ async function resolveUserInfoUncached(
242
+ auth: OAuthConnection | string,
243
+ userId: string,
244
+ ): Promise<SlackUserInfoLookupResult> {
245
+ let contactDisplayName: string | undefined;
167
246
  try {
168
247
  const result = findContactChannel({
169
248
  channelType: "slack",
170
249
  externalUserId: userId,
171
250
  });
172
251
  if (result) {
173
- const name = result.contact.displayName;
174
- userNameCache.set(userId, name);
175
- return name;
252
+ contactDisplayName = result.contact.displayName;
176
253
  }
177
254
  } catch {
178
255
  // Contact lookup failures are non-fatal — fall through to API
@@ -180,18 +257,86 @@ async function resolveUserName(
180
257
 
181
258
  try {
182
259
  const resp = await slack.userInfo(auth, userId);
183
- const name =
184
- resp.user.profile?.display_name ||
185
- resp.user.profile?.real_name ||
186
- resp.user.real_name ||
187
- resp.user.name;
188
- userNameCache.set(userId, name);
189
- return name;
190
- } catch {
191
- return userId;
260
+ return {
261
+ info: normalizeSlackUserInfo(resp.user, contactDisplayName),
262
+ cacheable: true,
263
+ };
264
+ } catch (err) {
265
+ return {
266
+ info: { displayName: contactDisplayName ?? userId },
267
+ cacheable: isPermanentSlackUserInfoFailure(err),
268
+ };
192
269
  }
193
270
  }
194
271
 
272
+ function isPermanentSlackUserInfoFailure(err: unknown): boolean {
273
+ return (
274
+ err instanceof SlackApiError &&
275
+ PERMANENT_USER_INFO_SLACK_ERRORS.has(err.slackError)
276
+ );
277
+ }
278
+
279
+ function slackUserInfoCacheKey(
280
+ auth: OAuthConnection | string,
281
+ userId: string,
282
+ ): string {
283
+ const authScope =
284
+ typeof auth === "string"
285
+ ? `token:${createHash("sha256").update(auth).digest("hex")}`
286
+ : `connection:${auth.id}:${auth.accountInfo ?? ""}`;
287
+ return `${authScope}:user:${userId}`;
288
+ }
289
+
290
+ function normalizeSlackUserInfo(
291
+ user: SlackUser,
292
+ contactDisplayName: string | undefined,
293
+ ): NormalizedSlackUserInfo {
294
+ const displayName =
295
+ contactDisplayName ||
296
+ user.profile?.display_name ||
297
+ user.profile?.real_name ||
298
+ user.real_name ||
299
+ user.name ||
300
+ user.id;
301
+ const timezone = trimNonEmpty(user.tz);
302
+ const timezoneLabel = trimNonEmpty(user.tz_label);
303
+ const timezoneOffsetSeconds =
304
+ typeof user.tz_offset === "number" && Number.isFinite(user.tz_offset)
305
+ ? user.tz_offset
306
+ : undefined;
307
+ return {
308
+ displayName,
309
+ ...(timezone ? { timezone } : {}),
310
+ ...(timezoneLabel ? { timezoneLabel } : {}),
311
+ ...(timezoneOffsetSeconds !== undefined ? { timezoneOffsetSeconds } : {}),
312
+ };
313
+ }
314
+
315
+ function trimNonEmpty(value: unknown): string | undefined {
316
+ if (typeof value !== "string") return undefined;
317
+ const trimmed = value.trim();
318
+ return trimmed.length > 0 ? trimmed : undefined;
319
+ }
320
+
321
+ export function __resetSlackUserInfoCacheForTests(): void {
322
+ userInfoCache.clear();
323
+ }
324
+
325
+ function slackUserInfoMetadata(
326
+ userInfo: NormalizedSlackUserInfo | undefined,
327
+ ): Record<string, unknown> {
328
+ if (!userInfo) return {};
329
+ return {
330
+ ...(userInfo.timezone ? { actorTimezone: userInfo.timezone } : {}),
331
+ ...(userInfo.timezoneLabel
332
+ ? { actorTimezoneLabel: userInfo.timezoneLabel }
333
+ : {}),
334
+ ...(userInfo.timezoneOffsetSeconds !== undefined
335
+ ? { actorTimezoneOffsetSeconds: userInfo.timezoneOffsetSeconds }
336
+ : {}),
337
+ };
338
+ }
339
+
195
340
  function mapConversationType(conv: SlackConversation): Conversation["type"] {
196
341
  if (conv.is_im) return "dm";
197
342
  if (conv.is_mpim) return "group";
@@ -249,19 +394,25 @@ function mapSlackFiles(files: SlackMessage["files"]):
249
394
  function mapMessage(
250
395
  msg: SlackMessage,
251
396
  channelId: string,
252
- senderName: string,
397
+ senderInfo: NormalizedSlackUserInfo,
253
398
  renderedText: string,
254
399
  ): Message {
255
400
  // Bot-authored when Slack sets `subtype: "bot_message"` or attributes the
256
- // row to a `bot_id` with no user. Backfill callers rely on this flag to
257
- // avoid rehydrating assistant/bot replies as user turns.
401
+ // row to a `bot_id` with no user. Backfill callers use this flag for
402
+ // bot-specific filtering while preserving real bot rows as channel replay.
258
403
  const isBot =
259
404
  msg.subtype === "bot_message" || (msg.bot_id != null && !msg.user);
260
405
  const slackFiles = mapSlackFiles(msg.files);
406
+ const slackBotId = msg.bot_id?.trim();
407
+ const userMetadata = slackUserInfoMetadata(msg.user ? senderInfo : undefined);
408
+ const hasUserMetadata = Object.keys(userMetadata).length > 0;
261
409
  return {
262
410
  id: msg.ts,
263
411
  conversationId: channelId,
264
- sender: { id: msg.user ?? msg.bot_id ?? "unknown", name: senderName },
412
+ sender: {
413
+ id: msg.user ?? msg.bot_id ?? "unknown",
414
+ name: senderInfo.displayName,
415
+ },
265
416
  text: renderedText,
266
417
  timestamp: parseFloat(msg.ts) * 1000,
267
418
  threadId: msg.thread_ts,
@@ -269,11 +420,13 @@ function mapMessage(
269
420
  platform: "slack",
270
421
  reactions: msg.reactions?.map((r) => ({ name: r.name, count: r.count })),
271
422
  hasAttachments: (msg.files?.length ?? 0) > 0,
272
- ...(isBot || slackFiles
423
+ ...(isBot || slackFiles || hasUserMetadata
273
424
  ? {
274
425
  metadata: {
275
426
  ...(isBot ? { isBot: true } : {}),
427
+ ...(slackBotId ? { slackBotId } : {}),
276
428
  ...(slackFiles ? { slackFiles } : {}),
429
+ ...userMetadata,
277
430
  },
278
431
  }
279
432
  : {}),
@@ -307,12 +460,12 @@ async function mapSlackMessages(
307
460
  );
308
461
  const messages: Message[] = [];
309
462
  for (const msg of slackMessages) {
310
- const name = await resolveUserName(auth, msg.user ?? "");
463
+ const senderInfo = await resolveUserInfo(auth, msg.user ?? "");
311
464
  messages.push(
312
465
  mapMessage(
313
466
  msg,
314
467
  channelId,
315
- name,
468
+ senderInfo,
316
469
  renderSlackTextForModel(msg.text, { userLabels }),
317
470
  ),
318
471
  );
@@ -0,0 +1,54 @@
1
+ import { afterAll, afterEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ const BOT_TOKEN = "xoxb-test";
4
+
5
+ mock.module("../../../security/secure-keys.js", () => ({
6
+ getSecureKeyAsync: async () => BOT_TOKEN,
7
+ }));
8
+
9
+ const { getSlackConversationInfo } = await import("./api.js");
10
+
11
+ const originalFetch = globalThis.fetch;
12
+
13
+ afterEach(() => {
14
+ globalThis.fetch = originalFetch;
15
+ });
16
+
17
+ afterAll(() => {
18
+ mock.restore();
19
+ });
20
+
21
+ describe("getSlackConversationInfo", () => {
22
+ test("calls conversations.info with GET query params", async () => {
23
+ let capturedUrl: string | undefined;
24
+ let capturedInit: RequestInit | undefined;
25
+
26
+ globalThis.fetch = mock(async (input, init) => {
27
+ capturedUrl = String(input);
28
+ capturedInit = init;
29
+ return new Response(
30
+ JSON.stringify({
31
+ ok: true,
32
+ channel: {
33
+ id: "C123",
34
+ name: "engineering",
35
+ },
36
+ }),
37
+ { status: 200, headers: { "content-type": "application/json" } },
38
+ );
39
+ }) as unknown as typeof fetch;
40
+
41
+ const info = await getSlackConversationInfo("C123");
42
+
43
+ expect(info).toEqual({ id: "C123", name: "engineering" });
44
+ expect(capturedUrl).toBeDefined();
45
+ const url = new URL(capturedUrl!);
46
+ expect(url.pathname).toBe("/api/conversations.info");
47
+ expect(url.searchParams.get("channel")).toBe("C123");
48
+ expect(capturedInit?.method).toBe("GET");
49
+ expect(capturedInit?.body).toBeUndefined();
50
+ expect(capturedInit?.headers).toEqual({
51
+ Authorization: `Bearer ${BOT_TOKEN}`,
52
+ });
53
+ });
54
+ });
@@ -53,9 +53,7 @@ const SLACK_ERROR_CODE_MAP: Record<string, SlackErrorCategory> = {
53
53
  invalid_blocks: "client_error",
54
54
  };
55
55
 
56
- function classifySlackError(
57
- errorCode: string | undefined,
58
- ): SlackErrorCategory {
56
+ function classifySlackError(errorCode: string | undefined): SlackErrorCategory {
59
57
  if (!errorCode) return "unknown";
60
58
  return SLACK_ERROR_CODE_MAP[errorCode] ?? "unknown";
61
59
  }
@@ -98,6 +96,20 @@ interface SlackApiResponse {
98
96
  file_id?: string;
99
97
  }
100
98
 
99
+ interface SlackConversationsInfoResponse extends SlackApiResponse {
100
+ channel?: {
101
+ id?: unknown;
102
+ name?: unknown;
103
+ name_normalized?: unknown;
104
+ };
105
+ }
106
+
107
+ export interface SlackConversationInfo {
108
+ id: string;
109
+ name?: string;
110
+ nameNormalized?: string;
111
+ }
112
+
101
113
  /**
102
114
  * Call a Slack Web API method with rate-limit retries.
103
115
  *
@@ -178,6 +190,110 @@ export async function callSlackApi(
178
190
  );
179
191
  }
180
192
 
193
+ /**
194
+ * Call a Slack Web API read method with query parameters.
195
+ */
196
+ async function callSlackApiGet(
197
+ method: string,
198
+ params: URLSearchParams,
199
+ ): Promise<SlackApiResponse> {
200
+ const botToken = await resolveBotToken();
201
+ const query = params.toString();
202
+ const url = `${SLACK_API_BASE}/${method}${query ? `?${query}` : ""}`;
203
+
204
+ let lastError: string | undefined;
205
+
206
+ for (let attempt = 0; attempt <= SLACK_MAX_RATE_LIMIT_RETRIES; attempt++) {
207
+ const response = await fetch(url, {
208
+ method: "GET",
209
+ headers: {
210
+ Authorization: `Bearer ${botToken}`,
211
+ },
212
+ });
213
+
214
+ if (response.status === 429) {
215
+ if (attempt >= SLACK_MAX_RATE_LIMIT_RETRIES) {
216
+ throw new Error("Slack rate limit exceeded after retries");
217
+ }
218
+ const retryAfter =
219
+ parseInt(response.headers.get("Retry-After") ?? "", 10) ||
220
+ SLACK_DEFAULT_RETRY_AFTER_S;
221
+ log.warn({ method, retryAfter, attempt }, "Slack rate limited, retrying");
222
+ await new Promise((r) => setTimeout(r, retryAfter * 1000));
223
+ continue;
224
+ }
225
+
226
+ if (response.status >= 500) {
227
+ if (attempt >= SLACK_MAX_RATE_LIMIT_RETRIES) {
228
+ throw new Error(
229
+ `Slack ${method} failed with status ${response.status} after retries`,
230
+ );
231
+ }
232
+ log.warn(
233
+ { method, status: response.status, attempt },
234
+ "Slack 5xx error, retrying",
235
+ );
236
+ await new Promise((r) =>
237
+ setTimeout(r, SLACK_DEFAULT_RETRY_AFTER_S * 1000),
238
+ );
239
+ continue;
240
+ }
241
+
242
+ const data = (await response.json()) as SlackApiResponse;
243
+
244
+ if (!data.ok) {
245
+ lastError = data.error;
246
+ const category = classifySlackError(data.error);
247
+
248
+ if (category === "rate_limit" && attempt < SLACK_MAX_RATE_LIMIT_RETRIES) {
249
+ log.warn(
250
+ { method, slackError: data.error, attempt },
251
+ "Slack rate limited (body), retrying",
252
+ );
253
+ await new Promise((r) =>
254
+ setTimeout(r, SLACK_DEFAULT_RETRY_AFTER_S * 1000),
255
+ );
256
+ continue;
257
+ }
258
+
259
+ throw new SlackApiError(data.error);
260
+ }
261
+
262
+ return data;
263
+ }
264
+
265
+ throw new Error(
266
+ `Slack ${method} failed after retries: ${lastError ?? "unknown"}`,
267
+ );
268
+ }
269
+
270
+ function normalizeSlackString(value: unknown): string | undefined {
271
+ if (typeof value !== "string") return undefined;
272
+ const trimmed = value.trim();
273
+ return trimmed || undefined;
274
+ }
275
+
276
+ export async function getSlackConversationInfo(
277
+ channelId: string,
278
+ ): Promise<SlackConversationInfo | null> {
279
+ const data = (await callSlackApiGet(
280
+ "conversations.info",
281
+ new URLSearchParams({ channel: channelId }),
282
+ )) as SlackConversationsInfoResponse;
283
+
284
+ const id = normalizeSlackString(data.channel?.id);
285
+ if (!id) return null;
286
+
287
+ const name = normalizeSlackString(data.channel?.name);
288
+ const nameNormalized = normalizeSlackString(data.channel?.name_normalized);
289
+
290
+ return {
291
+ id,
292
+ ...(name ? { name } : {}),
293
+ ...(nameNormalized ? { nameNormalized } : {}),
294
+ };
295
+ }
296
+
181
297
  /**
182
298
  * Call a Slack Web API method with form-urlencoded body.
183
299
  */
@@ -14,6 +14,7 @@ import type { OAuthConnection } from "../../../oauth/connection.js";
14
14
  import type {
15
15
  SlackApiResponse,
16
16
  SlackAuthTestResponse,
17
+ SlackBotsInfoResponse,
17
18
  SlackConversationHistoryResponse,
18
19
  SlackConversationMarkResponse,
19
20
  SlackConversationRepliesResponse,
@@ -286,6 +287,17 @@ export async function authTest(
286
287
  return request<SlackAuthTestResponse>(connectionOrToken, "auth.test");
287
288
  }
288
289
 
290
+ export async function botsInfo(
291
+ connectionOrToken: OAuthConnection | string,
292
+ botId: string,
293
+ teamId?: string,
294
+ ): Promise<SlackBotsInfoResponse> {
295
+ return request<SlackBotsInfoResponse>(connectionOrToken, "bots.info", {
296
+ bot: botId,
297
+ team_id: teamId,
298
+ });
299
+ }
300
+
289
301
  export async function listConversations(
290
302
  connectionOrToken: OAuthConnection | string,
291
303
  types = "public_channel,private_channel,mpim,im",
@@ -40,13 +40,31 @@ export function buildSlackWebMessageUrl(params: {
40
40
  teamUrl?: string | null;
41
41
  channelId: string;
42
42
  messageTs: string;
43
+ threadTs?: string;
43
44
  }): string | undefined {
44
45
  const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
45
46
  if (!teamUrl) return undefined;
46
47
 
47
- return `${teamUrl}/archives/${encodeURIComponent(
48
+ const baseUrl = `${teamUrl}/archives/${encodeURIComponent(
48
49
  params.channelId,
49
50
  )}/p${formatSlackPermalinkTimestamp(params.messageTs)}`;
51
+ if (!params.threadTs) return baseUrl;
52
+
53
+ const search = new URLSearchParams({
54
+ thread_ts: params.threadTs,
55
+ cid: params.channelId,
56
+ });
57
+ return `${baseUrl}?${search.toString()}`;
58
+ }
59
+
60
+ export function buildSlackWebChannelUrl(params: {
61
+ teamUrl?: string | null;
62
+ channelId: string;
63
+ }): string | undefined {
64
+ const teamUrl = normalizeSlackTeamUrl(params.teamUrl);
65
+ if (!teamUrl) return undefined;
66
+
67
+ return `${teamUrl}/archives/${encodeURIComponent(params.channelId)}`;
50
68
  }
51
69
 
52
70
  export function buildSlackMessageDeepLinks(params: {
@@ -54,6 +72,7 @@ export function buildSlackMessageDeepLinks(params: {
54
72
  teamUrl?: string | null;
55
73
  channelId: string;
56
74
  messageTs: string;
75
+ threadTs?: string;
57
76
  }): SlackMessageDeepLinks | undefined {
58
77
  const appUrl = buildSlackAppMessageUrl(params);
59
78
  const webUrl = buildSlackWebMessageUrl(params);
@@ -1,6 +1,8 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
3
  import {
4
+ buildSlackTimezoneMetadata,
5
+ formatSlackTimezoneLabel,
4
6
  mergeSlackMetadata,
5
7
  readSlackMetadata,
6
8
  readSlackMetadataFromMessageMetadata,
@@ -8,6 +10,42 @@ import {
8
10
  writeSlackMetadata,
9
11
  } from "./message-metadata.js";
10
12
 
13
+ describe("formatSlackTimezoneLabel", () => {
14
+ test("uses compact common labels for IANA timezones", () => {
15
+ expect(formatSlackTimezoneLabel("America/Denver")).toBe("MT");
16
+ expect(formatSlackTimezoneLabel("America/New_York")).toBe("ET");
17
+ });
18
+
19
+ test("compacts persisted Slack profile labels before falling back to timezone", () => {
20
+ expect(
21
+ formatSlackTimezoneLabel("America/New_York", {
22
+ persistedLabel: "Eastern Time",
23
+ }),
24
+ ).toBe("ET");
25
+ });
26
+ });
27
+
28
+ describe("buildSlackTimezoneMetadata", () => {
29
+ test("copies only populated Slack timezone fields", () => {
30
+ expect(
31
+ buildSlackTimezoneMetadata({
32
+ actorTimezone: " America/New_York ",
33
+ actorTimezoneLabel: " ET ",
34
+ actorTimezoneOffsetSeconds: -18_000,
35
+ timestampTimezone: "America/New_York",
36
+ timestampTimezoneLabel: "",
37
+ speakerTimezoneLabel: " Eastern Time ",
38
+ }),
39
+ ).toEqual({
40
+ actorTimezone: "America/New_York",
41
+ actorTimezoneLabel: "ET",
42
+ actorTimezoneOffsetSeconds: -18_000,
43
+ timestampTimezone: "America/New_York",
44
+ speakerTimezoneLabel: "Eastern Time",
45
+ });
46
+ });
47
+ });
48
+
11
49
  describe("readSlackMetadata", () => {
12
50
  test("tolerates null and undefined", () => {
13
51
  expect(readSlackMetadata(null)).toBeNull();
@@ -62,9 +100,17 @@ describe("readSlackMetadata", () => {
62
100
  eventKind: "message",
63
101
  editedAt: "not-a-number",
64
102
  });
103
+ const badChannelName = JSON.stringify({
104
+ source: "slack",
105
+ channelId: "C123",
106
+ channelName: 42,
107
+ channelTs: "1700000000.000100",
108
+ eventKind: "message",
109
+ });
65
110
  expect(readSlackMetadata(badThreadTs)).toBeNull();
66
111
  expect(readSlackMetadata(badReactionOp)).toBeNull();
67
112
  expect(readSlackMetadata(badEditedAt)).toBeNull();
113
+ expect(readSlackMetadata(badChannelName)).toBeNull();
68
114
  });
69
115
 
70
116
  test("strips unknown top-level keys from the returned object", () => {
@@ -111,6 +157,7 @@ describe("readSlackMetadata", () => {
111
157
  const meta: SlackMessageMetadata = {
112
158
  source: "slack",
113
159
  channelId: "C123",
160
+ channelName: "engineering",
114
161
  channelTs: "1700000000.000100",
115
162
  threadTs: "1699999999.000000",
116
163
  displayName: "Alice",
@@ -174,6 +221,7 @@ describe("writeSlackMetadata", () => {
174
221
  const meta: SlackMessageMetadata = {
175
222
  source: "slack",
176
223
  channelId: "C123",
224
+ channelName: "engineering",
177
225
  channelTs: "1700000000.000100",
178
226
  threadTs: "1699999999.000000",
179
227
  displayName: "Alice",