@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
@@ -31,6 +31,7 @@ import { type AbortReason, createAbortReason } from "../util/abort-reasons.js";
31
31
  import { getLogger } from "../util/logger.js";
32
32
  import { unregisterCallNotifiers } from "./conversation-notifiers.js";
33
33
  import type { MessageQueue } from "./conversation-queue-manager.js";
34
+ import { stripInjectionsForCompaction } from "./conversation-runtime-assembly.js";
34
35
  import { resetSkillToolProjection } from "./conversation-skill-tools.js";
35
36
  import { resolveTrustClass } from "./conversation-tool-setup.js";
36
37
  import { repairHistory } from "./history-repair.js";
@@ -188,6 +189,19 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
188
189
  ctx.contextCompactedAt = conv?.contextCompactedAt ?? null;
189
190
  }
190
191
 
192
+ // `/clean` persists a timestamp; messages older than this should skip
193
+ // metadata rehydration and have any injection prefixes still embedded in
194
+ // their content stripped, so the cleaned state survives reload and forks.
195
+ const cleanedAt = conv?.cleanedAt ?? null;
196
+ const slicedDbMessages = dbMessages.slice(ctx.contextCompactedMessageCount);
197
+ let preCleanCount = 0;
198
+ if (cleanedAt != null) {
199
+ const boundary = slicedDbMessages.findIndex(
200
+ (m) => m.createdAt >= cleanedAt,
201
+ );
202
+ preCleanCount = boundary === -1 ? slicedDbMessages.length : boundary;
203
+ }
204
+
191
205
  // Mirror the injection-time gate (`shouldExposePersonalMemory` in
192
206
  // `conversation-agent-loop.ts`) so background/local conversations
193
207
  // (sourceChannel `undefined` or `"vellum"`) can rehydrate the persisted
@@ -198,129 +212,141 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
198
212
  sourceChannel: ctx.trustContext?.sourceChannel,
199
213
  isTrustedActor: resolveTrustClass(ctx.trustContext) === "guardian",
200
214
  });
