@vellumai/assistant 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -1,8 +1,14 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import {
3
+ existsSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
3
9
  import { tmpdir } from "node:os";
4
10
  import { join } from "node:path";
5
- import { describe, expect, mock, test } from "bun:test";
11
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
6
12
 
7
13
  mock.module("../util/logger.js", () => ({
8
14
  getLogger: () =>
@@ -18,12 +24,23 @@ mock.module("../util/secure-keys.js", () => ({
18
24
  import { getDb } from "../memory/db-connection.js";
19
25
  import { initializeDb } from "../memory/db-init.js";
20
26
  import { llmUsageEvents } from "../memory/schema.js";
27
+ import { assistantEventHub } from "../runtime/assistant-event-hub.js";
21
28
  import { ROUTES } from "../runtime/routes/log-export-routes.js";
22
29
 
23
30
  initializeDb();
24
31
 
25
32
  const exportRoute = ROUTES.find((r) => r.endpoint === "export")!;
26
33
 
34
+ function clearConnectedClients(): void {
35
+ for (const client of assistantEventHub.listClients()) {
36
+ assistantEventHub.disposeClient(client.clientId);
37
+ }
38
+ }
39
+
40
+ beforeEach(() => {
41
+ clearConnectedClients();
42
+ });
43
+
27
44
  async function extractArchive(bytes: Uint8Array): Promise<string> {
28
45
  const extractDir = mkdtempSync(join(tmpdir(), "log-export-routes-"));
29
46
  const archivePath = join(extractDir, "archive.tar.gz");
@@ -39,6 +56,86 @@ async function extractArchive(bytes: Uint8Array): Promise<string> {
39
56
  return extractDir;
40
57
  }
41
58
 
59
+ describe("POST /v1/export - connected clients", () => {
60
+ test("includes current `assistant clients list --json` output", async () => {
61
+ const subscription = assistantEventHub.subscribe({
62
+ type: "client",
63
+ clientId: "client-list-export-test",
64
+ interfaceId: "macos",
65
+ capabilities: ["host_bash", "host_file"],
66
+ machineName: "test-macbook",
67
+ callback: () => {},
68
+ });
69
+
70
+ try {
71
+ const result = await exportRoute.handler({ body: {} });
72
+ expect(result).toBeInstanceOf(Uint8Array);
73
+
74
+ const dir = await extractArchive(result as Uint8Array);
75
+ try {
76
+ const clientsList = JSON.parse(
77
+ readFileSync(join(dir, "clients-list.json"), "utf-8"),
78
+ ) as {
79
+ clients: Array<{
80
+ clientId: string;
81
+ interfaceId: string;
82
+ capabilities: string[];
83
+ machineName?: string;
84
+ connectedAt: string;
85
+ lastActiveAt: string;
86
+ }>;
87
+ };
88
+
89
+ expect(clientsList.clients).toHaveLength(1);
90
+ expect(clientsList.clients[0]).toMatchObject({
91
+ clientId: "client-list-export-test",
92
+ interfaceId: "macos",
93
+ capabilities: ["host_bash", "host_file"],
94
+ machineName: "test-macbook",
95
+ });
96
+ expect(new Date(clientsList.clients[0].connectedAt).toISOString()).toBe(
97
+ clientsList.clients[0].connectedAt,
98
+ );
99
+ expect(
100
+ new Date(clientsList.clients[0].lastActiveAt).toISOString(),
101
+ ).toBe(clientsList.clients[0].lastActiveAt);
102
+ } finally {
103
+ rmSync(dir, { recursive: true, force: true });
104
+ }
105
+ } finally {
106
+ subscription.dispose();
107
+ }
108
+ });
109
+
110
+ test("writes a non-blocking error artifact when client listing fails", async () => {
111
+ const originalListClients = assistantEventHub.listClients;
112
+ assistantEventHub.listClients = () => {
113
+ throw new Error("client list unavailable");
114
+ };
115
+
116
+ try {
117
+ const result = await exportRoute.handler({ body: {} });
118
+ expect(result).toBeInstanceOf(Uint8Array);
119
+
120
+ const dir = await extractArchive(result as Uint8Array);
121
+ try {
122
+ expect(existsSync(join(dir, "clients-list.json"))).toBe(false);
123
+ const errorArtifact = JSON.parse(
124
+ readFileSync(join(dir, "clients-list-error.json"), "utf-8"),
125
+ ) as { error: string; collectedAt: string };
126
+ expect(errorArtifact.error).toBe("client list unavailable");
127
+ expect(new Date(errorArtifact.collectedAt).toISOString()).toBe(
128
+ errorArtifact.collectedAt,
129
+ );
130
+ } finally {
131
+ rmSync(dir, { recursive: true, force: true });
132
+ }
133
+ } finally {
134
+ assistantEventHub.listClients = originalListClients;
135
+ }
136
+ });
137
+ });
138
+
42
139
  describe("POST /v1/export - LLM usage events", () => {
43
140
  test("full export includes usage attribution columns", async () => {
44
141
  const db = getDb();
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ MessageQueue,
5
+ type QueuedMessage,
6
+ } from "../daemon/conversation-queue-manager.js";
7
+
8
+ function makeItem(content: string, requestId: string): QueuedMessage {
9
+ return {
10
+ content,
11
+ attachments: [],
12
+ requestId,
13
+ onEvent: () => {},
14
+ sentAt: Date.now(),
15
+ };
16
+ }
17
+
18
+ describe("MessageQueue.promoteToHead", () => {
19
+ test("returns undefined when queue is empty", () => {
20
+ const q = new MessageQueue();
21
+ expect(q.promoteToHead("nonexistent")).toBeUndefined();
22
+ });
23
+
24
+ test("returns undefined when requestId is not found", () => {
25
+ const q = new MessageQueue();
26
+ q.push(makeItem("a", "r1"));
27
+ q.push(makeItem("b", "r2"));
28
+ expect(q.promoteToHead("r99")).toBeUndefined();
29
+ // Queue unchanged
30
+ expect(q.peek(0)?.requestId).toBe("r1");
31
+ expect(q.peek(1)?.requestId).toBe("r2");
32
+ });
33
+
34
+ test("no-op when the message is already at head", () => {
35
+ const q = new MessageQueue();
36
+ q.push(makeItem("a", "r1"));
37
+ q.push(makeItem("b", "r2"));
38
+
39
+ const bytesBefore = q.totalBytes;
40
+ const result = q.promoteToHead("r1");
41
+
42
+ expect(result).toBeDefined();
43
+ expect(result?.requestId).toBe("r1");
44
+ expect(q.peek(0)?.requestId).toBe("r1");
45
+ expect(q.peek(1)?.requestId).toBe("r2");
46
+ expect(q.length).toBe(2);
47
+ expect(q.totalBytes).toBe(bytesBefore);
48
+ });
49
+
50
+ test("moves a middle item to head", () => {
51
+ const q = new MessageQueue();
52
+ q.push(makeItem("a", "r1"));
53
+ q.push(makeItem("b", "r2"));
54
+ q.push(makeItem("c", "r3"));
55
+
56
+ const bytesBefore = q.totalBytes;
57
+ const result = q.promoteToHead("r2");
58
+
59
+ expect(result).toBeDefined();
60
+ expect(result?.requestId).toBe("r2");
61
+ expect(q.peek(0)?.requestId).toBe("r2");
62
+ expect(q.peek(1)?.requestId).toBe("r1");
63
+ expect(q.peek(2)?.requestId).toBe("r3");
64
+ expect(q.length).toBe(3);
65
+ expect(q.totalBytes).toBe(bytesBefore);
66
+ });
67
+
68
+ test("moves the last item to head", () => {
69
+ const q = new MessageQueue();
70
+ q.push(makeItem("a", "r1"));
71
+ q.push(makeItem("b", "r2"));
72
+ q.push(makeItem("c", "r3"));
73
+
74
+ const bytesBefore = q.totalBytes;
75
+ const result = q.promoteToHead("r3");
76
+
77
+ expect(result).toBeDefined();
78
+ expect(result?.requestId).toBe("r3");
79
+ expect(q.peek(0)?.requestId).toBe("r3");
80
+ expect(q.peek(1)?.requestId).toBe("r1");
81
+ expect(q.peek(2)?.requestId).toBe("r2");
82
+ expect(q.length).toBe(3);
83
+ expect(q.totalBytes).toBe(bytesBefore);
84
+ });
85
+
86
+ test("byte accounting unchanged after promote (item is reordered, not added/removed)", () => {
87
+ const q = new MessageQueue(10_000);
88
+ q.push(makeItem("short", "r1"));
89
+ q.push(makeItem("a".repeat(200), "r2"));
90
+ q.push(makeItem("medium text", "r3"));
91
+
92
+ const bytesBefore = q.totalBytes;
93
+ q.promoteToHead("r2");
94
+ expect(q.totalBytes).toBe(bytesBefore);
95
+
96
+ // shift all and verify bytes go to 0
97
+ q.shift();
98
+ q.shift();
99
+ q.shift();
100
+ expect(q.totalBytes).toBe(0);
101
+ });
102
+
103
+ test("promoted item is returned by shift()", () => {
104
+ const q = new MessageQueue();
105
+ q.push(makeItem("a", "r1"));
106
+ q.push(makeItem("b", "r2"));
107
+ q.push(makeItem("c", "r3"));
108
+
109
+ q.promoteToHead("r3");
110
+ const head = q.shift();
111
+ expect(head?.requestId).toBe("r3");
112
+ expect(head?.content).toBe("c");
113
+ });
114
+ });
@@ -1744,4 +1744,109 @@ describe("OpenAIProvider reasoning_effort", () => {
1744
1744
  });
1745
1745
  expect(lastCreateParams!.reasoning_effort).toBe("medium");
1746
1746
  });
1747
+
1748
+ test('maxReasoningEffort: "max" passes "max" through unclamped', async () => {
1749
+ const uncapped = new OpenAIProvider("test-key", "gpt-5", {
1750
+ maxReasoningEffort: "max",
1751
+ });
1752
+ await uncapped.sendMessage([userMsg("hi")], undefined, "system", {
1753
+ config: { effort: "max" },
1754
+ });
1755
+ expect(lastCreateParams!.reasoning_effort).toBe("max");
1756
+ await uncapped.sendMessage([userMsg("hi")], undefined, "system", {
1757
+ config: { effort: "xhigh" },
1758
+ });
1759
+ expect(lastCreateParams!.reasoning_effort).toBe("xhigh");
1760
+ });
1761
+ });
1762
+
1763
+ describe("FireworksProvider reasoning_effort ceiling", () => {
1764
+ beforeEach(() => {
1765
+ fakeChunks = [];
1766
+ lastCreateParams = null;
1767
+ });
1768
+
1769
+ test('DeepSeek V4 Pro accepts "max" unclamped', async () => {
1770
+ const fw = new FireworksProvider(
1771
+ "fw-key",
1772
+ "accounts/fireworks/models/deepseek-v4-pro",
1773
+ );
1774
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1775
+ config: { effort: "max" },
1776
+ });
1777
+ expect(lastCreateParams!.reasoning_effort).toBe("max");
1778
+ });
1779
+
1780
+ test('DeepSeek V4 Pro accepts "xhigh" unclamped', async () => {
1781
+ const fw = new FireworksProvider(
1782
+ "fw-key",
1783
+ "accounts/fireworks/models/deepseek-v4-pro",
1784
+ );
1785
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1786
+ config: { effort: "xhigh" },
1787
+ });
1788
+ expect(lastCreateParams!.reasoning_effort).toBe("xhigh");
1789
+ });
1790
+
1791
+ test('Kimi K2.6 clamps "xhigh"/"max" down to "high"', async () => {
1792
+ const fw = new FireworksProvider(
1793
+ "fw-key",
1794
+ "accounts/fireworks/models/kimi-k2p6",
1795
+ );
1796
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1797
+ config: { effort: "xhigh" },
1798
+ });
1799
+ expect(lastCreateParams!.reasoning_effort).toBe("high");
1800
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1801
+ config: { effort: "max" },
1802
+ });
1803
+ expect(lastCreateParams!.reasoning_effort).toBe("high");
1804
+ });
1805
+
1806
+ test('unknown Fireworks model falls back to "high" ceiling', async () => {
1807
+ const fw = new FireworksProvider(
1808
+ "fw-key",
1809
+ "accounts/fireworks/models/some-future-model",
1810
+ );
1811
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1812
+ config: { effort: "max" },
1813
+ });
1814
+ expect(lastCreateParams!.reasoning_effort).toBe("high");
1815
+ });
1816
+
1817
+ test("effort below ceiling is forwarded verbatim", async () => {
1818
+ const fw = new FireworksProvider(
1819
+ "fw-key",
1820
+ "accounts/fireworks/models/deepseek-v4-pro",
1821
+ );
1822
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1823
+ config: { effort: "medium" },
1824
+ });
1825
+ expect(lastCreateParams!.reasoning_effort).toBe("medium");
1826
+ });
1827
+
1828
+ test('effort: "none" passes through regardless of ceiling', async () => {
1829
+ const fw = new FireworksProvider(
1830
+ "fw-key",
1831
+ "accounts/fireworks/models/kimi-k2p6",
1832
+ );
1833
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1834
+ config: { effort: "none" },
1835
+ });
1836
+ expect(lastCreateParams!.reasoning_effort).toBe("none");
1837
+ });
1838
+
1839
+ test("model override picks ceiling from runtime model, not constructor model", async () => {
1840
+ const fw = new FireworksProvider(
1841
+ "fw-key",
1842
+ "accounts/fireworks/models/kimi-k2p6",
1843
+ );
1844
+ await fw.sendMessage([userMsg("hi")], undefined, "system", {
1845
+ config: {
1846
+ effort: "max",
1847
+ model: "accounts/fireworks/models/deepseek-v4-pro",
1848
+ },
1849
+ });
1850
+ expect(lastCreateParams!.reasoning_effort).toBe("max");
1851
+ });
1747
1852
  });
