@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
@@ -15,10 +15,16 @@ import {
15
15
  isChannelId,
16
16
  parseInterfaceId,
17
17
  } from "../../channels/types.js";
18
+ import { getConfig } from "../../config/loader.js";
18
19
  import {
19
20
  createApprovalConversationGenerator,
20
21
  createApprovalCopyGenerator,
21
22
  } from "../../daemon/approval-generators.js";
23
+ import { findConversation } from "../../daemon/conversation-store.js";
24
+ import {
25
+ canonicalizeTimeZone,
26
+ resolveTurnTimezoneContext,
27
+ } from "../../daemon/date-context.js";
22
28
  import { getDiskPressureStatus } from "../../daemon/disk-pressure-guard.js";
23
29
  import { classifyDiskPressureTurnPolicy } from "../../daemon/disk-pressure-policy.js";
24
30
  import { processMessage } from "../../daemon/process-message.js";
@@ -55,7 +61,10 @@ import {
55
61
  import { markProcessed } from "../../memory/delivery-status.js";
56
62
  import { upsertBinding } from "../../memory/external-conversation-store.js";
57
63
  import type { Message as ProviderMessage } from "../../messaging/provider-types.js";
58
- import { withSlackBotToken } from "../../messaging/providers/slack/adapter.js";
64
+ import {
65
+ resolveSlackBotUserId,
66
+ withSlackBotToken,
67
+ } from "../../messaging/providers/slack/adapter.js";
59
68
  import {
60
69
  backfillDm,
61
70
  backfillThreadWindowPage,
@@ -63,6 +72,8 @@ import {
63
72
  } from "../../messaging/providers/slack/backfill.js";
64
73
  import { downloadSlackFile } from "../../messaging/providers/slack/download.js";
65
74
  import {
75
+ buildSlackTimezoneMetadata,
76
+ formatSlackTimezoneLabel,
66
77
  mergeSlackMetadata,
67
78
  readSlackMetadataFromMessageMetadata,
68
79
  type SlackFileMetadata,
@@ -102,6 +113,108 @@ const DISK_PRESSURE_REMOTE_BLOCK_REPLY =
102
113
  let deleteLookupRetries = 5;
103
114
  let deleteLookupDelayMs = 2000;
104
115
 
116
+ interface SlackActorTimezoneMetadata {
117
+ timezone?: string;
118
+ timezoneLabel?: string;
119
+ timezoneOffsetSeconds?: number;
120
+ }
121
+
122
+ function trimMetadataString(
123
+ metadata: Record<string, unknown> | undefined,
124
+ key: string,
125
+ ): string | undefined {
126
+ const value = metadata?.[key];
127
+ if (typeof value !== "string") return undefined;
128
+ const trimmed = value.trim();
129
+ return trimmed.length > 0 ? trimmed : undefined;
130
+ }
131
+
132
+ function parseSlackActorTimezoneMetadata(
133
+ sourceChannel: string,
134
+ metadata: Record<string, unknown> | undefined,
135
+ ): SlackActorTimezoneMetadata | undefined {
136
+ if (sourceChannel !== "slack") return undefined;
137
+
138
+ const timezone = trimMetadataString(metadata, "timezone");
139
+ const timezoneLabel = trimMetadataString(metadata, "timezoneLabel");
140
+ const rawOffset = metadata?.timezoneOffsetSeconds;
141
+ const timezoneOffsetSeconds =
142
+ typeof rawOffset === "number" && Number.isFinite(rawOffset)
143
+ ? rawOffset
144
+ : undefined;
145
+
146
+ if (
147
+ timezone === undefined &&
148
+ timezoneLabel === undefined &&
149
+ timezoneOffsetSeconds === undefined
150
+ ) {
151
+ return undefined;
152
+ }
153
+
154
+ return {
155
+ ...(timezone ? { timezone } : {}),
156
+ ...(timezoneLabel ? { timezoneLabel } : {}),
157
+ ...(timezoneOffsetSeconds !== undefined ? { timezoneOffsetSeconds } : {}),
158
+ };
159
+ }
160
+
161
+ function attachSlackRequesterTimezone(
162
+ trustCtx: TrustContext,
163
+ timezone: SlackActorTimezoneMetadata | undefined,
164
+ ): TrustContext {
165
+ if (!timezone) return trustCtx;
166
+ return {
167
+ ...trustCtx,
168
+ ...(timezone.timezone ? { requesterTimezone: timezone.timezone } : {}),
169
+ ...(timezone.timezoneLabel
170
+ ? { requesterTimezoneLabel: timezone.timezoneLabel }
171
+ : {}),
172
+ ...(timezone.timezoneOffsetSeconds !== undefined
173
+ ? { requesterTimezoneOffsetSeconds: timezone.timezoneOffsetSeconds }
174
+ : {}),
175
+ };
176
+ }
177
+
178
+ function resolveSlackTranscriptTimestampTimezone(
179
+ clientTimezone?: string | null,
180
+ ): {
181
+ timestampTimezone: string;
182
+ timestampTimezoneLabel?: string;
183
+ } {
184
+ const config = getConfig();
185
+ const timestampTimezone = resolveTurnTimezoneContext({
186
+ configuredUserTimeZone: config.ui?.userTimezone ?? null,
187
+ clientTimezone: clientTimezone ?? null,
188
+ detectedTimezone: config.ui?.detectedTimezone ?? null,
189
+ hostTimeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
190
+ }).effectiveTimezone;
191
+ const timestampTimezoneLabel = formatSlackTimezoneLabel(timestampTimezone);
192
+ return {
193
+ timestampTimezone,
194
+ ...(timestampTimezoneLabel ? { timestampTimezoneLabel } : {}),
195
+ };
196
+ }
197
+
198
+ function resolveInboundClientTimezone(params: {
199
+ bodyClientTimezone?: unknown;
200
+ sourceMetadata?: Record<string, unknown>;
201
+ conversationId: string;
202
+ }): string | undefined {
203
+ const bodyClientTimezone =
204
+ typeof params.bodyClientTimezone === "string"
205
+ ? canonicalizeTimeZone(params.bodyClientTimezone)
206
+ : undefined;
207
+ const metadataClientTimezone =
208
+ typeof params.sourceMetadata?.clientTimezone === "string"
209
+ ? canonicalizeTimeZone(params.sourceMetadata.clientTimezone)
210
+ : undefined;
211
+ return (
212
+ bodyClientTimezone ??
213
+ metadataClientTimezone ??
214
+ findConversation(params.conversationId)?.clientTimezone
215
+ );
216
+ }
217
+
105
218
  /**
106
219
  * Test-only override for the delete-lookup retry timings. Used by
107
220
  * tests that exercise the "no such message" path without waiting
@@ -143,6 +256,7 @@ export async function handleChannelInbound({
143
256
  replyCallbackUrl?: string;
144
257
  callbackQueryId?: string;
145
258
  callbackData?: string;
259
+ clientTimezone?: unknown;
146
260
  };
147
261
 
148
262
  const {
@@ -168,6 +282,14 @@ export async function handleChannelInbound({
168
282
  }
169
283
 
170
284
  const sourceChannel = body.sourceChannel;
285
+ const slackChannelName =
286
+ sourceChannel === "slack" && typeof sourceMetadata?.channelName === "string"
287
+ ? sourceMetadata.channelName.trim() || null
288
+ : null;
289
+ const slackActorTimezone = parseSlackActorTimezoneMetadata(
290
+ sourceChannel,
291
+ sourceMetadata,
292
+ );
171
293
 
172
294
  if (!body.interface || typeof body.interface !== "string") {
173
295
  throw new BadRequestError("interface is required");
@@ -547,6 +669,7 @@ export async function handleChannelInbound({
547
669
  conversationId: result.conversationId,
548
670
  sourceChannel,
549
671
  externalChatId: conversationExternalId,
672
+ externalChatName: slackChannelName,
550
673
  externalThreadId: slackThreadTs ?? null,
551
674
  externalUserId: canonicalSenderId ?? rawSenderId ?? null,
552
675
  displayName: body.actorDisplayName ?? null,
@@ -557,14 +680,17 @@ export async function handleChannelInbound({
557
680
  // ── Actor role resolution ──
558
681
  // Uses shared channel-agnostic resolution so all ingress paths classify
559
682
  // guardian vs non-guardian actors the same way.
560
- const trustCtx: TrustContext = resolveTrustContext({
561
- assistantId: canonicalAssistantId,
562
- sourceChannel,
563
- conversationExternalId,
564
- actorExternalId: rawSenderId,
565
- actorUsername: body.actorUsername,
566
- actorDisplayName: body.actorDisplayName,
567
- });
683
+ const trustCtx: TrustContext = attachSlackRequesterTimezone(
684
+ resolveTrustContext({
685
+ assistantId: canonicalAssistantId,
686
+ sourceChannel,
687
+ conversationExternalId,
688
+ actorExternalId: rawSenderId,
689
+ actorUsername: body.actorUsername,
690
+ actorDisplayName: body.actorDisplayName,
691
+ }),
692
+ slackActorTimezone,
693
+ );
568
694
 
569
695
  const diskPressureDecision = classifyDiskPressureTurnPolicy(
570
696
  getDiskPressureStatus(),
@@ -649,14 +775,17 @@ export async function handleChannelInbound({
649
775
  // preconditions used by the standard approval interception call below.
650
776
  const isReactionAdded = body.callbackData?.startsWith("reaction:") === true;
651
777
  if (isReactionAdded && replyCallbackUrl && !result.duplicate) {
652
- const trustCtxForReaction: TrustContext = resolveTrustContext({
653
- assistantId: canonicalAssistantId,
654
- sourceChannel,
655
- conversationExternalId,
656
- actorExternalId: rawSenderId,
657
- actorUsername: body.actorUsername,
658
- actorDisplayName: body.actorDisplayName,
659
- });
778
+ const trustCtxForReaction: TrustContext = attachSlackRequesterTimezone(
779
+ resolveTrustContext({
780
+ assistantId: canonicalAssistantId,
781
+ sourceChannel,
782
+ conversationExternalId,
783
+ actorExternalId: rawSenderId,
784
+ actorUsername: body.actorUsername,
785
+ actorDisplayName: body.actorDisplayName,
786
+ }),
787
+ slackActorTimezone,
788
+ );
660
789
 
661
790
  const approvalMessageTs =
662
791
  typeof sourceMetadata?.messageId === "string"
@@ -767,7 +896,6 @@ export async function handleChannelInbound({
767
896
  typeof hint === "string" && hint.trim().length > 0,
768
897
  )
769
898
  : [];
770
- let slackRuntimeContextNotice: string | undefined;
771
899
 
772
900
  // Inject channel-scoped permission hints for Slack channel messages
773
901
  if (sourceChannel === "slack") {
@@ -1043,10 +1171,24 @@ export async function handleChannelInbound({
1043
1171
  // this into a `slackMeta` sub-object in the row's metadata column so
1044
1172
  // the chronological renderer can reconstruct thread structure without
1045
1173
  // re-fetching from Slack.
1174
+ const slackSpeakerTimezoneLabel =
1175
+ trustCtx.trustClass !== "guardian"
1176
+ ? slackActorTimezone?.timezoneLabel
1177
+ : undefined;
1178
+ const inboundClientTimezone = resolveInboundClientTimezone({
1179
+ bodyClientTimezone: body.clientTimezone,
1180
+ sourceMetadata,
1181
+ conversationId: result.conversationId,
1182
+ });
1183
+ const slackTranscriptTimestampTimezone =
1184
+ sourceChannel === "slack"
1185
+ ? resolveSlackTranscriptTimestampTimezone(inboundClientTimezone)
1186
+ : undefined;
1046
1187
  const slackInbound =
1047
1188
  sourceChannel === "slack"
1048
1189
  ? {
1049
1190
  channelId: conversationExternalId,
1191
+ ...(slackChannelName ? { channelName: slackChannelName } : {}),
1050
1192
  channelTs: sourceMessageId ?? externalMessageId,
1051
1193
  ...(slackThreadTs ? { threadTs: slackThreadTs } : {}),
1052
1194
  ...((body.actorDisplayName ?? body.actorUsername)
@@ -1057,6 +1199,17 @@ export async function handleChannelInbound({
1057
1199
  ...(trustCtx.requesterExternalUserId
1058
1200
  ? { actorExternalUserId: trustCtx.requesterExternalUserId }
1059
1201
  : {}),
1202
+ ...buildSlackTimezoneMetadata({
1203
+ actorTimezone: slackActorTimezone?.timezone,
1204
+ actorTimezoneLabel: slackActorTimezone?.timezoneLabel,
1205
+ actorTimezoneOffsetSeconds:
1206
+ slackActorTimezone?.timezoneOffsetSeconds,
1207
+ timestampTimezone:
1208
+ slackTranscriptTimestampTimezone?.timestampTimezone,
1209
+ timestampTimezoneLabel:
1210
+ slackTranscriptTimestampTimezone?.timestampTimezoneLabel,
1211
+ speakerTimezoneLabel: slackSpeakerTimezoneLabel,
1212
+ }),
1060
1213
  }
1061
1214
  : undefined;
1062
1215
 
@@ -1075,13 +1228,16 @@ export async function handleChannelInbound({
1075
1228
  sourceChannel === "slack" && sourceMetadata?.slackBotMentioned === true;
1076
1229
 
1077
1230
  // ── DM cold-start backfill ──
1078
- // First time a Slack DM lands in a conversation that has fewer than
1079
- // SLACK_DM_BACKFILL_WARM_THRESHOLD stored slackMeta messages, fetch a
1080
- // window of recent history so the agent sees prior context. One-shot:
1081
- // once persistence warms up past the threshold, subsequent DMs no
1082
- // longer trigger backfill. Failures are non-fatal the new message
1083
- // proceeds without backfilled history.
1084
- if (sourceChannel === "slack" && sourceChatType === "im") {
1231
+ // First time a Slack DM without thread_ts lands in a conversation that
1232
+ // has fewer than SLACK_DM_BACKFILL_WARM_THRESHOLD stored slackMeta
1233
+ // messages, fetch a window of recent history so the agent sees prior
1234
+ // context. Threaded Slack DMs use the thread gap/delta path below so
1235
+ // separate app conversations do not pull unrelated whole-DM history.
1236
+ if (
1237
+ sourceChannel === "slack" &&
1238
+ sourceChatType === "im" &&
1239
+ !slackThreadTs
1240
+ ) {
1085
1241
  // Exclude the just-arrived webhook message from the history window —
1086
1242
  // the normal inbound persistence path writes it separately, so
1087
1243
  // including it here would produce duplicate user turns. Only pass a
@@ -1102,18 +1258,17 @@ export async function handleChannelInbound({
1102
1258
  }
1103
1259
 
1104
1260
  // ── Thread gap/delta backfill ──
1105
- // When a Slack thread reply arrives, compare the stored thread state
1106
- // with the inbound message's ts and fetch only the bounded unseen
1107
- // window. Initial late-join turns hydrate the earliest thread messages
1108
- // plus a recent window adjacent to the inbound reply; later turns use
1109
- // a delta window after the latest stored thread ts and before the
1110
- // inbound ts. Awaited (mirrors the DM cold-start path above) so the
1111
- // agent loop dispatched immediately afterwards observes hydrated
1112
- // context. A late-join notice is added only to the current turn's
1113
- // runtime context, not persisted as durable Slack metadata. Failures
1114
- // are swallowed inside the helper so they never block dispatch.
1261
+ // When a Slack thread reply arrives, including app/agent DMs that carry
1262
+ // thread_ts, compare the stored thread state with the inbound message's
1263
+ // ts and fetch only the bounded unseen window. Initial late-join turns
1264
+ // hydrate the earliest thread messages plus a recent window adjacent to
1265
+ // the inbound reply; later turns use a delta window after the latest
1266
+ // stored thread ts and before the inbound ts. Awaited (mirrors the DM
1267
+ // cold-start path above) so the agent loop dispatched immediately
1268
+ // afterwards observes hydrated context. Failures are swallowed inside
1269
+ // the helper so they never block dispatch.
1115
1270
  if (slackThreadTs) {
1116
- const backfillResult = await triggerSlackThreadBackfillIfNeeded({
1271
+ await triggerSlackThreadBackfillIfNeeded({
1117
1272
  conversationId: result.conversationId,
1118
1273
  channelId: conversationExternalId,
1119
1274
  threadTs: slackThreadTs,
@@ -1121,8 +1276,6 @@ export async function handleChannelInbound({
1121
1276
  account: slackAccount,
1122
1277
  guardianExternalUserId: trustCtx.guardianExternalUserId,
1123
1278
  });
1124
- const lateJoinNotice = buildSlackLateJoinNotice(backfillResult);
1125
- if (lateJoinNotice) slackRuntimeContextNotice = lateJoinNotice;
1126
1279
  }
1127
1280
 
1128
1281
  // Wrap non-guardian inbound content in external_content boundaries so
@@ -1155,7 +1308,6 @@ export async function handleChannelInbound({
1155
1308
  externalChatId: conversationExternalId,
1156
1309
  trustCtx,
1157
1310
  metadataHints,
1158
- slackRuntimeContextNotice,
1159
1311
  metadataUxBrief,
1160
1312
  commandIntent,
1161
1313
  sourceLanguageCode,
@@ -1163,6 +1315,7 @@ export async function handleChannelInbound({
1163
1315
  assistantId: canonicalAssistantId,
1164
1316
  approvalCopyGenerator,
1165
1317
  chatType: sourceChatType,
1318
+ clientTimezone: inboundClientTimezone,
1166
1319
  slackBotMentioned,
1167
1320
  slackInbound,
1168
1321
  });
@@ -1448,9 +1601,10 @@ function readStoredSlackThreadState(
1448
1601
  *
1449
1602
  * Shared insertion point for any path that hydrates Slack history lazily
1450
1603
  * (DM cold-start backfill, thread gap/delta backfill, etc.). Backfilled Slack
1451
- * rows are always persisted as `user` history: `assistant` rows are reserved
1452
- * for messages produced by the local assistant loop, not third-party channel
1453
- * replay.
1604
+ * rows normally persist as `user` history, but rows authored by this
1605
+ * assistant's configured Slack bot are replayed as assistant history so prior
1606
+ * assistant messages do not enter model context wrapped as external user
1607
+ * content.
1454
1608
  * Caller is responsible for dedup checks before invoking; this helper
1455
1609
  * performs no idempotency check itself.
1456
1610
  */
@@ -1473,6 +1627,17 @@ async function persistBackfilledSlackMessage(params: {
1473
1627
  ...(f.mimetype ? { mimetype: f.mimetype } : {}),
1474
1628
  }));
1475
1629
  const actorExternalUserId = message.sender?.id?.trim();
1630
+ const actorTimezone = trimMetadataString(message.metadata, "actorTimezone");
1631
+ const actorTimezoneLabel = trimMetadataString(
1632
+ message.metadata,
1633
+ "actorTimezoneLabel",
1634
+ );
1635
+ const isGuardian = isBackfilledSlackGuardianMessage(
1636
+ message,
1637
+ params.guardianExternalUserId,
1638
+ );
1639
+ const slackTranscriptTimestampTimezone =
1640
+ resolveSlackTranscriptTimestampTimezone();
1476
1641
  const slackMeta: SlackMessageMetadata = {
1477
1642
  source: "slack",
1478
1643
  channelId: params.channelId,
@@ -1481,14 +1646,24 @@ async function persistBackfilledSlackMessage(params: {
1481
1646
  ...(message.threadId ? { threadTs: message.threadId } : {}),
1482
1647
  ...(message.sender?.name ? { displayName: message.sender.name } : {}),
1483
1648
  ...(actorExternalUserId ? { actorExternalUserId } : {}),
1649
+ ...buildSlackTimezoneMetadata({
1650
+ actorTimezone,
1651
+ actorTimezoneLabel,
1652
+ actorTimezoneOffsetSeconds: message.metadata?.actorTimezoneOffsetSeconds,
1653
+ timestampTimezone: slackTranscriptTimestampTimezone?.timestampTimezone,
1654
+ timestampTimezoneLabel:
1655
+ slackTranscriptTimestampTimezone?.timestampTimezoneLabel,
1656
+ speakerTimezoneLabel: isGuardian ? undefined : actorTimezoneLabel,
1657
+ }),
1484
1658
  ...(slackFiles.length > 0 ? { slackFiles } : {}),
1485
1659
  };
1486
1660
 
1487
- const isGuardian = isBackfilledSlackGuardianMessage(
1661
+ const role = (await isBackfilledSlackAssistantMessage(
1488
1662
  message,
1489
- params.guardianExternalUserId,
1490
- );
1491
- const role = "user";
1663
+ params.account,
1664
+ ))
1665
+ ? "assistant"
1666
+ : "user";
1492
1667
 
1493
1668
  const rawText = message.text ?? "";
1494
1669
 
@@ -1620,6 +1795,67 @@ function isBackfilledSlackGuardianMessage(
1620
1795
  return canonicalSender === canonicalGuardian;
1621
1796
  }
1622
1797
 
1798
+ const SLACK_ASSISTANT_THREAD_PLACEHOLDER_TEXT = "New Assistant Thread";
1799
+
1800
+ async function isSlackAssistantThreadPlaceholder(
1801
+ message: ProviderMessage,
1802
+ account: string | undefined,
1803
+ ): Promise<boolean> {
1804
+ if (message.metadata?.isBot !== true) return false;
1805
+ const hasSlackFiles =
1806
+ Array.isArray(message.metadata.slackFiles) &&
1807
+ message.metadata.slackFiles.length > 0;
1808
+ return (
1809
+ message.text.replace(/\s+/g, " ").trim() ===
1810
+ SLACK_ASSISTANT_THREAD_PLACEHOLDER_TEXT &&
1811
+ (message.threadId === undefined || message.threadId === message.id) &&
1812
+ message.hasAttachments !== true &&
1813
+ !hasSlackFiles &&
1814
+ (await isBackfilledSlackAssistantMessage(message, account))
1815
+ );
1816
+ }
1817
+
1818
+ async function isBackfilledSlackAssistantMessage(
1819
+ message: ProviderMessage,
1820
+ account: string | undefined,
1821
+ ): Promise<boolean> {
1822
+ if (message.metadata?.isBot !== true) return false;
1823
+
1824
+ const botUserId = getConfig().slack.botUserId.trim();
1825
+ const rawSenderId = message.sender?.id?.trim();
1826
+ if (!botUserId) return false;
1827
+
1828
+ if (rawSenderId && slackIdentityMatches(rawSenderId, botUserId)) return true;
1829
+
1830
+ const rawBotId =
1831
+ typeof message.metadata.slackBotId === "string"
1832
+ ? message.metadata.slackBotId.trim()
1833
+ : "";
1834
+ if (!rawBotId) return false;
1835
+
1836
+ try {
1837
+ const resolvedBotUserId = await resolveSlackBotUserId(account, rawBotId);
1838
+ return (
1839
+ typeof resolvedBotUserId === "string" &&
1840
+ slackIdentityMatches(resolvedBotUserId, botUserId)
1841
+ );
1842
+ } catch (err) {
1843
+ log.warn(
1844
+ { err, slackBotId: rawBotId, channelTs: message.id },
1845
+ "Failed to resolve Slack bot id for backfilled assistant detection",
1846
+ );
1847
+ return false;
1848
+ }
1849
+ }
1850
+
1851
+ function slackIdentityMatches(left: string, right: string): boolean {
1852
+ const canonicalSender =
1853
+ canonicalizeInboundIdentity("slack", left) ?? left.trim();
1854
+ const canonicalBot =
1855
+ canonicalizeInboundIdentity("slack", right) ?? right.trim();
1856
+ return canonicalSender === canonicalBot;
1857
+ }
1858
+
1623
1859
  /**
1624
1860
  * Transient view of `slackFiles` that preserves the download URLs added by
1625
1861
  * `mapSlackFiles` on the in-flight `ProviderMessage`. These URLs never reach
@@ -1745,6 +1981,9 @@ async function runBackfillSlackDmIfCold(params: {
1745
1981
  const ordered = [...fetched].reverse();
1746
1982
  for (const message of ordered) {
1747
1983
  if (seen.has(message.id)) continue;
1984
+ if (await isSlackAssistantThreadPlaceholder(message, params.account)) {
1985
+ continue;
1986
+ }
1748
1987
  try {
1749
1988
  await persistBackfilledSlackMessage({
1750
1989
  conversationId: params.conversationId,
@@ -2168,18 +2407,6 @@ function sortSlackProviderMessages(
2168
2407
  });
2169
2408
  }
2170
2409
 
2171
- function buildSlackLateJoinNotice(
2172
- result: SlackThreadBackfillResult,
2173
- ): string | null {
2174
- if (result.reason !== "thread_late_join" || result.persisted === 0) {
2175
- return null;
2176
- }
2177
- const omitted = result.omittedMiddle
2178
- ? " Some middle thread messages were intentionally omitted from this turn's hydrated context to keep latency bounded."
2179
- : "";
2180
- return `Slack context note: this turn joined an existing thread. ${result.persisted} earlier thread message${result.persisted === 1 ? " was" : "s were"} backfilled before the current message.${omitted}`;
2181
- }
2182
-
2183
2410
  /**
2184
2411
  * Lazily backfill Slack thread gaps for an inbound thread reply.
2185
2412
  *
@@ -2320,6 +2547,9 @@ export async function triggerSlackThreadBackfillIfNeeded(params: {
2320
2547
  for (const message of fetched) {
2321
2548
  if (!message.id) continue;
2322
2549
  if (threadState.storedChannelTs.has(message.id)) continue;
2550
+ if (await isSlackAssistantThreadPlaceholder(message, account)) {
2551
+ continue;
2552
+ }
2323
2553
  try {
2324
2554
  await persistBackfilledSlackMessage({
2325
2555
  conversationId,