201
- const parsedMessages: Message[] = dbMessages
202
- .slice(ctx.contextCompactedMessageCount)
203
- .map((m, index, arr) => {
204
- const role = m.role as "user" | "assistant";
205
- let content: ContentBlock[];
215
+ const parsedMessages: Message[] = slicedDbMessages.map((m, index, arr) => {
216
+ const isPreClean = index < preCleanCount;
217
+ const role = m.role as "user" | "assistant";
218
+ let content: ContentBlock[];
219
+ try {
220
+ const parsed = JSON.parse(m.content);
221
+ content = Array.isArray(parsed)
222
+ ? parsed
223
+ : [{ type: "text", text: m.content }];
224
+ } catch {
225
+ log.warn(
226
+ { conversationId: ctx.conversationId, messageId: m.id },
227
+ "Invalid JSON in persisted message content, replacing with safe text block",
228
+ );
229
+ content = [{ type: "text", text: m.content }];
230
+ }
231
+
232
+ content = reinjectImageSourcePaths(content, role, m.metadata);
233
+
234
+ // Re-inject persisted injection blocks from metadata so it survives
235
+ // conversation reloads (eviction, restart, fork).
236
+ if (role === "user" && m.metadata && !isPreClean) {
206
237
  try {
207
- const parsed = JSON.parse(m.content);
208
- content = Array.isArray(parsed)
209
- ? parsed
210
- : [{ type: "text", text: m.content }];
211
- } catch {
212
- log.warn(
213
- { conversationId: ctx.conversationId, messageId: m.id },
214
- "Invalid JSON in persisted message content, replacing with safe text block",
215
- );
216
- content = [{ type: "text", text: m.content }];
217
- }
238
+ const meta = JSON.parse(m.metadata);
239
+ const isTail = index === arr.length - 1;
240
+
241
+ // Rehydrate in reverse injection order (innermost block first)
242
+ // so the resulting layout matches `applyRuntimeInjections`'s
243
+ // after-memory-prefix splices in ascending injector order
244
+ // (pkb-context 30, pkb-reminder 35, memory-v2-static 38,
245
+ // now-md 40 the v2 static block lands inside the memory
246
+ // prefix, so now-md splices *after* it):
247
+ // [<workspace>, <turn_context>, <memory __injected>,
248
+ // <memory>\n…</memory>, <NOW.md>, <system_reminder>,
249
+ // <knowledge_base>, ...original]
250
+ // Required so Anthropic's prefix cache keeps matching msg[0]
251
+ // across daemon restart and conversation eviction. The tail
252
+ // row only rehydrates `memoryInjectedBlock` — the next turn
253
+ // re-injects the rest fresh.
254
+ if (!isTail && typeof meta.pkbContextBlock === "string") {
255
+ content = [
256
+ { type: "text" as const, text: meta.pkbContextBlock },
257
+ ...content,
258
+ ];
259
+ }
260
+
261
+ if (!isTail && typeof meta.pkbSystemReminderBlock === "string") {
262
+ content = [
263
+ { type: "text" as const, text: meta.pkbSystemReminderBlock },
264
+ ...content,
265
+ ];
266
+ }
218
267
 
219
- content = reinjectImageSourcePaths(content, role, m.metadata);
268
+ if (!isTail && typeof meta.nowScratchpadBlock === "string") {
269
+ content = [
270
+ { type: "text" as const, text: meta.nowScratchpadBlock },
271
+ ...content,
272
+ ];
273
+ }
220
274
 
221
- // Re-inject persisted injection blocks from metadata so it survives
222
- // conversation reloads (eviction, restart, fork).
223
- if (role === "user" && m.metadata) {
224
- try {
225
- const meta = JSON.parse(m.metadata);
226
- const isTail = index === arr.length - 1;
227
-
228
- // Rehydrate in reverse injection order (innermost block first)
229
- // so the resulting layout matches `applyRuntimeInjections`'s
230
- // after-memory-prefix splices in ascending injector order
231
- // (pkb-context 30, pkb-reminder 35, memory-v2-static 38,
232
- // now-md 40 — the v2 static block lands inside the memory
233
- // prefix, so now-md splices *after* it):
234
- // [<workspace>, <turn_context>, <memory __injected>,
235
- // <memory>\n…</memory>, <NOW.md>, <system_reminder>,
236
- // <knowledge_base>, ...original]
237
- // Required so Anthropic's prefix cache keeps matching msg[0]
238
- // across daemon restart and conversation eviction. The tail
239
- // row only rehydrates `memoryInjectedBlock` — the next turn
240
- // re-injects the rest fresh.
241
- if (!isTail && typeof meta.pkbContextBlock === "string") {
242
- content = [
243
- { type: "text" as const, text: meta.pkbContextBlock },
244
- ...content,
245
- ];
246
- }
247
-
248
- if (!isTail && typeof meta.pkbSystemReminderBlock === "string") {
249
- content = [
250
- { type: "text" as const, text: meta.pkbSystemReminderBlock },
251
- ...content,
252
- ];
253
- }
254
-
255
- if (!isTail && typeof meta.nowScratchpadBlock === "string") {
256
- content = [
257
- { type: "text" as const, text: meta.nowScratchpadBlock },
258
- ...content,
259
- ];
260
- }
261
-
262
- // The v2 static memory block (essentials/threads/recent/buffer
263
- // wrapped in `<memory>…</memory>`) carries personal user memory.
264
- // Trust-gated to mirror `shouldExposePersonalMemory` at injection
265
- // time — untrusted-actor views must not read persisted personal
266
- // memory back through metadata. Skipped on the tail row because
267
- // the next turn re-injects fresh content on full-mode turns.
268
- if (
269
- !isTail &&
270
- personalMemoryAllowed &&
271
- typeof meta.memoryV2StaticBlock === "string"
272
- ) {
273
- content = [
274
- { type: "text" as const, text: meta.memoryV2StaticBlock },
275
- ...content,
276
- ];
277
- }
278
-
279
- // Memory remains rehydrated on all rows (existing behavior).
280
- // Strip any pre-existing wrapper before re-wrapping so historical
281
- // rows persisted with the wrapper (v2 path before the
282
- // injectedBlockText contract was unified with v1's unwrapped form)
283
- // don't render double-wrapped after rehydrate. Only unwrap when
284
- // the full <memory>...</memory> pair is present so we don't mutate
285
- // legitimate unwrapped payloads that happen to start with
286
- // "<memory>\n" or end with "\n</memory>".
287
- if (typeof meta.memoryInjectedBlock === "string") {
288
- const block = meta.memoryInjectedBlock;
289
- const inner =
290
- block.startsWith("<memory>\n") && block.endsWith("\n</memory>")
291
- ? block.slice("<memory>\n".length, -"\n</memory>".length)
292
- : block;
293
- content = [
294
- {
295
- type: "text" as const,
296
- text: `<memory>\n${inner}\n</memory>`,
297
- },
298
- ...content,
299
- ];
300
- }
301
-
302
- if (!isTail && typeof meta.turnContextBlock === "string") {
303
- content = [
304
- { type: "text" as const, text: meta.turnContextBlock },
305
- ...content,
306
- ];
307
- }
308
-
309
- if (!isTail && typeof meta.workspaceBlock === "string") {
310
- content = [
311
- { type: "text" as const, text: meta.workspaceBlock },
312
- ...content,
313
- ];
314
- }
315
- } catch {
316
- /* ignore parse errors — metadata may be malformed */
275
+ // The v2 static memory block (essentials/threads/recent/buffer
276
+ // wrapped in `<memory>…</memory>`) carries personal user memory.
277
+ // Trust-gated to mirror `shouldExposePersonalMemory` at injection
278
+ // time — untrusted-actor views must not read persisted personal
279
+ // memory back through metadata. Skipped on the tail row because
280
+ // the next turn re-injects fresh content on full-mode turns.
281
+ if (
282
+ !isTail &&
283
+ personalMemoryAllowed &&
284
+ typeof meta.memoryV2StaticBlock === "string"
285
+ ) {
286
+ content = [
287
+ { type: "text" as const, text: meta.memoryV2StaticBlock },
288
+ ...content,
289
+ ];
317
290
  }
291
+
292
+ // Memory remains rehydrated on all rows (existing behavior).
293
+ // Strip any pre-existing wrapper before re-wrapping so historical
294
+ // rows persisted with the wrapper (v2 path before the
295
+ // injectedBlockText contract was unified with v1's unwrapped form)
296
+ // don't render double-wrapped after rehydrate. Only unwrap when
297
+ // the full <memory>...</memory> pair is present so we don't mutate
298
+ // legitimate unwrapped payloads that happen to start with
299
+ // "<memory>\n" or end with "\n</memory>".
300
+ if (typeof meta.memoryInjectedBlock === "string") {
301
+ const block = meta.memoryInjectedBlock;
302
+ const inner =
303
+ block.startsWith("<memory>\n") && block.endsWith("\n</memory>")
304
+ ? block.slice("<memory>\n".length, -"\n</memory>".length)
305
+ : block;
306
+ content = [
307
+ {
308
+ type: "text" as const,
309
+ text: `<memory>\n${inner}\n</memory>`,
310
+ },
311
+ ...content,
312
+ ];
313
+ }
314
+
315
+ if (!isTail && typeof meta.turnContextBlock === "string") {
316
+ content = [
317
+ { type: "text" as const, text: meta.turnContextBlock },
318
+ ...content,
319
+ ];
320
+ }
321
+
322
+ if (!isTail && typeof meta.workspaceBlock === "string") {
323
+ content = [
324
+ { type: "text" as const, text: meta.workspaceBlock },
325
+ ...content,
326
+ ];
327
+ }
328
+ } catch {
329
+ /* ignore parse errors — metadata may be malformed */
318
330
  }
331
+ }
319
332
 
320
- return { role, content };
321
- });
333
+ return { role, content };
334
+ });
322
335
 
323
- const { messages: repairedMessages, stats } = repairHistory(parsedMessages);
336
+ // Strip pre-clean messages only; post-clean messages keep the fresh
337
+ // injections they were generated with.
338
+ const messagesBeforeRepair =
339
+ preCleanCount === 0
340
+ ? parsedMessages
341
+ : [
342
+ ...stripInjectionsForCompaction(
343
+ parsedMessages.slice(0, preCleanCount),
344
+ ),
345
+ ...parsedMessages.slice(preCleanCount),
346
+ ];
347
+
348
+ const { messages: repairedMessages, stats } =
349
+ repairHistory(messagesBeforeRepair);
324
350
  if (
325
351
  stats.assistantToolResultsMigrated > 0 ||
326
352
  stats.missingToolResultsInserted > 0 ||
@@ -36,6 +36,7 @@ import {
36
36
  updateMetaFile,
37
37
  } from "../memory/conversation-disk-view.js";
38
38
  import {
39
+ buildSlackTimezoneMetadata,
39
40
  type SlackMessageMetadata,
40
41
  writeSlackMetadata,
41
42
  } from "../messaging/providers/slack/message-metadata.js";
@@ -270,6 +271,7 @@ export function buildSlackMetaForPersistence(params: {
270
271
  const slackMeta: SlackMessageMetadata = {
271
272
  source: "slack",
272
273
  channelId: candidate.channelId,
274
+ ...(candidate.channelName ? { channelName: candidate.channelName } : {}),
273
275
  channelTs: candidate.channelTs,
274
276
  eventKind: "message",
275
277
  ...(candidate.threadTs ? { threadTs: candidate.threadTs } : {}),
@@ -277,6 +279,7 @@ export function buildSlackMetaForPersistence(params: {
277
279
  ...(candidate.actorExternalUserId
278
280
  ? { actorExternalUserId: candidate.actorExternalUserId }
279
281
  : {}),
282
+ ...buildSlackTimezoneMetadata(candidate),
280
283
  };
281
284
  return writeSlackMetadata(slackMeta);
282
285
  }
@@ -33,6 +33,7 @@ import type { Message } from "../providers/types.js";
33
33
  import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
34
34
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
35
35
  import { getLogger } from "../util/logger.js";
36
+ import type { CleanResult } from "./conversation.js";
36
37
  import {
37
38
  persistQueuedMessageBody,
38
39
  serializePersistedUserMessageContent,
@@ -84,6 +85,22 @@ export function formatCompactResult(result: ContextWindowResult): string {
84
85
  result.maxInputTokens,
85
86
  )} tokens`,
86
87
  `Messages: ${fmt(result.compactedMessages)} compacted`,
88
+ `Tail: ${fmt(result.preservedTailMessages)} preserved`,
89
+ ].join("\n");
90
+ }
91
+
92
+ /** Format the result of a forced clean into a user-facing message. */
93
+ export function formatCleanResult(result: CleanResult): string {
94
+ const fmt = (n: number | undefined) => (n ?? 0).toLocaleString("en-US");
95
+ const reclaimed =
96
+ result.previousEstimatedInputTokens - result.estimatedInputTokens;
97
+ return [
98
+ "Context Cleaned\n",
99
+ `Tokens: ${fmt(result.previousEstimatedInputTokens)} → ${fmt(result.estimatedInputTokens)} (${fmt(reclaimed)} reclaimed)`,
100
+ `Context: ${fmt(result.estimatedInputTokens)} / ${fmt(
101
+ result.maxInputTokens,
102
+ )} tokens`,
103
+ `Messages: ${fmt(result.preservedMessages)} preserved`,
87
104
  ].join("\n");
88
105
  }
89
106
 
@@ -122,6 +139,9 @@ export interface ProcessConversationContext {
122
139
  readonly surfaceActionRequestIds: Set<string>;
123
140
  currentActiveSurfaceId?: string;
124
141
  currentPage?: string;
142
+ /** When true, the drain path should inject synthetic tool_result messages
143
+ * for any pending tool_use blocks abandoned by a steered abort. */
144
+ pendingSteerRepair?: boolean;
125
145
  /** Cumulative token usage stats for the conversation. */
126
146
  readonly usageStats: UsageStats;
127
147
  /** Request-scoped skill IDs preactivated via config or programmatic injection. */
@@ -187,6 +207,8 @@ export interface ProcessConversationContext {
187
207
  forceCompact(options?: {
188
208
  targetInputTokensOverride?: number;
189
209
  }): Promise<ContextWindowResult>;
210
+ /** Strip runtime injections and reset memory-injection state. */
211
+ forceClean(): Promise<CleanResult>;
190
212
  /** Set transport-derived hints for the conversation. */
191
213
  setTransportHints(hints: string[] | undefined): void;
192
214
  /** IANA timezone reported by the active client for the current turn. */
@@ -346,6 +368,76 @@ async function buildPassthroughBatch(
346
368
  return conversation.queue.shiftN(matched);
347
369
  }
348
370
 
371
+ // ── Steer repair ────────────────────────────────────────────────────
372
+
373
+ /**
374
+ * When a steer-to-message abort interrupts an in-flight tool call, the
375
+ * conversation history may end with an assistant message containing one
376
+ * or more `tool_use` blocks that have no corresponding `tool_result`.
377
+ * LLM providers reject this sequence. This helper scans the tail of the
378
+ * history and injects synthetic error `tool_result` messages for any
379
+ * unmatched `tool_use` blocks.
380
+ */
381
+ function repairPendingToolUseBlocks(
382
+ conversation: ProcessConversationContext,
383
+ ): void {
384
+ if (!conversation.pendingSteerRepair) return;
385
+ conversation.pendingSteerRepair = false;
386
+
387
+ const messages = conversation.messages;
388
+ if (messages.length === 0) return;
389
+
390
+ // Walk backwards from the tail to find the last assistant message with
391
+ // tool_use blocks. Collect resolved IDs from any user messages between
392
+ // the tail and that assistant message, then subtract them.
393
+ const resolvedToolUseIds = new Set<string>();
394
+ const pendingToolUseIds: string[] = [];
395
+ for (let i = messages.length - 1; i >= 0; i--) {
396
+ const msg = messages[i];
397
+ if (msg.role === "user") {
398
+ for (const block of msg.content) {
399
+ if (
400
+ block.type === "tool_result" ||
401
+ block.type === "web_search_tool_result"
402
+ ) {
403
+ resolvedToolUseIds.add(block.tool_use_id);
404
+ }
405
+ }
406
+ } else if (msg.role === "assistant") {
407
+ for (const block of msg.content) {
408
+ if (block.type === "tool_use" && !resolvedToolUseIds.has(block.id)) {
409
+ pendingToolUseIds.push(block.id);
410
+ }
411
+ }
412
+ // Only repair tool_use blocks from the last assistant message that
413
+ // has them — earlier history should already be consistent.
414
+ break;
415
+ }
416
+ }
417
+
418
+ if (pendingToolUseIds.length === 0) return;
419
+
420
+ log.info(
421
+ {
422
+ conversationId: conversation.conversationId,
423
+ pendingToolUseCount: pendingToolUseIds.length,
424
+ },
425
+ "Injecting synthetic tool_result for pending tool_use blocks after steer",
426
+ );
427
+
428
+ // Build a single user message with tool_result blocks for all pending IDs.
429
+ const syntheticContent = pendingToolUseIds.map((toolUseId) => ({
430
+ type: "tool_result" as const,
431
+ tool_use_id: toolUseId,
432
+ content: "Tool execution was interrupted by user steering.",
433
+ is_error: true,
434
+ }));
435
+ conversation.messages.push({
436
+ role: "user",
437
+ content: syntheticContent,
438
+ });
439
+ }
440
+
349
441
  // ── drainQueue ───────────────────────────────────────────────────────
350
442
 
351
443
  /**
@@ -362,6 +454,20 @@ export async function drainQueue(
362
454
  conversation: ProcessConversationContext,
363
455
  reason: QueueDrainReason = "loop_complete",
364
456
  ): Promise<void> {
457
+ // After a steer, drain only the promoted head message — don't batch
458
+ // the remaining queue items into the same turn.
459
+ const steered = conversation.pendingSteerRepair;
460
+
461
+ // Repair any pending tool_use blocks left over from a steered abort
462
+ // before the drain path sends the next message to the LLM.
463
+ repairPendingToolUseBlocks(conversation);
464
+
465
+ if (steered) {
466
+ const next = conversation.queue.shift();
467
+ if (!next) return;
468
+ return drainSingleMessage(conversation, next, reason);
469
+ }
470
+
365
471
  const batch = await buildPassthroughBatch(conversation);
366
472
  if (batch.length === 0) {
367
473
  // Head is a slash / verification intent / empty queue. If the queue has
@@ -701,6 +807,94 @@ async function drainSingleMessage(
701
807
  return;
702
808
  }
703
809
 
810
+ // /clean — strip runtime injections and reset memory state, no LLM call.
811
+ if (slashResult.kind === "clean") {
812
+ let persistedCleanMessage = false;
813
+ try {
814
+ const drainProvenance = provenanceFromTrustContext(
815
+ conversation.trustContext,
816
+ );
817
+ const drainChannelMeta = {
818
+ ...drainProvenance,
819
+ ...(queuedTurnCtx
820
+ ? {
821
+ userMessageChannel: queuedTurnCtx.userMessageChannel,
822
+ assistantMessageChannel: queuedTurnCtx.assistantMessageChannel,
823
+ }
824
+ : {}),
825
+ ...(queuedInterfaceCtx
826
+ ? {
827
+ userMessageInterface: queuedInterfaceCtx.userMessageInterface,
828
+ assistantMessageInterface:
829
+ queuedInterfaceCtx.assistantMessageInterface,
830
+ }
831
+ : {}),
832
+ sentAt: next.sentAt,
833
+ };
834
+ const cleanUserMsg = createUserMessage(next.content, next.attachments);
835
+ await addMessage(
836
+ conversation.conversationId,
837
+ "user",
838
+ serializePersistedUserMessageContent(
839
+ next.content,
840
+ next.attachments,
841
+ next.displayContent,
842
+ ),
843
+ drainChannelMeta,
844
+ );
845
+ persistedCleanMessage = true;
846
+ conversation.messages.push(cleanUserMsg);
847
+
848
+ const result = await conversation.forceClean();
849
+ const responseText = formatCleanResult(result);
850
+
851
+ const assistantMsg = createAssistantMessage(responseText);
852
+ await addMessage(
853
+ conversation.conversationId,
854
+ "assistant",
855
+ JSON.stringify(assistantMsg.content),
856
+ { ...drainChannelMeta, sentAt: Date.now() },
857
+ );
858
+ conversation.messages.push(assistantMsg);
859
+
860
+ next.onEvent({
861
+ type: "assistant_text_delta",
862
+ text: responseText,
863
+ conversationId: conversation.conversationId,
864
+ });
865
+ conversation.traceEmitter.emit(
866
+ "message_complete",
867
+ "Clean slash command handled",
868
+ { requestId: next.requestId, status: "success" },
869
+ );
870
+ next.onEvent({
871
+ type: "message_complete",
872
+ conversationId: conversation.conversationId,
873
+ });
874
+ publishConversationMessagesChanged(conversation.conversationId);
875
+ } catch (err) {
876
+ if (persistedCleanMessage) {
877
+ publishConversationMessagesChanged(conversation.conversationId);
878
+ }
879
+ const message = err instanceof Error ? err.message : String(err);
880
+ log.error(
881
+ {
882
+ err,
883
+ conversationId: conversation.conversationId,
884
+ requestId: next.requestId,
885
+ },
886
+ "Failed to execute /clean",
887
+ );
888
+ next.onEvent({
889
+ type: "error",
890
+ conversationId: conversation.conversationId,
891
+ message,
892
+ });
893
+ }
894
+ await drainQueue(conversation);
895
+ return;
896
+ }
897
+
704
898
  const resolvedContent = slashResult.content;
705
899
 
706
900
  // Guardian verification intent interception for queued messages.
@@ -1560,6 +1754,85 @@ export async function processMessage(
1560
1754
  }
1561
1755
  }
1562
1756
 
1757
+ // /clean — strip runtime injections, return message ID. No LLM call.
1758
+ if (slashResult.kind === "clean") {
1759
+ conversation.processing = true;
1760
+ let persistedCleanMessage = false;
1761
+ try {
1762
+ const pmTurnCtx = conversation.getTurnChannelContext();
1763
+ const pmInterfaceCtx = conversation.getTurnInterfaceContext();
1764
+ const pmProvenance = provenanceFromTrustContext(
1765
+ conversation.trustContext,
1766
+ );
1767
+ const pmChannelMeta = {
1768
+ ...pmProvenance,
1769
+ ...(pmTurnCtx
1770
+ ? {
1771
+ userMessageChannel: pmTurnCtx.userMessageChannel,
1772
+ assistantMessageChannel: pmTurnCtx.assistantMessageChannel,
1773
+ }
1774
+ : {}),
1775
+ ...(pmInterfaceCtx
1776
+ ? {
1777
+ userMessageInterface: pmInterfaceCtx.userMessageInterface,
1778
+ assistantMessageInterface:
1779
+ pmInterfaceCtx.assistantMessageInterface,
1780
+ }
1781
+ : {}),
1782
+ };
1783
+ const cleanUserMsg = createUserMessage(content, attachments);
1784
+ const persisted = await addMessage(
1785
+ conversation.conversationId,
1786
+ "user",
1787
+ serializePersistedUserMessageContent(
1788
+ content,
1789
+ attachments,
1790
+ displayContent,
1791
+ ),
1792
+ pmChannelMeta,
1793
+ );
1794
+ persistedCleanMessage = true;
1795
+ conversation.messages.push(cleanUserMsg);
1796
+
1797
+ const result = await conversation.forceClean();
1798
+ const responseText = formatCleanResult(result);
1799
+
1800
+ const assistantMsg = createAssistantMessage(responseText);
1801
+ await addMessage(
1802
+ conversation.conversationId,
1803
+ "assistant",
1804
+ JSON.stringify(assistantMsg.content),
1805
+ pmChannelMeta,
1806
+ );
1807
+ conversation.messages.push(assistantMsg);
1808
+
1809
+ onEvent({
1810
+ type: "assistant_text_delta",
1811
+ text: responseText,
1812
+ conversationId: conversation.conversationId,
1813
+ });
1814
+ conversation.traceEmitter.emit(
1815
+ "message_complete",
1816
+ "Clean slash command handled",
1817
+ { requestId, status: "success" },
1818
+ );
1819
+ onEvent({
1820
+ type: "message_complete",
1821
+ conversationId: conversation.conversationId,
1822
+ });
1823
+ publishConversationMessagesChanged(conversation.conversationId);
1824
+ return persisted.id;
1825
+ } catch (err) {
1826
+ if (persistedCleanMessage) {
1827
+ publishConversationMessagesChanged(conversation.conversationId);
1828
+ }
1829
+ throw err;
1830
+ } finally {
1831
+ conversation.processing = false;
1832
+ await drainQueue(conversation);
1833
+ }
1834
+ }
1835
+
1563
1836
  const resolvedContent = slashResult.content;
1564
1837
 
1565
1838
  // Guardian verification intent interception — force direct guardian
@@ -158,6 +158,20 @@ export class MessageQueue {
158
158
  return this.currentBytes;
159
159
  }
160
160
 
161
+ /**
162
+ * Move a queued message to the head of the queue (index 0) by its requestId.
163
+ * Returns the promoted message, or undefined if not found.
164
+ * Byte accounting is unchanged — the item stays in the queue, just reordered.
165
+ */
166
+ promoteToHead(requestId: string): QueuedMessage | undefined {
167
+ const idx = this.items.findIndex((m) => m.requestId === requestId);
168
+ if (idx === -1) return undefined;
169
+ if (idx === 0) return this.items[0]; // already at head
170
+ const [promoted] = this.items.splice(idx, 1);
171
+ this.items.unshift(promoted);
172
+ return promoted;
173
+ }
174
+
161
175
  /**
162
176
  * Remove a queued message by its requestId.
163
177
  * Returns the removed message, or undefined if not found.