@@ -714,16 +714,16 @@ describe("OpenAIResponsesProvider", () => {
714
714
  });
715
715
 
716
716
  // -----------------------------------------------------------------------
717
- // store: false
717
+ // store — omitted so prompt caching can persist across turns
718
718
  // -----------------------------------------------------------------------
719
- test("sends store: false in params", async () => {
719
+ test("does not set store in params (defaults to true server-side)", async () => {
720
720
  fakeStreamEvents = [textDeltaEvent("OK"), completedEvent(10, 2)];
721
721
 
722
722
  await provider.sendMessage([
723
723
  { role: "user", content: [{ type: "text", text: "Hi" }] },
724
724
  ]);
725
725
 
726
- expect(lastStreamParams!.store).toBe(false);
726
+ expect(lastStreamParams!.store).toBeUndefined();
727
727
  });
728
728
 
729
729
  // -----------------------------------------------------------------------
@@ -1122,7 +1122,7 @@ describe("OpenAIResponsesProvider", () => {
1122
1122
  const rawReq = result.rawRequest as Record<string, unknown>;
1123
1123
  expect(rawReq.model).toBe("gpt-5.2");
1124
1124
  expect(rawReq.instructions).toBe("System prompt");
1125
- expect(rawReq.store).toBe(false);
1125
+ expect(rawReq.store).toBeUndefined();
1126
1126
 
1127
1127
  // rawResponse should contain the final response object, including `output`
1128
1128
  // which downstream normalization relies on for Responses API detection.
@@ -22,24 +22,33 @@ mock.module("../util/logger.js", () => ({
22
22
  }),
23
23
  }));
24
24
 
25
- mock.module("../config/loader.js", () => ({
26
- getConfig: () => ({
27
- skills: {
28
- entries: {},
29
- load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
30
- install: { nodeManager: "npm" },
31
- allowBundled: null,
32
- remoteProviders: {
33
- skillssh: { enabled: true },
34
- clawhub: { enabled: true },
35
- },
36
- remotePolicy: {
37
- blockSuspicious: true,
38
- blockMalware: true,
39
- maxSkillsShRisk: "medium",
40
- },
25
+ type TestConfig = {
26
+ ui: { userTimezone?: string; detectedTimezone?: string };
27
+ skills: Record<string, unknown>;
28
+ };
29
+
30
+ const baseConfig: TestConfig = {
31
+ ui: {},
32
+ skills: {
33
+ entries: {},
34
+ load: { extraDirs: [], watch: true, watchDebounceMs: 250 },
35
+ install: { nodeManager: "npm" },
36
+ allowBundled: null,
37
+ remoteProviders: {
38
+ skillssh: { enabled: true },
39
+ clawhub: { enabled: true },
41
40
  },
42
- }),
41
+ remotePolicy: {
42
+ blockSuspicious: true,
43
+ blockMalware: true,
44
+ maxSkillsShRisk: "medium",
45
+ },
46
+ },
47
+ };
48
+ let currentConfig: TestConfig = structuredClone(baseConfig);
49
+
50
+ mock.module("../config/loader.js", () => ({
51
+ getConfig: () => currentConfig,
43
52
  loadConfig: () => ({}),
44
53
  }));
45
54
 
@@ -47,12 +56,21 @@ mock.module("../config/loader.js", () => ({
47
56
  // its arguments per test invocation so each case can assert on the metadata
48
57
  // that was actually persisted.
49
58
  interface AddMessageCall {
59
+ id: string;
50
60
  conversationId: string;
51
61
  role: string;
52
62
  content: string;
53
63
  metadata?: Record<string, unknown>;
54
64
  }
55
65
  const addMessageCalls: AddMessageCall[] = [];
66
+ const persistedRows: Array<{
67
+ id: string;
68
+ conversationId: string;
69
+ role: string;
70
+ content: string;
71
+ createdAt: number;
72
+ metadata: string | null;
73
+ }> = [];
56
74
  mock.module("../memory/conversation-crud.js", () => ({
57
75
  addMessage: (
58
76
  conversationId: string,
@@ -60,11 +78,35 @@ mock.module("../memory/conversation-crud.js", () => ({
60
78
  content: string,
61
79
  metadata?: Record<string, unknown>,
62
80
  ) => {
63
- addMessageCalls.push({ conversationId, role, content, metadata });
64
- return { id: `mock-msg-${addMessageCalls.length}` };
81
+ const id = `mock-msg-${addMessageCalls.length + 1}`;
82
+ addMessageCalls.push({ id, conversationId, role, content, metadata });
83
+ persistedRows.push({
84
+ id,
85
+ conversationId,
86
+ role,
87
+ content,
88
+ createdAt: Date.now(),
89
+ metadata: metadata ? JSON.stringify(metadata) : null,
90
+ });
91
+ return { id };
65
92
  },
66
93
  getConversation: () => null,
67
- getMessageById: () => null,
94
+ getMessageById: (messageId: string) =>
95
+ persistedRows.find((row) => row.id === messageId) ?? null,
96
+ getMessages: (conversationId: string) =>
97
+ persistedRows.filter((row) => row.conversationId === conversationId),
98
+ updateMessageMetadata: (
99
+ messageId: string,
100
+ updates: Record<string, unknown>,
101
+ ) => {
102
+ const row = persistedRows.find((candidate) => candidate.id === messageId);
103
+ if (!row) return;
104
+ const existing =
105
+ row.metadata && typeof row.metadata === "string"
106
+ ? (JSON.parse(row.metadata) as Record<string, unknown>)
107
+ : {};
108
+ row.metadata = JSON.stringify({ ...existing, ...updates });
109
+ },
68
110
  updateMessageContent: () => {},
69
111
  // The handler treats provenance as a flat spread; returning {} keeps the
70
112
  // metadata snapshot focused on the fields under test.
@@ -84,6 +126,18 @@ mock.module("../memory/conversation-disk-view.js", () => ({
84
126
  syncMessageToDisk: () => {},
85
127
  }));
86
128
 
129
+ let nextDeliveryTs: string | null = null;
130
+ mock.module("../runtime/gateway-client.js", () => ({
131
+ deliverChannelReply: async () => ({
132
+ ok: true,
133
+ ...(nextDeliveryTs ? { ts: nextDeliveryTs } : {}),
134
+ }),
135
+ }));
136
+
137
+ mock.module("../memory/attachments-store.js", () => ({
138
+ getAttachmentMetadataForMessage: () => [],
139
+ }));
140
+
87
141
  // ── Imports (after mocks) ──────────────────────────────────────────────────
88
142
 
89
143
  import type { AgentEvent } from "../agent/loop.js";
@@ -98,6 +152,7 @@ import {
98
152
  import type { ServerMessage } from "../daemon/message-protocol.js";
99
153
  import { clearThreadTs, setThreadTs } from "../memory/slack-thread-store.js";
100
154
  import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
155
+ import { deliverReplyViaCallback } from "../runtime/channel-reply-delivery.js";
101
156
 
102
157
  // ── Helpers ────────────────────────────────────────────────────────────────
103
158
 
@@ -106,6 +161,8 @@ function makeDeps(
106
161
  overrides: {
107
162
  assistantMessageChannel?: "slack" | "vellum" | "telegram";
108
163
  requesterChatId?: string;
164
+ requesterTimezoneLabel?: string;
165
+ clientTimezone?: string;
109
166
  } = {},
110
167
  ): EventHandlerDeps {
111
168
  const assistantMessageChannel = overrides.assistantMessageChannel ?? "slack";
@@ -119,7 +176,9 @@ function makeDeps(
119
176
  sourceChannel: assistantMessageChannel,
120
177
  trustClass: "guardian",
121
178
  requesterChatId: overrides.requesterChatId,
179
+ requesterTimezoneLabel: overrides.requesterTimezoneLabel,
122
180
  },
181
+ clientTimezone: overrides.clientTimezone,
123
182
  } as unknown as EventHandlerDeps["ctx"],
124
183
  onEvent: (_msg: ServerMessage) => {},
125
184
  reqId: "test-req-id",
@@ -172,12 +231,17 @@ describe("outbound assistant Slack metadata persistence", () => {
172
231
 
173
232
  beforeEach(() => {
174
233
  addMessageCalls.length = 0;
234
+ persistedRows.length = 0;
235
+ currentConfig = structuredClone(baseConfig);
236
+ nextDeliveryTs = null;
175
237
  state = createEventHandlerState();
176
238
  state.turnStartedAt = 1_700_000_000_000;
177
239
  });
178
240
 
179
241
  afterEach(() => {
180
242
  addMessageCalls.length = 0;
243
+ persistedRows.length = 0;
244
+ nextDeliveryTs = null;
181
245
  });
182
246
 
183
247
  test("stamps slackMeta with threadTs when the conversation has a Slack thread mapping", async () => {
@@ -217,6 +281,109 @@ describe("outbound assistant Slack metadata persistence", () => {
217
281
  expect(readSlackMetadata(slackMetaRaw as string)).toBeNull();
218
282
  });
219
283
 
284
+ test("stamps assistant Slack rows with effective timestamp timezone and no speaker suffix", async () => {
285
+ currentConfig = {
286
+ ...structuredClone(baseConfig),
287
+ ui: { userTimezone: "America/Denver" },
288
+ };
289
+ state.turnStartedAt = Date.parse("2026-03-05T03:38:00Z");
290
+ const conversationId = "conv-slack-timezone";
291
+ const channelId = "C999TIMEZONE";
292
+
293
+ const deps = makeDeps(conversationId, {
294
+ assistantMessageChannel: "slack",
295
+ requesterChatId: channelId,
296
+ requesterTimezoneLabel: "ET",
297
+ clientTimezone: "America/Los_Angeles",
298
+ });
299
+ await handleMessageComplete(
300
+ state,
301
+ deps,
302
+ makeMessageCompleteEvent("timezone-aware reply"),
303
+ );
304
+
305
+ const persisted = lastAssistantPersisted();
306
+ const slackMetaRaw = persisted.metadata?.slackMeta;
307
+ expect(typeof slackMetaRaw).toBe("string");
308
+
309
+ const slackMeta = JSON.parse(slackMetaRaw as string) as Record<
310
+ string,
311
+ unknown
312
+ >;
313
+ expect(slackMeta.timestampTimezone).toBe("America/Denver");
314
+ expect(slackMeta.timestampTimezoneLabel).toBe("MT");
315
+ expect(slackMeta.speakerTimezoneLabel).toBeUndefined();
316
+ });
317
+
318
+ test("falls back to the turn client timezone when no configured user timezone is set", async () => {
319
+ const conversationId = "conv-slack-client-timezone";
320
+ const channelId = "C999CLIENTTZ";
321
+
322
+ const deps = makeDeps(conversationId, {
323
+ assistantMessageChannel: "slack",
324
+ requesterChatId: channelId,
325
+ clientTimezone: "America/Los_Angeles",
326
+ });
327
+ await handleMessageComplete(
328
+ state,
329
+ deps,
330
+ makeMessageCompleteEvent("client timezone reply"),
331
+ );
332
+
333
+ const persisted = lastAssistantPersisted();
334
+ const slackMeta = JSON.parse(
335
+ persisted.metadata?.slackMeta as string,
336
+ ) as Record<string, unknown>;
337
+ expect(slackMeta.timestampTimezone).toBe("America/Los_Angeles");
338
+ expect(slackMeta.timestampTimezoneLabel).toBe("PT");
339
+ });
340
+
341
+ test("post-send reconciliation preserves assistant Slack timezone metadata", async () => {
342
+ currentConfig = {
343
+ ...structuredClone(baseConfig),
344
+ ui: { userTimezone: "America/Denver" },
345
+ };
346
+ const conversationId = "conv-slack-reconcile-timezone";
347
+ const channelId = "C999RECONCILE";
348
+
349
+ const deps = makeDeps(conversationId, {
350
+ assistantMessageChannel: "slack",
351
+ requesterChatId: channelId,
352
+ requesterTimezoneLabel: "ET",
353
+ });
354
+ await handleMessageComplete(
355
+ state,
356
+ deps,
357
+ makeMessageCompleteEvent("delivery reconciliation reply"),
358
+ );
359
+
360
+ const persisted = lastAssistantPersisted();
361
+ const beforeRaw = persisted.metadata?.slackMeta;
362
+ expect(typeof beforeRaw).toBe("string");
363
+ expect(readSlackMetadata(beforeRaw as string)).toBeNull();
364
+
365
+ nextDeliveryTs = "1772678280.000200";
366
+ await deliverReplyViaCallback(
367
+ conversationId,
368
+ channelId,
369
+ "http://gateway/deliver/slack",
370
+ "assistant-1",
371
+ { messageId: persisted.id },
372
+ );
373
+
374
+ const row = persistedRows.find(
375
+ (candidate) => candidate.id === persisted.id,
376
+ );
377
+ expect(typeof row?.metadata).toBe("string");
378
+ const envelope = JSON.parse(row!.metadata!) as Record<string, unknown>;
379
+ const reconciled = readSlackMetadata(envelope.slackMeta as string);
380
+ expect(reconciled).not.toBeNull();
381
+ expect(reconciled!.channelTs).toBe("1772678280.000200");
382
+ expect(reconciled!.timestampTimezone).toBe("America/Denver");
383
+ expect(reconciled!.timestampTimezoneLabel).toBe("MT");
384
+ expect(reconciled!.speakerTimezoneLabel).toBeUndefined();
385
+ });
386
+
220
387
  test("stamps slackMeta WITHOUT threadTs for top-level Slack replies", async () => {
221
388
  const conversationId = "conv-slack-toplevel";
222
389
  const channelId = "C456NOTHREAD";