@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
@@ -9,6 +9,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
9
9
  import { join, resolve } from "node:path";
10
10
 
11
11
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
12
+ import { getConfig } from "../config/loader.js";
12
13
  import { createContextSummaryMessage } from "../context/window-manager.js";
13
14
  import { getAppDirPath, listAppFiles } from "../memory/app-store.js";
14
15
  import {
@@ -20,15 +21,16 @@ import {
20
21
  extractMemoryPrefixBlocks,
21
22
  } from "../memory/graph/conversation-graph-memory.js";
22
23
  import type { QdrantSparseVector } from "../memory/qdrant-client.js";
23
- import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
24
+ import {
25
+ readSlackMetadata,
26
+ readSlackMetadataFromMessageMetadata,
27
+ } from "../messaging/providers/slack/message-metadata.js";
24
28
  import {
25
29
  compareSlackTs,
26
30
  extractTagLineTexts,
27
- isReactionTagLine,
28
31
  isSlackTsAfter,
29
32
  type RenderableSlackMessage,
30
33
  type RenderedSlackTranscriptMessage,
31
- renderSlackTranscript,
32
34
  renderSlackTranscriptWithProvenance,
33
35
  } from "../messaging/providers/slack/render-transcript.js";
34
36
  import { getInjectors } from "../plugins/registry.js";
@@ -48,6 +50,7 @@ import {
48
50
  import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
49
51
  import type { SubagentState } from "../subagent/types.js";
50
52
  import { TERMINAL_STATUSES } from "../subagent/types.js";
53
+ import { canonicalizeInboundIdentity } from "../util/canonicalize-identity.js";
51
54
  import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
52
55
  import { stripCommentLines } from "../util/strip-comment-lines.js";
53
56
  import { filterMessagesForUntrustedActor } from "./conversation-lifecycle.js";
@@ -680,18 +683,19 @@ export function injectChannelCapabilityContext(
680
683
  "- Do NOT reference the dashboard UI, settings panels, or visual preference pickers.",
681
684
  );
682
685
  if (!caps.supportsDynamicUi) {
683
- lines.push(
684
- "- Do NOT use ui_show, ui_update, or app_create — this channel cannot render them.",
685
- );
686
+ if (caps.channel === "slack") {
687
+ lines.push(
688
+ '- Do NOT use app_create. Only use ui_show/ui_update for card surfaces with template: "task_progress"; present all other information as text.',
689
+ );
690
+ } else {
691
+ lines.push(
692
+ "- Do NOT use ui_show, ui_update, or app_create — this channel cannot render them.",
693
+ );
694
+ }
686
695
  lines.push(
687
696
  "- Present information as well-formatted text instead of dynamic UI.",
688
697
  );
689
698
  }
690
- lines.push(
691
- "- Defer dashboard-specific actions (e.g. accent color selection) by telling the user",
692
- );
693
- lines.push(" they can complete those steps later from the desktop app.");
694
-
695
699
  if (caps.channel === "whatsapp") {
696
700
  lines.push(
697
701
  "- Do NOT use markdown tables — use bullet lists instead. No markdown headers — use **bold** or CAPS for emphasis.",
@@ -699,10 +703,6 @@ export function injectChannelCapabilityContext(
699
703
  }
700
704
  }
701
705
 
702
- if (!caps.supportsVoiceInput) {
703
- lines.push("- Do NOT ask the user to use voice or microphone input.");
704
- }
705
-
706
706
  // Inject group chat etiquette only when the chat type indicates a multi-party
707
707
  // conversation, avoiding misconditioned "stay silent" guidance in 1:1 DMs.
708
708
  if (isGroupChatType(caps.chatType)) {
@@ -798,6 +798,13 @@ export interface UnifiedTurnContextOptions {
798
798
  * the model can acknowledge long absences; otherwise omitted.
799
799
  */
800
800
  timeSinceLastMessage?: string | null;
801
+ /**
802
+ * Human-readable model profile description. Only populated when the active
803
+ * inference profile changed since the last turn (or on the first turn of a
804
+ * conversation) so the model knows which profile/model it is using without
805
+ * paying per-turn token cost.
806
+ */
807
+ modelProfile?: string | null;
801
808
  }
802
809
 
803
810
  /**
@@ -856,6 +863,9 @@ export function buildUnifiedTurnContextBlock(
856
863
  if (options.timeSinceLastMessage) {
857
864
  lines.push(`time_since_last_message: ${options.timeSinceLastMessage}`);
858
865
  }
866
+ if (options.modelProfile) {
867
+ lines.push(`model_profile: ${options.modelProfile}`);
868
+ }
859
869
  if (options.interfaceName) {
860
870
  lines.push(`interface: ${options.interfaceName}`);
861
871
  }
@@ -973,6 +983,9 @@ export function buildUnifiedTurnContextBlock(
973
983
  lines.push(
974
984
  `response_discretion: Not every message in a channel thread requires your response. If a message is clearly not directed at you (e.g. people talking among themselves, acknowledgements, reactions), output exactly <no_response/> as your entire reply to stay silent.`,
975
985
  );
986
+ if (options.channelName === "slack") {
987
+ lines.push("if you are going to do work, use task_progress");
988
+ }
976
989
  }
977
990
 
978
991
  lines.push("</turn_context>");
@@ -1032,17 +1045,6 @@ function injectTransportHints(message: Message, hints: string[]): Message {
1032
1045
  };
1033
1046
  }
1034
1047
 
1035
- function injectSlackRuntimeContextNotice(
1036
- message: Message,
1037
- notice: string,
1038
- ): Message {
1039
- const block = `<slack_context_notice>\n${notice}\n</slack_context_notice>`;
1040
- return {
1041
- ...message,
1042
- content: [{ type: "text", text: block }, ...message.content],
1043
- };
1044
- }
1045
-
1046
1048
  // ---------------------------------------------------------------------------
1047
1049
  // Slack chronological transcript assembly
1048
1050
  // ---------------------------------------------------------------------------
@@ -1111,6 +1113,27 @@ function messageRowsToSlackTranscriptRows(
1111
1113
  }));
1112
1114
  }
1113
1115
 
1116
+ function hasSlackMetadata(row: MessageRow): boolean {
1117
+ return (
1118
+ readSlackMetadataFromMessageMetadata(row.metadata, {
1119
+ allowFlatLegacy: true,
1120
+ }) !== null
1121
+ );
1122
+ }
1123
+
1124
+ function filterSlackConversationRowsForActor(
1125
+ rows: MessageRow[],
1126
+ trustClass: TrustClass | undefined,
1127
+ ): MessageRow[] {
1128
+ if (!isUntrustedTrustClass(trustClass)) return rows;
1129
+ const nonSlackVisibleRows = filterMessagesForUntrustedActor(rows);
1130
+ const nonSlackVisibleIds = new Set(nonSlackVisibleRows.map((row) => row.id));
1131
+ return rows.filter((row) => {
1132
+ if (hasSlackMetadata(row)) return true;
1133
+ return nonSlackVisibleIds.has(row.id);
1134
+ });
1135
+ }
1136
+
1114
1137
  /**
1115
1138
  * Extract the user-facing plain text from an already-parsed `ContentBlock[]`.
1116
1139
  * Only `text` blocks contribute to the rendered transcript line. Tool-use /
@@ -1268,6 +1291,60 @@ function rowToRenderable(row: SlackTranscriptInputRow): RenderableSlackMessage {
1268
1291
  };
1269
1292
  }
1270
1293
 
1294
+ const SLACK_ASSISTANT_THREAD_PLACEHOLDER_TEXT = "New Assistant Thread";
1295
+
1296
+ function isSlackAssistantThreadPlaceholder(
1297
+ message: RenderableSlackMessage,
1298
+ canonicalConfiguredBotUserId: string | null,
1299
+ ): boolean {
1300
+ if (!canonicalConfiguredBotUserId) return false;
1301
+ const metadata = message.metadata;
1302
+ if (!metadata || metadata.eventKind !== "message") return false;
1303
+ const actorExternalUserId = metadata.actorExternalUserId?.trim();
1304
+ if (!actorExternalUserId) return false;
1305
+
1306
+ const canonicalActor =
1307
+ canonicalizeInboundIdentity("slack", actorExternalUserId) ??
1308
+ actorExternalUserId;
1309
+ const isThreadRoot =
1310
+ metadata.threadTs === undefined || metadata.threadTs === metadata.channelTs;
1311
+ const hasSlackFiles =
1312
+ Array.isArray(metadata.slackFiles) && metadata.slackFiles.length > 0;
1313
+
1314
+ return (
1315
+ message.role === "user" &&
1316
+ canonicalActor === canonicalConfiguredBotUserId &&
1317
+ isThreadRoot &&
1318
+ !hasSlackFiles &&
1319
+ message.content.replace(/\s+/g, " ").trim() ===
1320
+ SLACK_ASSISTANT_THREAD_PLACEHOLDER_TEXT
1321
+ );
1322
+ }
1323
+
1324
+ function getCanonicalConfiguredSlackBotUserId(): string | null {
1325
+ const configuredBotUserId = getConfig().slack.botUserId.trim();
1326
+ if (!configuredBotUserId) return null;
1327
+ return (
1328
+ canonicalizeInboundIdentity("slack", configuredBotUserId) ??
1329
+ configuredBotUserId
1330
+ );
1331
+ }
1332
+
1333
+ function rowsToRenderableSlackMessages(
1334
+ rows: SlackTranscriptInputRow[],
1335
+ ): RenderableSlackMessage[] {
1336
+ const canonicalConfiguredBotUserId = getCanonicalConfiguredSlackBotUserId();
1337
+ return rows
1338
+ .map(rowToRenderable)
1339
+ .filter(
1340
+ (message) =>
1341
+ !isSlackAssistantThreadPlaceholder(
1342
+ message,
1343
+ canonicalConfiguredBotUserId,
1344
+ ),
1345
+ );
1346
+ }
1347
+
1271
1348
  /**
1272
1349
  * Compatibility projection for callers that still need the legacy
1273
1350
  * `Message[] | null` shape. New runtime callers should use
@@ -1363,7 +1440,7 @@ function assembleSlackChronologicalContext(
1363
1440
  if (capabilities.channel !== "slack") {
1364
1441
  return null;
1365
1442
  }
1366
- const renderable = rows.map(rowToRenderable);
1443
+ const renderable = rowsToRenderableSlackMessages(rows);
1367
1444
  const rendered = renderSlackTranscriptWithProvenance(renderable);
1368
1445
  const contextSummary = options.contextSummary?.trim();
1369
1446
  const renderedMessages = rendered.renderedMessages;
@@ -1372,6 +1449,7 @@ function assembleSlackChronologicalContext(
1372
1449
  {
1373
1450
  message: createContextSummaryMessage(contextSummary),
1374
1451
  sourceChannelTs: null,
1452
+ tagLineProvenance: "none",
1375
1453
  },
1376
1454
  ...renderedMessages,
1377
1455
  ];
@@ -1392,11 +1470,10 @@ function assembleSlackChronologicalContext(
1392
1470
  * Compatibility wrapper over `loadSlackChronologicalContext` for callers that
1393
1471
  * still need only the legacy `Message[] | null` projection.
1394
1472
  *
1395
- * When `trustClass` identifies an untrusted actor (guardian-scoped rows
1396
- * must not leak into the model context), rows are passed through
1397
- * `filterMessagesForUntrustedActor` before assembly mirroring the
1398
- * filtering applied in `loadFromDb` so the chronological transcript
1399
- * respects the same per-actor scoping as the default history path.
1473
+ * When `trustClass` identifies an untrusted actor, non-Slack/private rows
1474
+ * are passed through the default trust filter. Slack-tagged rows stay visible
1475
+ * because the transcript is scoped to the external Slack chat/thread, which
1476
+ * the inbound actor can already read in Slack.
1400
1477
  *
1401
1478
  * Returns `null` when the channel is not Slack — callers should fall
1402
1479
  * through to the default in-memory message history.
@@ -1445,9 +1522,10 @@ export function loadSlackChronologicalContext(
1445
1522
  }
1446
1523
  const loader = options.loader ?? defaultGetMessages;
1447
1524
  const allRows = loader(conversationId);
1448
- const scopedRows = isUntrustedTrustClass(options.trustClass)
1449
- ? filterMessagesForUntrustedActor(allRows)
1450
- : allRows;
1525
+ const scopedRows = filterSlackConversationRowsForActor(
1526
+ allRows,
1527
+ options.trustClass,
1528
+ );
1451
1529
  const rows = filterRowsAfterSlackCompactionBoundary(
1452
1530
  messageRowsToSlackTranscriptRows(scopedRows),
1453
1531
  options,
@@ -1555,29 +1633,29 @@ function buildActiveThreadBlockFromRenderable(
1555
1633
  if (members.length === 0) return null;
1556
1634
 
1557
1635
  // The active-thread block is flattened to plain text below, which discards
1558
- // `Message.role`. Assistant rows are relabeled in the post-render step:
1559
- // `renderSlackTranscript` emits assistant content with no tag-line wrapper
1560
- // (to prevent the model mimicking `[MM/DD/YY HH:MM]:` prefixes in outbound
1561
- // replies), so we prepend an explicit `@assistant:` label to the flattened
1562
- // line. Unnamed user rows (no real Slack displayName) get a `@user`
1563
- // senderLabel here so their tag line carries attribution through the
1564
- // renderer. Labeled user rows and assistant rows pass through unchanged.
1636
+ // `Message.role`. Assistant rows that render content-only are relabeled in
1637
+ // the post-render step. Timezone-aware assistant rows are already
1638
+ // bracket-tagged by the renderer and must not receive another prefix.
1639
+ // Unnamed user rows (no real Slack displayName) get a `@user` senderLabel
1640
+ // here so their tag line carries attribution through the renderer. Labeled
1641
+ // user rows and assistant rows pass through unchanged.
1565
1642
  const labeledMembers = members.map((m) => {
1566
1643
  if (m.role === "assistant") return m;
1567
1644
  if (m.senderLabel !== null) return m;
1568
1645
  return { ...m, senderLabel: "@user" };
1569
1646
  });
1570
1647
 
1571
- const rendered = renderSlackTranscript(labeledMembers);
1572
- if (rendered.length === 0) return null;
1573
- // Reaction / overflow-trailer lines already embed `@assistant` inline, so
1574
- // `isReactionTagLine` is used to skip those and avoid double-attribution
1575
- // (`@assistant: [... @assistant reacted ...]`). Regular content and the
1576
- // `[deleted]` sentinel get the prefix so attribution survives flattening.
1577
- const lines = rendered
1578
- .map((msg) => {
1579
- const text = extractTagLineTexts([msg])[0] ?? "";
1580
- return msg.role === "assistant" && !isReactionTagLine(text)
1648
+ const rendered = renderSlackTranscriptWithProvenance(labeledMembers);
1649
+ if (rendered.renderedMessages.length === 0) return null;
1650
+ // Reaction / overflow-trailer lines are renderer-owned Slack event lines,
1651
+ // and timezone-aware assistant rows already carry metadata-backed compact
1652
+ // attribution. Regular assistant content and the `[deleted]` sentinel get
1653
+ // the prefix so attribution survives flattening.
1654
+ const lines = rendered.renderedMessages
1655
+ .map((entry) => {
1656
+ const text = extractTagLineTexts([entry.message])[0] ?? "";
1657
+ return entry.message.role === "assistant" &&
1658
+ entry.tagLineProvenance === "none"
1581
1659
  ? `@assistant: ${text}`
1582
1660
  : text;
1583
1661
  })
@@ -1605,7 +1683,7 @@ export function assembleSlackActiveThreadFocusBlock(
1605
1683
  // conversation and omits the field for DMs, so gate the focus block
1606
1684
  // on the positive `"channel"` match.
1607
1685
  if (capabilities.chatType !== "channel") return null;
1608
- const renderable = rows.map(rowToRenderable);
1686
+ const renderable = rowsToRenderableSlackMessages(rows);
1609
1687
  const activeThreadTs = detectActiveThreadTs(renderable);
1610
1688
  if (!activeThreadTs) return null;
1611
1689
  return buildActiveThreadBlockFromRenderable(renderable, activeThreadTs);
@@ -1631,9 +1709,10 @@ export function loadSlackActiveThreadFocusBlock(
1631
1709
  if (capabilities.chatType !== "channel") return null;
1632
1710
  const loader = options.loader ?? defaultGetMessages;
1633
1711
  const allRows = loader(conversationId);
1634
- const scopedRows = isUntrustedTrustClass(options.trustClass)
1635
- ? filterMessagesForUntrustedActor(allRows)
1636
- : allRows;
1712
+ const scopedRows = filterSlackConversationRowsForActor(
1713
+ allRows,
1714
+ options.trustClass,
1715
+ );
1637
1716
  const rows = filterRowsAfterSlackCompactionBoundary(
1638
1717
  messageRowsToSlackTranscriptRows(scopedRows),
1639
1718
  options,
@@ -1681,7 +1760,6 @@ const RUNTIME_INJECTION_PREFIXES = [
1681
1760
  "<pkb>", // backward-compat: strip legacy tag from pre-rename history
1682
1761
  "<system_reminder>",
1683
1762
  "<transport_hints>",
1684
- "<slack_context_notice>",
1685
1763
  // The Slack active-thread focus block is non-persisted and injected on
1686
1764
  // the FINAL user turn only. Strip it here so re-assembly during compaction
1687
1765
  // and overflow recovery does not duplicate it across turns.
@@ -1975,7 +2053,6 @@ export interface RuntimeInjectionOptions {
1975
2053
  */
1976
2054
  isBackgroundConversation?: boolean;
1977
2055
  transportHints?: string[] | null;
1978
- slackRuntimeContextNotice?: string | null;
1979
2056
  /**
1980
2057
  * Pre-rendered Slack chronological transcript that replaces the
1981
2058
  * default `runMessages` history for any Slack conversation (channels
@@ -2316,23 +2393,6 @@ export async function applyRuntimeInjections(
2316
2393
  }
2317
2394
  }
2318
2395
 
2319
- if (
2320
- mode === "full" &&
2321
- slackConversation &&
2322
- options.slackRuntimeContextNotice
2323
- ) {
2324
- const userTail = result[result.length - 1];
2325
- if (userTail && userTail.role === "user") {
2326
- result = [
2327
- ...result.slice(0, -1),
2328
- injectSlackRuntimeContextNotice(
2329
- userTail,
2330
- options.slackRuntimeContextNotice,
2331
- ),
2332
- ];
2333
- }
2334
- }
2335
-
2336
2396
  if (mode === "full" && options.channelCommandContext) {
2337
2397
  const userTail = result[result.length - 1];
2338
2398
  if (userTail && userTail.role === "user") {
@@ -14,7 +14,8 @@ import { getVisibleProviderCatalog } from "../providers/provider-catalog-visibil
14
14
  export type SlashResolution =
15
15
  | { kind: "passthrough"; content: string }
16
16
  | { kind: "unknown"; message: string }
17
- | { kind: "compact"; targetInputTokensOverride?: number };
17
+ | { kind: "compact"; targetInputTokensOverride?: number }
18
+ | { kind: "clean" };
18
19
 
19
20
  const COMPACT_USAGE_HINT =
20
21
  "Usage: `/compact [<tokens>]` (e.g. `/compact 30000`, `/compact 30k`, `/compact 1m`).";
@@ -52,6 +53,23 @@ function parseCompactCommand(trimmed: string): CompactParse | null {
52
53
  return { kind: "compact", targetInputTokensOverride: tokens };
53
54
  }
54
55
 
56
+ type CleanParse = { kind: "clean" } | { kind: "unknown"; message: string };
57
+
58
+ const CLEAN_COMMAND_PATTERN = /^\/clean(?:\s+(.+?))?\s*$/i;
59
+
60
+ function parseCleanCommand(trimmed: string): CleanParse | null {
61
+ const match = trimmed.match(CLEAN_COMMAND_PATTERN);
62
+ if (!match) return null;
63
+ const rest = match[1]?.trim();
64
+ if (rest) {
65
+ return {
66
+ kind: "unknown",
67
+ message: `\`/clean\` does not take arguments. Usage: \`/clean\`.`,
68
+ };
69
+ }
70
+ return { kind: "clean" };
71
+ }
72
+
55
73
  // ── /context and /status commands ────────────────────────────────────
56
74
 
57
75
  export interface SlashContext {
@@ -302,10 +320,14 @@ function resolveStatusCommand(context: SlashContext): SlashResolution {
302
320
  return { kind: "unknown", message: lines.join("\n") };
303
321
  }
304
322
 
323
+ const CLEAN_HELP_LINE =
324
+ "/clean — Strip injected runtime context and reset memory injection state (no summarization)";
325
+
305
326
  function resolveCommandsList(context?: SlashContext): string[] {
306
327
  const fallbackLines = [
307
328
  "/commands — List all available commands",
308
329
  "/compact — Force context compaction immediately",
330
+ CLEAN_HELP_LINE,
309
331
  ];
310
332
  if (context) {
311
333
  fallbackLines.push("/context — Show conversation context usage");
@@ -322,6 +344,7 @@ function resolveCommandsList(context?: SlashContext): string[] {
322
344
  return [
323
345
  "/commands — List all available commands",
324
346
  "/compact — Force context compaction immediately",
347
+ CLEAN_HELP_LINE,
325
348
  "/context — Show conversation context usage",
326
349
  "/model — List or switch inference profile",
327
350
  "/models — List all available models",
@@ -335,6 +358,7 @@ function resolveCommandsList(context?: SlashContext): string[] {
335
358
  return [
336
359
  "/commands — List all available commands",
337
360
  "/compact — Force context compaction immediately",
361
+ CLEAN_HELP_LINE,
338
362
  "/context — Show conversation context usage",
339
363
  "/model — List or switch inference profile",
340
364
  "/models — List all available models",
@@ -347,6 +371,7 @@ function resolveCommandsList(context?: SlashContext): string[] {
347
371
  return [
348
372
  "/commands — List all available commands",
349
373
  "/compact — Force context compaction immediately",
374
+ CLEAN_HELP_LINE,
350
375
  "/context — Show conversation context usage",
351
376
  "/model — List or switch inference profile",
352
377
  "/models — List all available models",
@@ -366,7 +391,7 @@ function resolveCommandsList(context?: SlashContext): string[] {
366
391
  */
367
392
  export function classifySlash(
368
393
  content: string,
369
- ): "passthrough" | "compact" | "unknown" {
394
+ ): "passthrough" | "compact" | "clean" | "unknown" {
370
395
  const trimmed = content.trim();
371
396
  if (parseModelCommand(trimmed) != null) {
372
397
  return "unknown";
@@ -381,6 +406,8 @@ export function classifySlash(
381
406
  if (trimmed === "/models") return "unknown";
382
407
  const compactParse = parseCompactCommand(trimmed);
383
408
  if (compactParse) return compactParse.kind;
409
+ const cleanParse = parseCleanCommand(trimmed);
410
+ if (cleanParse) return cleanParse.kind;
384
411
  if (trimmed === "/context") return "unknown";
385
412
  if (trimmed === "/status") return "unknown";
386
413
  if (trimmed === "/commands") return "unknown";
@@ -388,9 +415,10 @@ export function classifySlash(
388
415
  }
389
416
 
390
417
  /**
391
- * Resolve built-in slash commands (/models, /context, /status, /commands, /compact).
392
- * Returns `unknown` with a deterministic message, `compact` for forced compaction,
393
- * or the (possibly rewritten) content as `passthrough`.
418
+ * Resolve built-in slash commands (/models, /context, /status, /commands,
419
+ * /compact, /clean). Returns `unknown` with a deterministic message,
420
+ * `compact` for forced compaction, `clean` for injection stripping, or the
421
+ * (possibly rewritten) content as `passthrough`.
394
422
  */
395
423
  export async function resolveSlash(
396
424
  content: string,
@@ -424,6 +452,10 @@ export async function resolveSlash(
424
452
  const compactParse = parseCompactCommand(trimmed);
425
453
  if (compactParse) return compactParse;
426
454
 
455
+ // Handle /clean command (strip injections, no summarization).
456
+ const cleanParse = parseCleanCommand(trimmed);
457
+ if (cleanParse) return cleanParse;
458
+
427
459
  // Handle /context and legacy /status commands
428
460
  if (trimmed === "/context" || trimmed === "/status") {
429
461
  if (!context) {
@@ -390,6 +390,40 @@ function normalizeTaskProgressCardPatch(
390
390
  return normalizedPatch;
391
391
  }
392
392
 
393
+ function isTaskProgressCardData(data: SurfaceData | Record<string, unknown>) {
394
+ return (data as Record<string, unknown>).template === "task_progress";
395
+ }
396
+
397
+ function isSlackTaskProgressUiException(
398
+ ctx: SurfaceConversationContext,
399
+ toolName: string,
400
+ input: Record<string, unknown>,
401
+ ): boolean {
402
+ if (ctx.channelCapabilities?.channel !== "slack") return false;
403
+ if (toolName === "ui_show") {
404
+ const surfaceType = input.surface_type as SurfaceType;
405
+ if (surfaceType !== "card") return false;
406
+ const rawData = isPlainObject(input.data) ? input.data : {};
407
+ const data = normalizeCardShowData(input, rawData);
408
+ return isTaskProgressCardData(data);
409
+ }
410
+ if (toolName === "ui_update") {
411
+ const surfaceId = input.surface_id;
412
+ if (typeof surfaceId !== "string") return false;
413
+ const stored = ctx.surfaceState.get(surfaceId);
414
+ if (!stored || stored.surfaceType !== "card") return false;
415
+ if (!isTaskProgressCardData(stored.data)) return false;
416
+ const rawPatch = isPlainObject(input.data) ? input.data : {};
417
+ const patch = normalizeTaskProgressCardPatch(
418
+ stored.data as CardSurfaceData,
419
+ rawPatch,
420
+ );
421
+ const mergedData = { ...stored.data, ...patch } as SurfaceData;
422
+ return isTaskProgressCardData(mergedData);
423
+ }
424
+ return false;
425
+ }
426
+
393
427
  /**
394
428
  * Subset of Conversation state that surface helpers need access to.
395
429
  * The Conversation class implements this interface so its instances can be
@@ -1638,7 +1672,12 @@ export async function handleSurfaceAction(
1638
1672
  // One-shot interactive surfaces — auto-complete now that the message has
1639
1673
  // been accepted. Deferred until after rejection check so the surface stays
1640
1674
  // active and retryable if the queue was full.
1641
- const ONE_SHOT_SURFACE_TYPES = ["form", "confirmation", "file_upload"];
1675
+ const ONE_SHOT_SURFACE_TYPES = [
1676
+ "form",
1677
+ "confirmation",
1678
+ "file_upload",
1679
+ "task_preferences",
1680
+ ];
1642
1681
  if (ONE_SHOT_SURFACE_TYPES.includes(pending.surfaceType)) {
1643
1682
  broadcastMessage({
1644
1683
  type: "ui_surface_complete",
@@ -2135,7 +2174,11 @@ export async function surfaceProxyResolver(
2135
2174
 
2136
2175
  if (toolName === "ui_show" || toolName === "ui_update") {
2137
2176
  const caps = ctx.channelCapabilities;
2138
- if (caps && !caps.supportsDynamicUi) {
2177
+ if (
2178
+ caps &&
2179
+ !caps.supportsDynamicUi &&
2180
+ !isSlackTaskProgressUiException(ctx, toolName, input)
2181
+ ) {
2139
2182
  log.info(
2140
2183
  { toolName, channel: caps.channel, conversationId: ctx.conversationId },
2141
2184
  "Blocked UI surface tool on channel without dynamic UI support",
@@ -331,6 +331,7 @@ export interface SkillProjectionContext {
331
331
  // ── Conditional tool sets ────────────────────────────────────────────
332
332
 
333
333
  const UI_SURFACE_TOOL_NAMES = new Set(["ui_show", "ui_update", "ui_dismiss"]);
334
+ const SLACK_TASK_PROGRESS_UI_TOOL_NAMES = new Set(["ui_show", "ui_update"]);
334
335
  /**
335
336
  * Single source of truth for which tools are host tools and the capability
336
337
  * each one requires from the connected client interface. Adding a tool here
@@ -437,6 +438,12 @@ export function isToolActiveForContext(
437
438
  return false;
438
439
  }
439
440
  if (UI_SURFACE_TOOL_NAMES.has(name)) {
441
+ if (
442
+ ctx.channelCapabilities?.channel === "slack" &&
443
+ SLACK_TASK_PROGRESS_UI_TOOL_NAMES.has(name)
444
+ ) {
445
+ return !ctx.hasNoClient;
446
+ }
440
447
  return ctx.channelCapabilities?.supportsDynamicUi ?? !ctx.hasNoClient;
441
448
  }
442
449
  if (HOST_TOOL_NAMES.has(name)) {
@@ -53,6 +53,7 @@ import {
53
53
  getConversation,
54
54
  getConversationOriginChannel,
55
55
  getConversationOverrideProfileFromRow,
56
+ setConversationCleanedAt,
56
57
  } from "../memory/conversation-crud.js";
57
58
  import { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
58
59
  import { shouldExposePersonalMemory } from "../memory/v2/static-context.js";
@@ -104,6 +105,7 @@ import {
104
105
  type ChannelCapabilities,
105
106
  getSlackCompactionWatermarkForPrefix,
106
107
  loadSlackChronologicalContext,
108
+ stripInjectionsForCompaction,
107
109
  } from "./conversation-runtime-assembly.js";
108
110
  import type { SkillProjectionCache } from "./conversation-skill-tools.js";
109
111
  import {
@@ -142,6 +144,13 @@ import { TraceEmitter } from "./trace-emitter.js";
142
144
 
143
145
  const log = getLogger("conversation");
144
146
 
147
+ export interface CleanResult {
148
+ previousEstimatedInputTokens: number;
149
+ estimatedInputTokens: number;
150
+ maxInputTokens: number;
151
+ preservedMessages: number;
152
+ }
153
+
145
154
  export { findLastUndoableUserMessageIndex } from "./conversation-history.js";
146
155
  export type {
147
156
  QueueDrainReason,
@@ -240,7 +249,6 @@ export class Conversation {
240
249
  /** @internal */ loadedHistoryPersonalMemoryAllowed?: boolean;
241
250
  /** @internal */ voiceCallControlPrompt?: string;
242
251
  /** @internal */ transportHints?: string[];
243
- /** @internal */ slackRuntimeContextNotice?: string;
244
252
  /** @internal */ assistantId?: string;
245
253
  /** @internal */ commandIntent?: {
246
254
  type: string;
@@ -249,6 +257,13 @@ export class Conversation {
249
257
  };
250
258
  /** @internal */ surfaceActionRequestIds = new Set<string>();
251
259
  /** @internal */ approvedViaPromptThisTurn = false;
260
+ /**
261
+ * Set by `steerToMessage` to signal the drain path that it should inject
262
+ * synthetic tool_result messages for any pending tool_use blocks abandoned
263
+ * by the aborted generation. Cleared after repair.
264
+ * @internal
265
+ */
266
+ pendingSteerRepair = false;
252
267
  /**
253
268
  * When true, side-effect tools must prompt even if a trust/allow rule
254
269
  * would auto-allow. Set by non-interactive callers (e.g. non-guardian
@@ -1126,6 +1141,32 @@ export class Conversation {
1126
1141
  return result;
1127
1142
  }
1128
1143
 
1144
+ /**
1145
+ * Strip stale runtime injections from the message history and reset the
1146
+ * memory-injection ledger without summarizing any history. Mirrors the
1147
+ * non-LLM side effects of `forceCompact`: the next turn re-injects fresh
1148
+ * NOW.md / knowledge-base / memory-v2 static blocks, and per-turn memory
1149
+ * activations are no longer deduped against the prior session.
1150
+ */
1151
+ async forceClean(): Promise<CleanResult> {
1152
+ const previousEstimatedInputTokens =
1153
+ this.contextWindowManager.estimateInputTokens(this.messages);
1154
+ const stripped = stripInjectionsForCompaction(this.messages);
1155
+ this.messages = stripped;
1156
+ await this.graphMemory.onCompacted(0);
1157
+ this.pendingPostCompactReinject = true;
1158
+ setConversationCleanedAt(this.conversationId, Date.now());
1159
+ const estimatedInputTokens = this.contextWindowManager.estimateInputTokens(
1160
+ this.messages,
1161
+ );
1162
+ return {
1163
+ previousEstimatedInputTokens,
1164
+ estimatedInputTokens,
1165
+ maxInputTokens: this.contextWindowManager.maxInputTokens,
1166
+ preservedMessages: this.messages.length,
1167
+ };
1168
+ }
1169
+
1129
1170
  setChannelCapabilities(caps: ChannelCapabilities | null): void {
1130
1171
  this.channelCapabilities = caps ?? undefined;
1131
1172
  this.secretPrompter.setChannelContext(
@@ -1158,10 +1199,6 @@ export class Conversation {
1158
1199
  this.transportHints = hints;
1159
1200
  }
1160
1201
 
1161
- setSlackRuntimeContextNotice(notice: string | undefined): void {
1162
- this.slackRuntimeContextNotice = notice;
1163
- }
1164
-
1165
1202
  /**
1166
1203
  * Apply client-reported host environment (home dir, username) from
1167
1204
  * transport metadata onto the conversation. Only interfaces whose