@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
@@ -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" }),
@@ -51,6 +51,9 @@ let lastCheckArgs:
51
51
  /** Optional override for getTool — lets tests supply skill-origin tools. */
52
52
  let getToolOverride: ((name: string) => Tool | undefined) | undefined;
53
53
 
54
+ /** Optional override for getAllTools — lets tests supply a registry snapshot. */
55
+ let getAllToolsOverride: (() => Tool[]) | undefined;
56
+
54
57
  /** Override the check() result for tests that need to trigger prompting. */
55
58
  let checkResultOverride: { decision: string; reason: string } | undefined;
56
59
 
@@ -144,7 +147,7 @@ mock.module("../tools/registry.js", () => ({
144
147
  execute: async () => fakeToolResult,
145
148
  };
146
149
  },
147
- getAllTools: () => [],
150
+ getAllTools: () => (getAllToolsOverride ? getAllToolsOverride() : []),
148
151
  }));
149
152
 
150
153
  mock.module("../tools/shared/filesystem/path-policy.js", () => ({
@@ -179,6 +182,7 @@ describe("ToolExecutor allowedToolNames gating", () => {
179
182
  fakeToolResult = { content: "ok", isError: false };
180
183
  lastCheckArgs = undefined;
181
184
  getToolOverride = undefined;
185
+ getAllToolsOverride = undefined;
182
186
  checkResultOverride = undefined;
183
187
  checkFnOverride = undefined;
184
188
  cachedAssessmentOverride = undefined;
@@ -271,6 +275,89 @@ describe("ToolExecutor allowedToolNames gating", () => {
271
275
  expect(result.content).toContain("file_read");
272
276
  expect(result.content).toContain("not currently active");
273
277
  });
278
+
279
+ test("unknown tool suggestion list is scoped to allowedToolNames", async () => {
280
+ // Surfacing every globally registered tool would leak tools active in
281
+ // other sessions and misdirect the model to tools it cannot invoke.
282
+ const makeTool = (name: string): Tool =>
283
+ ({
284
+ name,
285
+ description: "test tool",
286
+ category: "test",
287
+ defaultRiskLevel: RiskLevel.Low,
288
+ getDefinition: () => ({
289
+ name,
290
+ description: "test tool",
291
+ input_schema: { type: "object" as const, properties: {} },
292
+ }),
293
+ execute: async () => fakeToolResult,
294
+ }) as unknown as Tool;
295
+ getAllToolsOverride = () => [
296
+ makeTool("file_read"),
297
+ makeTool("file_write"),
298
+ makeTool("secret_skill_tool"),
299
+ ];
300
+ const executor = new ToolExecutor(makePrompter());
301
+ const allowed = new Set(["file_read", "file_write"]);
302
+ const result = await executor.execute(
303
+ "unknown_tool",
304
+ { foo: "bar" },
305
+ makeContext({ allowedToolNames: allowed }),
306
+ );
307
+ expect(result.isError).toBe(true);
308
+ expect(result.content).toBe(
309
+ "Unknown tool: unknown_tool. Available tools: file_read, file_write",
310
+ );
311
+ expect(result.content).not.toContain("secret_skill_tool");
312
+ });
313
+
314
+ test("unknown tool name reports 'Unknown tool' even when allowedToolNames is set", async () => {
315
+ // Regression: a hallucinated tool name with allowedToolNames set used to
316
+ // hit the "not currently active. Load the skill that provides this tool
317
+ // first." gate, which sent the model chasing a nonexistent skill. The
318
+ // registry-lookup gate runs first now so the model sees the real list.
319
+ const executor = new ToolExecutor(makePrompter());
320
+ const allowed = new Set(["file_read"]);
321
+ const result = await executor.execute(
322
+ "unknown_tool",
323
+ { foo: "bar" },
324
+ makeContext({ allowedToolNames: allowed }),
325
+ );
326
+ expect(result.isError).toBe(true);
327
+ expect(result.content).toContain("Unknown tool: unknown_tool");
328
+ expect(result.content).not.toContain("not currently active");
329
+ });
330
+
331
+ test("inactive skill tool names the owning skill in the load hint", async () => {
332
+ const executor = new ToolExecutor(makePrompter());
333
+ getToolOverride = (name: string) => {
334
+ if (name !== "skill_tool_x") return undefined;
335
+ return {
336
+ name,
337
+ description: "tool from a skill",
338
+ category: "skill",
339
+ defaultRiskLevel: RiskLevel.Low,
340
+ origin: "skill" as const,
341
+ ownerSkillId: "my-skill",
342
+ getDefinition: () => ({
343
+ name,
344
+ description: "tool from a skill",
345
+ input_schema: { type: "object" as const, properties: {} },
346
+ }),
347
+ execute: async () => fakeToolResult,
348
+ };
349
+ };
350
+ const allowed = new Set(["file_read"]);
351
+ const result = await executor.execute(
352
+ "skill_tool_x",
353
+ {},
354
+ makeContext({ allowedToolNames: allowed }),
355
+ );
356
+ expect(result.isError).toBe(true);
357
+ expect(result.content).toBe(
358
+ 'Tool "skill_tool_x" is not currently active. Load the "my-skill" skill that provides this tool first.',
359
+ );
360
+ });
274
361
  });
275
362
 
276
363
  describe("ToolExecutor policy context plumbing", () => {
@@ -846,6 +933,7 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
846
933
  // Import the real buildSanitizedEnv (not mocked) for baseline credential tests
847
934
  const {
848
935
  buildSanitizedEnv,
936
+ KATA_INJECTED_ENV_VARS,
849
937
  KATA_SAFE_ENV_VARS,
850
938
  SAFE_ENV_VARS,
851
939
  ALWAYS_INJECTED_ENV_VARS,
@@ -910,6 +998,7 @@ describe("buildSanitizedEnv — baseline: credential exclusion", () => {
910
998
  const allowed: string[] = [
911
999
  ...SAFE_ENV_VARS,
912
1000
  ...KATA_SAFE_ENV_VARS,
1001
+ ...KATA_INJECTED_ENV_VARS,
913
1002
  ...ALWAYS_INJECTED_ENV_VARS,
914
1003
  ];
915
1004
  const env = buildSanitizedEnv();