@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
@@ -12,6 +12,7 @@ mock.module("../config/loader.js", () => ({
12
12
  return {
13
13
  ...real,
14
14
  memory: { ...real.memory, v2: { ...real.memory.v2, enabled: false } },
15
+ slack: { ...real.slack, botUserId: "U_BOT" },
15
16
  };
16
17
  },
17
18
  }));
@@ -245,7 +246,7 @@ describe("injectChannelCapabilityContext", () => {
245
246
  expect(text).toContain("CHANNEL CONSTRAINTS");
246
247
  expect(text).toContain("Do NOT reference the dashboard UI");
247
248
  expect(text).toContain("Do NOT use ui_show");
248
- expect(text).toContain("Do NOT ask the user to use voice");
249
+ expect(text).not.toContain("microphone");
249
250
  expect(text).toContain("dashboard_capable: false");
250
251
  });
251
252
 
@@ -337,6 +338,36 @@ describe("injectChannelCapabilityContext", () => {
337
338
  expect(text).toContain("emoji reactions");
338
339
  });
339
340
 
341
+ test("allows only task_progress ui_show/ui_update guidance for Slack", () => {
342
+ const caps: ChannelCapabilities = {
343
+ channel: "slack",
344
+ dashboardCapable: false,
345
+ supportsDynamicUi: false,
346
+ supportsVoiceInput: false,
347
+ };
348
+
349
+ const result = injectChannelCapabilityContext(baseUserMessage, caps);
350
+ const text = (result.content[0] as { type: "text"; text: string }).text;
351
+ expect(text).toContain(
352
+ 'Only use ui_show/ui_update for card surfaces with template: "task_progress"',
353
+ );
354
+ expect(text).not.toContain("Do NOT use ui_show, ui_update, or app_create");
355
+ });
356
+
357
+ test("keeps blanket ui_show/ui_update prohibition for other non-dynamic channels", () => {
358
+ const caps: ChannelCapabilities = {
359
+ channel: "phone",
360
+ dashboardCapable: false,
361
+ supportsDynamicUi: false,
362
+ supportsVoiceInput: false,
363
+ };
364
+
365
+ const result = injectChannelCapabilityContext(baseUserMessage, caps);
366
+ const text = (result.content[0] as { type: "text"; text: string }).text;
367
+ expect(text).toContain("Do NOT use ui_show, ui_update, or app_create");
368
+ expect(text).not.toContain("Only use ui_show/ui_update");
369
+ });
370
+
340
371
  test("still injects for group chats even when all capabilities are true", () => {
341
372
  const caps: ChannelCapabilities = {
342
373
  channel: "slack",
@@ -611,7 +642,7 @@ describe("trust-gating via channel capabilities", () => {
611
642
  });
612
643
 
613
644
  test("non-dashboard channel adds constraint rules preventing UI references", () => {
614
- const caps = resolveChannelCapabilities("slack");
645
+ const caps = resolveChannelCapabilities("telegram");
615
646
  const message: Message = {
616
647
  role: "user",
617
648
  content: [{ type: "text", text: "Show me a chart" }],
@@ -624,7 +655,8 @@ describe("trust-gating via channel capabilities", () => {
624
655
  expect(injected).toContain("Do NOT reference the dashboard UI");
625
656
  expect(injected).toContain("Do NOT use ui_show, ui_update, or app_create");
626
657
  expect(injected).toContain("Present information as well-formatted text");
627
- expect(injected).toContain("desktop app");
658
+ expect(injected).not.toContain("accent color selection");
659
+ expect(injected).not.toContain("complete those steps");
628
660
  });
629
661
 
630
662
  test("vellum web interface allows dynamic UI but constrains dashboard references", () => {
@@ -1250,6 +1282,24 @@ describe("buildUnifiedTurnContextBlock", () => {
1250
1282
  expect(telegramText).toContain("<no_response/>");
1251
1283
  });
1252
1284
 
1285
+ test("adds task_progress hint only for Slack turns", () => {
1286
+ const slackText = buildUnifiedTurnContextBlock({
1287
+ timestamp: "2026-04-02T12:00:00Z",
1288
+ interfaceName: "slack",
1289
+ channelName: "slack",
1290
+ });
1291
+ const telegramText = buildUnifiedTurnContextBlock({
1292
+ timestamp: "2026-04-02T12:00:00Z",
1293
+ interfaceName: "telegram",
1294
+ channelName: "telegram",
1295
+ });
1296
+
1297
+ expect(slackText).toContain(
1298
+ "if you are going to do work, use task_progress",
1299
+ );
1300
+ expect(telegramText).not.toContain("use task_progress");
1301
+ });
1302
+
1253
1303
  test("dedup logic: fields matching canonical_actor_identity are omitted", () => {
1254
1304
  const uuid = "vellum-principal-b77e94f5-67c0-4599-8baa-871b925b3da8";
1255
1305
  const options: UnifiedTurnContextOptions = {
@@ -2295,7 +2345,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2295
2345
  const T1 = "1700000010.000002"; // top-level message starting thread B
2296
2346
  const T2 = "1700000030.000003"; // newer top-level message
2297
2347
  const ALIAS_T0 = parentAlias(T0);
2298
- const ALIAS_T1 = parentAlias(T1);
2299
2348
  const ALIAS_T2 = parentAlias(T2);
2300
2349
 
2301
2350
  const SLACK_CHANNEL_ID = "C0123CHANNEL";
@@ -2490,16 +2539,16 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2490
2539
  expect(lines[2]).toContain("Reply in thread B");
2491
2540
  expect(lines[3]).toContain("Reply in thread A");
2492
2541
  // Cross-thread visibility: thread B's reply is in the rendered output
2493
- // alongside thread A's reply.
2494
- expect(lines[2]).toContain(`→ ${ALIAS_T1}`);
2495
- expect(lines[3]).toContain(`→ ${ALIAS_T0}`);
2542
+ // alongside thread A's reply, without parent-arrow prefixes.
2543
+ expect(lines[2]).not.toContain("→ M");
2544
+ expect(lines[3]).not.toContain("→ M");
2496
2545
  // Sender labels appear.
2497
2546
  expect(lines[0]).toContain("alice");
2498
2547
  expect(lines[1]).toContain("bob");
2499
2548
  });
2500
2549
 
2501
2550
  // ── Scenario 2: reply to a top-level (starts new thread) ─────────────
2502
- test("scenario 2 — reply to top-level renders thread tag pointing at parent", async () => {
2551
+ test("scenario 2 — reply to top-level renders without parent arrow", async () => {
2503
2552
  const rows: MessageRow[] = [
2504
2553
  userRow({
2505
2554
  id: "m1",
@@ -2523,15 +2572,13 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2523
2572
  const lines = texts(result);
2524
2573
 
2525
2574
  expect(lines.length).toBe(2);
2526
- // Top-level has no thread tag.
2527
2575
  expect(lines[0]).not.toContain("→ M");
2528
- // Reply points at the parent's deterministic alias.
2529
- expect(lines[1]).toContain(`→ ${ALIAS_T0}`);
2576
+ expect(lines[1]).not.toContain("→ M");
2530
2577
  expect(lines[1]).toContain("Reply that starts a new thread");
2531
2578
  });
2532
2579
 
2533
2580
  // ── Scenario 3: reply to the most-recent top-level message ───────────
2534
- test("scenario 3 — reply to last top-level still renders thread tag", async () => {
2581
+ test("scenario 3 — reply to last top-level still renders chronologically", async () => {
2535
2582
  const rows: MessageRow[] = [
2536
2583
  userRow({
2537
2584
  id: "m1",
@@ -2561,13 +2608,12 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2561
2608
  const lines = texts(result);
2562
2609
 
2563
2610
  expect(lines.length).toBe(3);
2564
- // The reply targets the newer top-level alias, not the older one.
2565
- expect(lines[2]).toContain(`→ ${ALIAS_T1}`);
2566
- expect(lines[2]).not.toContain(`→ ${ALIAS_T0}`);
2611
+ expect(lines[2]).toContain("Reply to the newer top-level");
2612
+ expect(lines[2]).not.toContain("→ M");
2567
2613
  });
2568
2614
 
2569
2615
  // ── Scenario 4: brand-new top-level message ──────────────────────────
2570
- test("scenario 4 — new top-level message has no thread tag", async () => {
2616
+ test("scenario 4 — new top-level message has no parent arrow", async () => {
2571
2617
  const rows: MessageRow[] = [
2572
2618
  userRow({
2573
2619
  id: "m1",
@@ -2587,7 +2633,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2587
2633
  const lines = texts(result);
2588
2634
 
2589
2635
  expect(lines.length).toBe(2);
2590
- // Both lines render without a thread tag — they are siblings, not
2636
+ // Both lines render without a parent arrow — they are siblings, not
2591
2637
  // members of the same thread.
2592
2638
  expect(lines[0]).not.toContain("→ M");
2593
2639
  expect(lines[1]).not.toContain("→ M");
@@ -2602,9 +2648,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2602
2648
  // ── Scenario 5: legacy mixed with post-upgrade rows ──────────────────
2603
2649
  // Pre-upgrade rows have no `slackMeta` sub-key. Post-upgrade rows have
2604
2650
  // it. Both kinds must appear in the rendered transcript with legacy
2605
- // rows rendered flat (no thread tag) and post-upgrade rows carrying
2606
- // their thread tags. The renderer's chronological sort must intermix
2607
- // them on the appropriate timeline.
2651
+ // rows and post-upgrade rows both rendered without parent-arrow prefixes.
2652
+ // The renderer's chronological sort must intermix them on the appropriate
2653
+ // timeline.
2608
2654
  test("scenario 5 — legacy rows mixed with post-upgrade rows render chronologically", async () => {
2609
2655
  const rows: MessageRow[] = [
2610
2656
  // Legacy user row with a displayName hint only — no slackMeta.
@@ -2621,8 +2667,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2621
2667
  text: "Legacy assistant reply",
2622
2668
  }),
2623
2669
  // Post-upgrade row anchored to a thread parent that has no record
2624
- // in storage (legacy parent) — the renderer still emits the alias
2625
- // because the metadata is intact.
2670
+ // in storage (legacy parent).
2626
2671
  userRow({
2627
2672
  id: "m3",
2628
2673
  createdAt: 1700000000_000,
@@ -2645,11 +2690,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2645
2690
  expect(lines[0]).toContain("Legacy user message");
2646
2691
  expect(lines[1]).toContain("Legacy assistant reply");
2647
2692
  expect(lines[2]).toContain("Post-upgrade thread reply");
2648
- // Legacy rows render flat — no thread tag arrow.
2649
2693
  expect(lines[0]).not.toContain("→ M");
2650
2694
  expect(lines[1]).not.toContain("→ M");
2651
- // Post-upgrade row carries its thread tag.
2652
- expect(lines[2]).toContain(`→ ${ALIAS_T0}`);
2695
+ expect(lines[2]).not.toContain("→ M");
2653
2696
  // Sender labels: legacy rows carry no structured displayName, and the
2654
2697
  // role slot already conveys user-vs-assistant identity, so the row
2655
2698
  // mapper emits `null` senderLabel and the renderer omits the label
@@ -2967,37 +3010,6 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2967
3010
  expect(allText).not.toContain("dm context");
2968
3011
  });
2969
3012
 
2970
- test("slack late-join notice is model-facing and non-persisted", async () => {
2971
- const slackChannelCaps: ChannelCapabilities = {
2972
- channel: "slack",
2973
- dashboardCapable: false,
2974
- supportsDynamicUi: false,
2975
- supportsVoiceInput: false,
2976
- chatType: "channel",
2977
- };
2978
- const notice =
2979
- "Slack context note: this turn joined an existing thread. 3 earlier thread messages were backfilled before the current message.";
2980
-
2981
- const { messages: result, blocks } = await applyRuntimeInjections(
2982
- [{ role: "user", content: [{ type: "text", text: "current turn" }] }],
2983
- {
2984
- channelCapabilities: slackChannelCaps,
2985
- slackRuntimeContextNotice: notice,
2986
- transportHints: [notice],
2987
- },
2988
- );
2989
-
2990
- const allText = result
2991
- .flatMap((m) => m.content)
2992
- .filter((b): b is { type: "text"; text: string } => b.type === "text")
2993
- .map((b) => b.text)
2994
- .join("\n");
2995
- expect(allText).toContain("<slack_context_notice>");
2996
- expect(allText).toContain(notice);
2997
- expect(allText).not.toContain("<transport_hints>");
2998
- expect(JSON.stringify(blocks)).not.toContain(notice);
2999
- });
3000
-
3001
3013
  // ── transport_hints kept for non-slack channels ───────────────────────
3002
3014
  test("non-slack conversations still receive <transport_hints>", async () => {
3003
3015
  const { messages: result } = await applyRuntimeInjections(
@@ -3023,10 +3035,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3023
3035
  });
3024
3036
 
3025
3037
  // ── trust-filter regression for loadSlackChronologicalMessages ───────
3026
- // For untrusted actors, guardian-scoped rows must be excluded
3027
- // from the chronological transcript the same way `loadFromDb` filters
3028
- // them out of the default history.
3029
- test("loadSlackChronologicalMessages filters guardian-scoped rows for untrusted actors", () => {
3038
+ // For untrusted actors, Slack-sourced rows are still shared channel/thread
3039
+ // context, while non-Slack guardian-scoped rows remain private.
3040
+ test("loadSlackChronologicalMessages keeps Slack-visible guardian rows for untrusted actors", () => {
3030
3041
  const caps: ChannelCapabilities = {
3031
3042
  channel: "slack",
3032
3043
  dashboardCapable: false,
@@ -3034,14 +3045,15 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3034
3045
  supportsVoiceInput: false,
3035
3046
  chatType: "channel",
3036
3047
  };
3037
- // Row 1 has no provenance → guardian-scoped (filtered out).
3038
- // Row 2 has provenance.trustClass === "trusted_contact" (kept).
3039
3048
  const rows: MessageRow[] = [
3040
3049
  userRow({
3041
3050
  id: "m1",
3042
3051
  createdAt: 1700000000_000,
3043
- text: "guardian-only context",
3052
+ text: "public guardian instruction",
3044
3053
  slackMeta: buildSlackMeta({ channelTs: T0, displayName: "alice" }),
3054
+ extraOuterMetadata: {
3055
+ provenanceTrustClass: "guardian",
3056
+ },
3045
3057
  }),
3046
3058
  userRow({
3047
3059
  id: "m2",
@@ -3052,6 +3064,14 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3052
3064
  provenanceTrustClass: "trusted_contact",
3053
3065
  },
3054
3066
  }),
3067
+ userRow({
3068
+ id: "m3",
3069
+ createdAt: 1700000020_000,
3070
+ text: "private guardian-only context",
3071
+ extraOuterMetadata: {
3072
+ provenanceTrustClass: "guardian",
3073
+ },
3074
+ }),
3055
3075
  ];
3056
3076
  const result = loadSlackChronologicalMessages("conv-1", caps, {
3057
3077
  loader: () => rows,
@@ -3063,8 +3083,9 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3063
3083
  .filter((b): b is { type: "text"; text: string } => b.type === "text")
3064
3084
  .map((b) => b.text)
3065
3085
  .join("\n");
3066
- expect(allText).not.toContain("guardian-only context");
3086
+ expect(allText).toContain("public guardian instruction");
3067
3087
  expect(allText).toContain("from untrusted actor");
3088
+ expect(allText).not.toContain("private guardian-only context");
3068
3089
  });
3069
3090
 
3070
3091
  test("loadSlackChronologicalContext preserves summary and filters by Slack watermark", () => {
@@ -3382,15 +3403,14 @@ describe("Slack channel chronological rendering — multi-thread", () => {
3382
3403
  expect(focusBlock).not.toBeNull();
3383
3404
  expect(focusBlock!).toContain("<active_thread>");
3384
3405
  expect(focusBlock!).toContain("</active_thread>");
3385
- // Parent (T0) is included, both by content and via the parent alias.
3406
+ // Parent (T0) is included by content.
3386
3407
  expect(focusBlock!).toContain("Top-level in thread A");
3387
3408
  // The new reply is included.
3388
3409
  expect(focusBlock!).toContain("New reply in thread A");
3389
- expect(focusBlock!).toContain(`→ ${ALIAS_T0}`);
3410
+ expect(focusBlock!).not.toContain("→ M");
3390
3411
  // Thread B's content is NOT in the focus block.
3391
3412
  expect(focusBlock!).not.toContain("Top-level in thread B");
3392
3413
  expect(focusBlock!).not.toContain("Cross-thread reply in B");
3393
- expect(focusBlock!).not.toContain(`→ ${ALIAS_T1}`);
3394
3414
 
3395
3415
  // The focus block is appended to the FINAL user message as a tail
3396
3416
  // text block — not to any earlier message.
@@ -3988,13 +4008,97 @@ describe("assembleSlackActiveThreadFocusBlock", () => {
3988
4008
  expect(result!).toContain("@assistant: Assistant reply");
3989
4009
  });
3990
4010
 
4011
+ test("timezone-aware assistant rows keep renderer attribution in active-thread focus block", () => {
4012
+ const rows: SlackTranscriptInputRow[] = [
4013
+ buildRow(
4014
+ "user",
4015
+ "Parent",
4016
+ 1_000,
4017
+ buildMeta({
4018
+ channelTs: PARENT_TS,
4019
+ displayName: "aaron",
4020
+ timestampTimezone: "America/Denver",
4021
+ timestampTimezoneLabel: "MT",
4022
+ }),
4023
+ ),
4024
+ buildRow(
4025
+ "assistant",
4026
+ "Assistant reply",
4027
+ 2_000,
4028
+ buildMeta({
4029
+ channelTs: "1700000005.000001",
4030
+ threadTs: PARENT_TS,
4031
+ timestampTimezone: "America/Denver",
4032
+ timestampTimezoneLabel: "MT",
4033
+ speakerTimezoneLabel: "ET",
4034
+ }),
4035
+ ),
4036
+ buildRow(
4037
+ "user",
4038
+ "Follow-up",
4039
+ 3_000,
4040
+ buildMeta({
4041
+ channelTs: REPLY_TS,
4042
+ threadTs: PARENT_TS,
4043
+ displayName: "aaron",
4044
+ timestampTimezone: "America/Denver",
4045
+ timestampTimezoneLabel: "MT",
4046
+ }),
4047
+ ),
4048
+ ];
4049
+
4050
+ const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
4051
+ expect(result).not.toBeNull();
4052
+ expect(result!).toContain(
4053
+ "[nov 14 2023 3:13 PM MT assistant] Assistant reply",
4054
+ );
4055
+ expect(result!).not.toContain(
4056
+ "@assistant: [nov 14 2023 3:13 PM MT assistant]",
4057
+ );
4058
+ });
4059
+
4060
+ test("assistant content that only looks like a compact tag still gets active-thread attribution", () => {
4061
+ const compactLookingContent =
4062
+ "[nov 14 2023 3:13 PM MT assistant] Assistant reply";
4063
+ const rows: SlackTranscriptInputRow[] = [
4064
+ buildRow(
4065
+ "user",
4066
+ "Parent",
4067
+ 1_000,
4068
+ buildMeta({ channelTs: PARENT_TS, displayName: "@alice" }),
4069
+ ),
4070
+ buildRow(
4071
+ "assistant",
4072
+ compactLookingContent,
4073
+ 2_000,
4074
+ buildMeta({
4075
+ channelTs: "1700000005.000001",
4076
+ threadTs: PARENT_TS,
4077
+ }),
4078
+ ),
4079
+ buildRow(
4080
+ "user",
4081
+ "Follow-up",
4082
+ 3_000,
4083
+ buildMeta({
4084
+ channelTs: REPLY_TS,
4085
+ threadTs: PARENT_TS,
4086
+ displayName: "@alice",
4087
+ }),
4088
+ ),
4089
+ ];
4090
+
4091
+ const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
4092
+ expect(result).not.toBeNull();
4093
+ expect(result!).toContain(`@assistant: ${compactLookingContent}`);
4094
+ });
4095
+
3991
4096
  test("assistant reaction overflow trailer is not double-attributed", () => {
3992
4097
  // When assistant reactions overflow the per-target cap, `renderSlackTranscript`
3993
4098
  // emits a trailer line (`[…and N more reactions to Mxxxxxx]`) whose role
3994
- // is inherited from the first overflowing reaction — i.e. `assistant`. The
3995
- // trailer embeds no actor attribution but ends with the parent alias and
3996
- // shares the same `M<hex>]` signature as a real reaction line, so it must
3997
- // be detected by `isReactionTagLine` and skipped by the prefix step.
4099
+ // is inherited from the first overflowing reaction — i.e. `assistant`.
4100
+ // Renderer provenance marks it as a Slack reaction line so the flattened
4101
+ // active-thread block does not add a content-message prefix.
3998
4102
  const PARENT_ALIAS_TS = PARENT_TS;
3999
4103
  const buildAssistantReaction = (ts: string, emoji: string) =>
4000
4104
  buildRow(
@@ -4079,6 +4183,7 @@ describe("assembleSlackChronologicalMessages", () => {
4079
4183
  // Anchor times mirror the renderer's HH:MM (UTC) output.
4080
4184
  // 14:25:00 UTC on 2023-11-14 = epoch second 1699971900.
4081
4185
  const TS_14_25 = "1699971900.000100"; // 14:25 UTC
4186
+ const TS_14_26 = "1699971960.000200"; // 14:26 UTC
4082
4187
  const TS_14_28 = "1699972080.000300"; // 14:28 UTC
4083
4188
  const MS_14_25 = 1699971900_000;
4084
4189
  const MS_14_26 = 1699971960_000;
@@ -4239,6 +4344,85 @@ describe("assembleSlackChronologicalMessages", () => {
4239
4344
  }
4240
4345
  });
4241
4346
 
4347
+ test("expanded Slack timezone metadata remains renderable", () => {
4348
+ const userMeta: SlackMessageMetadata = {
4349
+ source: "slack",
4350
+ channelId: DM_CHANNEL_ID,
4351
+ channelTs: TS_14_25,
4352
+ eventKind: "message",
4353
+ displayName: "@alice",
4354
+ actorTimezone: "America/New_York",
4355
+ actorTimezoneLabel: "ET",
4356
+ actorTimezoneOffsetSeconds: -18000,
4357
+ timestampTimezone: "America/New_York",
4358
+ timestampTimezoneLabel: "ET",
4359
+ speakerTimezoneLabel: "ET",
4360
+ };
4361
+ const rows: SlackTranscriptInputRow[] = [
4362
+ row("user", "timezone-aware hello", MS_14_25, metadataEnvelope(userMeta)),
4363
+ ];
4364
+
4365
+ const result = assembleSlackChronologicalMessages(rows, DM_CAPS);
4366
+ expect(result).not.toBeNull();
4367
+ expect(result!.map((m) => (m.content[0] as { text: string }).text)).toEqual(
4368
+ [
4369
+ `[nov 14 2023 9:25 AM ET @alice (ET)] ${slackExternal(
4370
+ "timezone-aware hello",
4371
+ "@alice",
4372
+ )}`,
4373
+ ],
4374
+ );
4375
+ });
4376
+
4377
+ test("Slack context skips configured assistant new-thread placeholder rows", () => {
4378
+ const placeholderMeta: SlackMessageMetadata = {
4379
+ source: "slack",
4380
+ channelId: DM_CHANNEL_ID,
4381
+ channelTs: TS_14_25,
4382
+ eventKind: "message",
4383
+ displayName: "Ada",
4384
+ actorExternalUserId: "U_BOT",
4385
+ };
4386
+ const otherBotMeta: SlackMessageMetadata = {
4387
+ source: "slack",
4388
+ channelId: DM_CHANNEL_ID,
4389
+ channelTs: TS_14_26,
4390
+ eventKind: "message",
4391
+ displayName: "Build Bot",
4392
+ actorExternalUserId: "B_OTHER",
4393
+ };
4394
+ const realBotMeta: SlackMessageMetadata = {
4395
+ source: "slack",
4396
+ channelId: DM_CHANNEL_ID,
4397
+ channelTs: TS_14_28,
4398
+ eventKind: "message",
4399
+ actorExternalUserId: "B_ASSISTANT",
4400
+ };
4401
+ const rows: SlackTranscriptInputRow[] = [
4402
+ row(
4403
+ "user",
4404
+ "New Assistant Thread",
4405
+ MS_14_25,
4406
+ metadataEnvelope(placeholderMeta),
4407
+ ),
4408
+ row(
4409
+ "user",
4410
+ "New Assistant Thread",
4411
+ MS_14_26,
4412
+ metadataEnvelope(otherBotMeta),
4413
+ ),
4414
+ row("user", "real bot context", MS_14_28, metadataEnvelope(realBotMeta)),
4415
+ ];
4416
+
4417
+ const result = assembleSlackChronologicalMessages(rows, DM_CAPS);
4418
+ expect(result).not.toBeNull();
4419
+ const rendered = JSON.stringify(result);
4420
+ expect(rendered).not.toContain("Ada");
4421
+ expect(rendered.split("New Assistant Thread").length - 1).toBe(1);
4422
+ expect(rendered).toContain("Build Bot");
4423
+ expect(rendered).toContain("real bot context");
4424
+ });
4425
+
4242
4426
  test("legacy-DM fixture: pre-upgrade rows (no slackMeta) interleave with post-upgrade rows", () => {
4243
4427
  // Mix:
4244
4428
  // - Two pre-upgrade rows (created before PR 16 wired slackMeta into
@@ -4452,17 +4636,16 @@ describe("assembleSlackChronologicalMessages", () => {
4452
4636
  });
4453
4637
  });
4454
4638
 
4455
- test("post-reconciliation: assistant rows with channelTs participate in thread tagging", () => {
4639
+ test("post-reconciliation: assistant rows with channelTs participate in chronological rendering", () => {
4456
4640
  // Once `deliverReplyViaCallback` reconciles `channelTs` from the
4457
4641
  // gateway's response, assistant rows carry a fully-formed slackMeta
4458
4642
  // envelope. They must then render through the Slack chronological
4459
4643
  // path (not the legacy fallback) so reply rows pointing at the
4460
- // assistant's prior message get a `→ Mxxxxxx` parent-alias arrow.
4644
+ // assistant's prior message appear in Slack timestamp order.
4461
4645
  //
4462
4646
  // This is the cross-thread visibility that the slack-thread-aware-
4463
4647
  // context plan promises: a follow-up user reply to the assistant's
4464
- // earlier post should render with a parent-alias arrow that the model
4465
- // can use to reason about which prior assistant message it threads off.
4648
+ // earlier post should render alongside the prior assistant row.
4466
4649
  const SLACK_CHANNEL_ID_2 = "C0THREAD";
4467
4650
  const ASSISTANT_TS = "1700001000.000111";
4468
4651
  const REPLY_TS = "1700001020.000222";
@@ -4507,13 +4690,13 @@ describe("assembleSlackChronologicalMessages", () => {
4507
4690
  expect(result).not.toBeNull();
4508
4691
  expect(result!.length).toBe(2);
4509
4692
 
4510
- // The user follow-up MUST carry a `→ Mxxxxxx` parent-alias arrow that
4511
- // points at the assistant's prior message. Before reconciliation, the
4512
- // assistant row was treated as legacy/null-metadata and excluded from
4513
- // alias issuance — the user reply rendered without the arrow.
4693
+ // The user follow-up keeps timestamp/sender attribution without carrying
4694
+ // the old parent-alias arrow.
4514
4695
  const replyText = (result![1].content[0] as { text: string }).text;
4515
- expect(replyText).toMatch(/→ M[0-9a-f]{6}/);
4516
- expect(replyText).toContain(parentAlias(ASSISTANT_TS));
4696
+ expect(replyText).toBe(
4697
+ `[11/14/23 22:30 @alice]: ${slackExternal("Following up", "@alice")}`,
4698
+ );
4699
+ expect(replyText).not.toContain("→ M");
4517
4700
  });
4518
4701
 
4519
4702
  test("post-reconciliation: assistant row appears in active-thread focus block", () => {
@@ -175,7 +175,7 @@ describe("isConversationSeedSane", () => {
175
175
 
176
176
  describe("composeConversationSeed", () => {
177
177
  describe("rich verbosity (vellum/macos)", () => {
178
- test("combines title and body into flowing prose", () => {
178
+ test("omits title when conversationTitle is absent (it's the chat header)", () => {
179
179
  const signal = makeSignal();
180
180
  const copy = makeCopy({ title: "Reminder", body: "Take out the trash" });
181
181
  const seed = composeConversationSeed(
@@ -183,12 +183,71 @@ describe("composeConversationSeed", () => {
183
183
  "vellum" as NotificationChannel,
184
184
  copy,
185
185
  );
186
- expect(seed).toContain("Reminder");
187
- expect(seed).toContain("Take out the trash");
188
- // Should be flowing prose (joined with ". "), not newline-separated
186
+ // The conversation header already shows copy.title — including it in
187
+ // the bubble would duplicate it.
188
+ expect(seed).toBe("Take out the trash");
189
+ });
190
+
191
+ test("includes title when conversationTitle provides a distinct header", () => {
192
+ const signal = makeSignal();
193
+ const copy = makeCopy({
194
+ conversationTitle: "Updates",
195
+ title: "Heart rate spike",
196
+ body: "You hit 103 bpm.",
197
+ });
198
+ const seed = composeConversationSeed(
199
+ signal,
200
+ "vellum" as NotificationChannel,
201
+ copy,
202
+ );
203
+ expect(seed).toContain("Heart rate spike");
204
+ expect(seed).toContain("You hit 103 bpm");
189
205
  expect(seed).not.toContain("\n");
190
206
  });
191
207
 
208
+ test("does not duplicate title when body already starts with it", () => {
209
+ const signal = makeSignal();
210
+ const copy = makeCopy({
211
+ title: "Status update — service running",
212
+ body: "Status update — service running since 12:00 PM.",
213
+ });
214
+ const seed = composeConversationSeed(
215
+ signal,
216
+ "vellum" as NotificationChannel,
217
+ copy,
218
+ );
219
+ // Without the fix, this would produce
220
+ // "Status update — service running. Status update — service running since 12:00 PM."
221
+ expect(seed).toBe("Status update — service running since 12:00 PM.");
222
+ });
223
+
224
+ test("falls back to title when body is empty and title is the header", () => {
225
+ const signal = makeSignal();
226
+ const copy = makeCopy({ title: "Reminder", body: "" });
227
+ const seed = composeConversationSeed(
228
+ signal,
229
+ "vellum" as NotificationChannel,
230
+ copy,
231
+ );
232
+ // Even though the title is the conversation header, keeping the
233
+ // bubble non-empty wins over avoiding the redundancy.
234
+ expect(seed).toBe("Reminder");
235
+ });
236
+
237
+ test("falls back to title when body is whitespace-only", () => {
238
+ const signal = makeSignal();
239
+ const copy = makeCopy({ title: "Reminder", body: " " });
240
+ const seed = composeConversationSeed(
241
+ signal,
242
+ "vellum" as NotificationChannel,
243
+ copy,
244
+ );
245
+ // Whitespace-only bodies must be treated as empty — otherwise the
246
+ // title would be suppressed (header dedupe) and the seed would
247
+ // render as a blank bubble.
248
+ expect(seed).toBe("Reminder");
249
+ });
250
+
192
251
  test('appends "Action required." when requiresAction is true', () => {
193
252
  const signal = makeSignal({
194
253
  attentionHints: {
@@ -277,6 +336,7 @@ describe("composeConversationSeed", () => {
277
336
  test("preserves localized LLM copy on vellum (rich)", () => {
278
337
  const signal = makeSignal();
279
338
  const copy = makeCopy({
339
+ conversationTitle: "通知",
280
340
  title: "リマインダー",
281
341
  body: "ゴミを出してください",
282
342
  });
@@ -285,6 +345,7 @@ describe("composeConversationSeed", () => {
285
345
  "vellum" as NotificationChannel,
286
346
  copy,
287
347
  );
348
+ // conversationTitle is set, so the rich seed includes both title and body.
288
349
  expect(seed).toContain("リマインダー");
289
350
  expect(seed).toContain("ゴミを出してください");
290
351
  });
@@ -314,6 +375,7 @@ describe("composeConversationSeed", () => {
314
375
  },
315
376
  });
316
377
  const copy = makeCopy({
378
+ conversationTitle: "ガーディアン",
317
379
  title: "ガーディアンの質問",
318
380
  body: "ゲートコードは何ですか?",
319
381
  });