@vellumai/assistant 0.8.7 → 0.8.8-dev.202606052332.17fc8ea

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 (570) hide show
  1. package/Dockerfile +20 -4
  2. package/bun.lock +2 -2
  3. package/docker-entrypoint.sh +4 -2
  4. package/docker-init-apt-root.sh +3 -1
  5. package/docker-kata-apt-env.sh +3 -1
  6. package/docker-kata-runtime-family.sh +12 -0
  7. package/docs/architecture/memory.md +1 -1
  8. package/examples/plugins/echo/README.md +61 -66
  9. package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
  10. package/examples/plugins/echo/hooks/stop.ts +16 -0
  11. package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
  12. package/examples/plugins/echo/package.json +1 -2
  13. package/examples/plugins/echo/src/emit.ts +19 -0
  14. package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +3 -3
  15. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
  16. package/openapi.yaml +3378 -335
  17. package/package.json +2 -2
  18. package/scripts/generate-openapi.ts +68 -41
  19. package/src/__tests__/agent-loop-exit-reason.test.ts +35 -93
  20. package/src/__tests__/agent-loop-provider-error-recording.test.ts +1 -1
  21. package/src/__tests__/agent-loop.test.ts +37 -87
  22. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  23. package/src/__tests__/annotate-activity-metadata.test.ts +262 -0
  24. package/src/__tests__/annotate-risk-options.test.ts +2 -3
  25. package/src/__tests__/anthropic-provider.test.ts +95 -2
  26. package/src/__tests__/app-control-flow.test.ts +1 -1
  27. package/src/__tests__/app-dir-path-guard.test.ts +1 -0
  28. package/src/__tests__/approval-routes-http.test.ts +4 -1
  29. package/src/__tests__/assistant-event-hub.test.ts +25 -0
  30. package/src/__tests__/assistant-events-sse-shed.test.ts +8 -0
  31. package/src/__tests__/{conversation-stream-state.test.ts → assistant-stream-state.test.ts} +252 -91
  32. package/src/__tests__/auth-fallback-events-store.test.ts +116 -0
  33. package/src/__tests__/background-workers-disk-pressure.test.ts +6 -0
  34. package/src/__tests__/btw-routes.test.ts +62 -3
  35. package/src/__tests__/build-persisted-content.test.ts +184 -0
  36. package/src/__tests__/catalog-files.test.ts +1 -1
  37. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  38. package/src/__tests__/channel-approvals.test.ts +1 -1
  39. package/src/__tests__/clawhub-files.test.ts +1 -1
  40. package/src/__tests__/compaction-circuit.test.ts +258 -0
  41. package/src/__tests__/compaction-direct.test.ts +132 -0
  42. package/src/__tests__/compaction.benchmark.test.ts +0 -30
  43. package/src/__tests__/config-watcher.test.ts +1 -1
  44. package/src/__tests__/conversation-abort-tool-results.test.ts +57 -19
  45. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +6 -5
  46. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +10 -7
  47. package/src/__tests__/conversation-agent-loop-overflow.test.ts +316 -1143
  48. package/src/__tests__/conversation-agent-loop.test.ts +638 -1655
  49. package/src/__tests__/conversation-analysis-routes.test.ts +6 -0
  50. package/src/__tests__/conversation-clean-command.test.ts +5 -2
  51. package/src/__tests__/conversation-history-web-search.test.ts +11 -1
  52. package/src/__tests__/conversation-pairing.test.ts +4 -31
  53. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +6 -0
  54. package/src/__tests__/conversation-provider-retry-repair.test.ts +30 -10
  55. package/src/__tests__/conversation-queue.test.ts +2 -0
  56. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -0
  57. package/src/__tests__/conversation-routes-slash-commands.test.ts +6 -5
  58. package/src/__tests__/conversation-runtime-assembly.test.ts +310 -300
  59. package/src/__tests__/conversation-runtime-workspace.test.ts +105 -45
  60. package/src/__tests__/conversation-slash-commands.test.ts +8 -42
  61. package/src/__tests__/conversation-slash-queue.test.ts +6 -1
  62. package/src/__tests__/conversation-starter-routes.test.ts +14 -6
  63. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +84 -0
  64. package/src/__tests__/conversation-sync-tags.test.ts +27 -15
  65. package/src/__tests__/conversation-title-service.test.ts +135 -2
  66. package/src/__tests__/conversation-workspace-cache-state.test.ts +17 -16
  67. package/src/__tests__/conversation-workspace-injection.test.ts +67 -2
  68. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +7 -6
  69. package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
  70. package/src/__tests__/cross-provider-web-search.test.ts +214 -1
  71. package/src/__tests__/db-acp-history.test.ts +101 -0
  72. package/src/__tests__/db-schedule-syntax-migration.test.ts +5 -0
  73. package/src/__tests__/dm-persistence.test.ts +5 -1
  74. package/src/__tests__/dynamic-page-surface.test.ts +31 -0
  75. package/src/__tests__/empty-response-hook.test.ts +304 -0
  76. package/src/__tests__/feature-flag-test-helpers.ts +2 -2
  77. package/src/__tests__/file-write-tool.test.ts +63 -0
  78. package/src/__tests__/gateway-only-guard.test.ts +12 -2
  79. package/src/__tests__/gemini-image-service.test.ts +13 -0
  80. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  81. package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
  82. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  83. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  84. package/src/__tests__/heartbeat-service.test.ts +1 -0
  85. package/src/__tests__/helpers/mock-provider.ts +110 -0
  86. package/src/__tests__/helpers/native-web-search-harness.ts +129 -0
  87. package/src/__tests__/history-repair-hook.test.ts +1 -0
  88. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  89. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  90. package/src/__tests__/identity-intro-cache.test.ts +12 -100
  91. package/src/__tests__/identity-routes.test.ts +248 -7
  92. package/src/__tests__/inbound-slack-persistence.test.ts +5 -1
  93. package/src/__tests__/injector-background-turn.test.ts +3 -9
  94. package/src/__tests__/injector-chain.test.ts +139 -275
  95. package/src/__tests__/injector-disk-pressure.test.ts +75 -41
  96. package/src/__tests__/injector-document-comments.test.ts +3 -3
  97. package/src/__tests__/injector-pkb-v2-silenced.test.ts +30 -22
  98. package/src/__tests__/injector-v3-suppression.test.ts +31 -37
  99. package/src/__tests__/internal-telemetry-routes.test.ts +109 -0
  100. package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
  101. package/src/__tests__/list-messages-page-latest.test.ts +60 -0
  102. package/src/__tests__/list-messages-tool-merge.test.ts +20 -0
  103. package/src/__tests__/llm-usage-store.test.ts +223 -1
  104. package/src/__tests__/memory-retrieval-hook.test.ts +297 -0
  105. package/src/__tests__/memory-v2-static-injector.test.ts +103 -35
  106. package/src/__tests__/native-web-search.test.ts +191 -0
  107. package/src/__tests__/onboarding-template-contract.test.ts +2 -0
  108. package/src/__tests__/openai-image-service.test.ts +17 -0
  109. package/src/__tests__/openai-provider.test.ts +31 -1
  110. package/src/__tests__/{overflow-reduce-pipeline.test.ts → overflow-reduction-loop.test.ts} +64 -284
  111. package/src/__tests__/persist-unsendable-image.test.ts +215 -0
  112. package/src/__tests__/persistence-secret-redaction.test.ts +1 -0
  113. package/src/__tests__/pkb-autoinject.test.ts +2 -5
  114. package/src/__tests__/plugin-api-shim.test.ts +3 -6
  115. package/src/__tests__/plugin-bootstrap.test.ts +14 -40
  116. package/src/__tests__/plugin-registry.test.ts +3 -76
  117. package/src/__tests__/plugin-types.test.ts +0 -193
  118. package/src/__tests__/process-message-display-content.test.ts +6 -2
  119. package/src/__tests__/reaction-persistence.test.ts +1 -1
  120. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +5 -1
  121. package/src/__tests__/resolve-trust-class.test.ts +4 -4
  122. package/src/__tests__/runtime-events-sse-reconnect.test.ts +60 -23
  123. package/src/__tests__/schedule-routes.test.ts +603 -2
  124. package/src/__tests__/schedule-store.test.ts +41 -0
  125. package/src/__tests__/schedule-tools.test.ts +35 -0
  126. package/src/__tests__/send-endpoint-busy.test.ts +4 -1
  127. package/src/__tests__/server-history-render.test.ts +314 -1
  128. package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
  129. package/src/__tests__/skillssh-files.test.ts +1 -1
  130. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  131. package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
  132. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  133. package/src/__tests__/subagent-manager-notify.test.ts +1 -3
  134. package/src/__tests__/subagent-notify-parent.test.ts +1 -3
  135. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  136. package/src/__tests__/system-prompt.test.ts +20 -0
  137. package/src/__tests__/task-scheduler.test.ts +162 -1
  138. package/src/__tests__/terminal-tools.test.ts +6 -1
  139. package/src/__tests__/title-generate-hook.test.ts +319 -0
  140. package/src/__tests__/tool-error-hook.test.ts +278 -0
  141. package/src/__tests__/tool-preview-lifecycle.test.ts +468 -5
  142. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  143. package/src/__tests__/tool-result-truncate-hook.test.ts +127 -0
  144. package/src/__tests__/tool-result-truncation.test.ts +0 -2
  145. package/src/__tests__/ui-choice-copy-surfaces.test.ts +254 -0
  146. package/src/__tests__/ui-work-result-surface.test.ts +159 -0
  147. package/src/__tests__/usage-routes.test.ts +285 -1
  148. package/src/__tests__/user-plugin-loader.test.ts +54 -286
  149. package/src/__tests__/voice-session-bridge.test.ts +6 -3
  150. package/src/__tests__/web-search-backend-failure.test.ts +166 -0
  151. package/src/acp/__tests__/agent-process.test.ts +161 -0
  152. package/src/acp/__tests__/client-handler.test.ts +40 -0
  153. package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
  154. package/src/acp/__tests__/helpers/exec-file-stub.ts +101 -0
  155. package/src/acp/__tests__/prepare-agent-env.test.ts +137 -0
  156. package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
  157. package/src/acp/__tests__/session-manager-resume.test.ts +736 -0
  158. package/src/acp/agent-process.ts +61 -1
  159. package/src/acp/auto-install.test.ts +196 -0
  160. package/src/acp/auto-install.ts +177 -0
  161. package/src/acp/client-handler.ts +31 -0
  162. package/src/acp/feature-gate.test.ts +48 -0
  163. package/src/acp/feature-gate.ts +34 -0
  164. package/src/acp/prepare-agent-env.ts +83 -29
  165. package/src/acp/resolve-agent.test.ts +320 -7
  166. package/src/acp/resolve-agent.ts +182 -18
  167. package/src/acp/resume-hint.ts +25 -0
  168. package/src/acp/session-manager.ts +495 -73
  169. package/src/acp/types.ts +8 -0
  170. package/src/agent/compaction-circuit.ts +60 -102
  171. package/src/agent/loop.ts +362 -485
  172. package/src/api/events/assistant-thinking-delta.ts +33 -0
  173. package/src/api/events/tool-output-chunk.ts +45 -0
  174. package/src/api/events/tool-use-preview-start.ts +32 -0
  175. package/src/api/events/trace-event.ts +69 -0
  176. package/src/api/index.ts +48 -13
  177. package/src/api/responses/conversation-message.ts +374 -0
  178. package/src/approvals/guardian-request-resolvers.ts +1 -1
  179. package/src/avatar/__tests__/avatar-store.test.ts +34 -29
  180. package/src/background-wake/next-wake.ts +1 -0
  181. package/src/cli/commands/__tests__/notifications.test.ts +58 -14
  182. package/src/cli/commands/notifications.ts +112 -60
  183. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  184. package/src/config/acp-defaults.test.ts +10 -0
  185. package/src/config/acp-defaults.ts +6 -0
  186. package/src/config/assistant-feature-flags.ts +22 -11
  187. package/src/config/bundled-skills/acp/SKILL.md +83 -31
  188. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  189. package/src/config/bundled-skills/app-builder/SKILL.md +224 -398
  190. package/src/config/bundled-skills/app-builder/TOOLS.json +29 -0
  191. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
  192. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
  193. package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
  194. package/src/config/bundled-skills/app-builder/references/examples/README.md +17 -0
  195. package/src/config/bundled-skills/app-builder/references/examples/expense-tracker.md +515 -0
  196. package/src/config/bundled-skills/app-builder/references/examples/focus-timer.md +342 -0
  197. package/src/config/bundled-skills/app-builder/references/examples/habit-tracker.md +490 -0
  198. package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
  199. package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
  200. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  201. package/src/config/bundled-skills/messaging/SKILL.md +0 -7
  202. package/src/config/bundled-tool-registry.ts +2 -0
  203. package/src/config/feature-flag-cache.ts +3 -3
  204. package/src/config/feature-flag-registry.json +48 -7
  205. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -0
  207. package/src/config/schemas/heartbeat.ts +9 -0
  208. package/src/config/schemas/llm.ts +1 -0
  209. package/src/config/schemas/memory-v2.ts +8 -0
  210. package/src/config/schemas/memory-v3.ts +8 -0
  211. package/src/config/schemas/platform.ts +8 -0
  212. package/src/config/seed-inference-profiles.ts +2 -2
  213. package/src/config/skills.ts +13 -0
  214. package/src/context/compactor.ts +1 -1
  215. package/src/context/strip-injections.ts +128 -0
  216. package/src/context/token-estimator.ts +23 -0
  217. package/src/context/tool-result-truncation.ts +0 -23
  218. package/src/context/window-manager.ts +5 -7
  219. package/src/credential-execution/executable-discovery.ts +16 -0
  220. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +6 -0
  221. package/src/daemon/__tests__/inference-profile-notification.test.ts +153 -0
  222. package/src/daemon/__tests__/native-web-search-metadata.test.ts +10 -8
  223. package/src/daemon/assistant-attachments.ts +1 -1
  224. package/src/daemon/config-watcher.ts +2 -2
  225. package/src/daemon/context-overflow-reducer.ts +0 -1
  226. package/src/daemon/conversation-agent-loop-handlers.ts +594 -153
  227. package/src/daemon/conversation-agent-loop.ts +301 -997
  228. package/src/daemon/conversation-history.ts +5 -4
  229. package/src/daemon/conversation-lifecycle.ts +3 -4
  230. package/src/daemon/conversation-messaging.ts +7 -6
  231. package/src/daemon/conversation-process.ts +11 -16
  232. package/src/daemon/conversation-registry.ts +159 -0
  233. package/src/daemon/conversation-runtime-assembly.ts +218 -398
  234. package/src/daemon/conversation-slash.ts +6 -25
  235. package/src/daemon/conversation-store.ts +9 -90
  236. package/src/daemon/conversation-surfaces.ts +222 -4
  237. package/src/daemon/conversation-tool-setup.ts +2 -29
  238. package/src/daemon/conversation-workspace.ts +17 -0
  239. package/src/daemon/conversation.ts +32 -20
  240. package/src/daemon/external-plugins-bootstrap.ts +17 -18
  241. package/src/daemon/handlers/config-a2a.ts +51 -36
  242. package/src/daemon/handlers/config-slack-channel.ts +20 -14
  243. package/src/daemon/handlers/config-telegram.ts +16 -2
  244. package/src/daemon/handlers/conversations.ts +3 -1
  245. package/src/daemon/handlers/shared.ts +156 -84
  246. package/src/daemon/handlers/skills.ts +42 -10
  247. package/src/daemon/lifecycle.ts +25 -0
  248. package/src/daemon/message-types/apps.ts +1 -29
  249. package/src/daemon/message-types/messages.ts +9 -57
  250. package/src/daemon/message-types/skills.ts +2 -0
  251. package/src/daemon/message-types/surfaces.ts +136 -3
  252. package/src/daemon/now-scratchpad.ts +21 -0
  253. package/src/daemon/orphan-reaper.test.ts +210 -0
  254. package/src/daemon/orphan-reaper.ts +240 -0
  255. package/src/daemon/overflow-reduction-loop.ts +230 -0
  256. package/src/daemon/persist-unsendable-image.ts +117 -0
  257. package/src/daemon/process-message.ts +1 -3
  258. package/src/daemon/server.ts +2 -0
  259. package/src/daemon/trace-emitter.ts +6 -4
  260. package/src/daemon/trust-context.ts +19 -0
  261. package/src/daemon/wake-target-adapter.ts +3 -1
  262. package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
  263. package/src/heartbeat/heartbeat-run-store.ts +23 -1
  264. package/src/heartbeat/heartbeat-service.ts +26 -0
  265. package/src/home/home-greeting-cache.ts +24 -1
  266. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  267. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  268. package/src/ipc/gateway-client.test.ts +2 -2
  269. package/src/ipc/gateway-client.ts +3 -3
  270. package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
  271. package/src/ipc/skill-routes/memory.ts +4 -2
  272. package/src/media/gemini-image-service.ts +15 -0
  273. package/src/media/openai-image-service.ts +14 -0
  274. package/src/media/types.ts +34 -0
  275. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +56 -0
  276. package/src/memory/auth-fallback-events-store.ts +94 -0
  277. package/src/memory/conversation-starter-checkpoints.ts +1 -0
  278. package/src/memory/conversation-title-service.ts +65 -41
  279. package/src/memory/db-init.ts +6 -0
  280. package/src/memory/graph/__tests__/conversation-graph-memory-registry.test.ts +119 -0
  281. package/src/memory/graph/conversation-graph-memory.ts +65 -0
  282. package/src/memory/job-handlers/conversation-starters.ts +13 -2
  283. package/src/memory/jobs-store.ts +33 -0
  284. package/src/memory/jobs-worker.ts +32 -5
  285. package/src/memory/llm-usage-store.ts +224 -50
  286. package/src/memory/migrations/222-strip-placeholder-sentinels-from-messages.ts +6 -5
  287. package/src/memory/migrations/270-schedule-source-conversation.ts +13 -0
  288. package/src/memory/migrations/271-create-auth-fallback-events.ts +21 -0
  289. package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
  290. package/src/memory/migrations/index.ts +3 -0
  291. package/src/memory/pkb/autoinject.ts +61 -0
  292. package/src/memory/pkb/context.ts +50 -0
  293. package/src/memory/pkb/types.ts +14 -0
  294. package/src/memory/schedule-attribution-sql.ts +104 -0
  295. package/src/memory/schema/acp.ts +4 -0
  296. package/src/memory/schema/infrastructure.ts +16 -0
  297. package/src/memory/usage-grouped-buckets.ts +6 -1
  298. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -4
  299. package/src/memory/v2/consolidation-job.ts +14 -5
  300. package/src/notifications/conversation-pairing.ts +8 -15
  301. package/src/notifications/decision-engine.ts +6 -3
  302. package/src/notifications/home-feed-side-effect.ts +12 -1
  303. package/src/permissions/prompter.ts +4 -0
  304. package/src/plugin-api/constants.ts +4 -0
  305. package/src/plugin-api/index.ts +7 -5
  306. package/src/plugin-api/types.ts +151 -1
  307. package/src/plugins/defaults/compaction/compact.ts +59 -0
  308. package/src/plugins/defaults/compaction/package.json +1 -1
  309. package/src/plugins/defaults/compaction/register.ts +8 -19
  310. package/src/plugins/defaults/empty-response/hooks/stop.ts +126 -0
  311. package/src/plugins/defaults/empty-response/register.ts +8 -13
  312. package/src/plugins/defaults/index.ts +2 -18
  313. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +95 -0
  314. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +216 -0
  315. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +35 -0
  316. package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +288 -81
  317. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
  318. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +16 -0
  319. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +4 -4
  320. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
  321. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +48 -12
  322. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +13 -0
  323. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
  324. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
  325. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +104 -32
  326. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
  327. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +96 -30
  328. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +34 -16
  329. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +5 -5
  330. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +2 -2
  331. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
  332. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
  333. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
  334. package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
  335. package/src/plugins/defaults/memory-v3-shadow/llm-retry.ts +32 -0
  336. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +8 -8
  337. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +26 -14
  338. package/src/plugins/defaults/{llm-call → memory-v3-shadow}/package.json +2 -2
  339. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
  340. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +26 -0
  341. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +3 -3
  342. package/src/plugins/defaults/memory-v3-shadow/register.ts +26 -0
  343. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
  344. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +51 -45
  345. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
  346. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +61 -46
  347. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +69 -99
  348. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
  349. package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +8 -0
  350. package/src/plugins/defaults/title-generate/hooks/stop.ts +75 -0
  351. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +35 -0
  352. package/src/plugins/defaults/title-generate/package.json +1 -1
  353. package/src/plugins/defaults/title-generate/register.ts +18 -18
  354. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +118 -0
  355. package/src/plugins/defaults/tool-error/package.json +1 -1
  356. package/src/plugins/defaults/tool-error/register.ts +9 -21
  357. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +32 -0
  358. package/src/plugins/defaults/tool-result-truncate/register.ts +10 -21
  359. package/src/plugins/defaults/tool-result-truncate/terminal.ts +37 -18
  360. package/src/plugins/external-api.ts +2 -2
  361. package/src/plugins/pipeline.ts +6 -305
  362. package/src/plugins/registry.ts +10 -55
  363. package/src/plugins/types.ts +62 -797
  364. package/src/plugins/user-loader.ts +30 -127
  365. package/src/proactive-artifact/aux-message-injector.ts +4 -4
  366. package/src/proactive-artifact/job.test.ts +8 -13
  367. package/src/prompts/__tests__/system-prompt.test.ts +42 -0
  368. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +64 -0
  369. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  370. package/src/prompts/templates/system-sections.ts +15 -0
  371. package/src/providers/anthropic/client.ts +37 -29
  372. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +112 -0
  373. package/src/providers/openai/chat-completions-provider.ts +44 -0
  374. package/src/providers/openrouter/client.ts +1 -0
  375. package/src/providers/placeholder-sentinels.ts +35 -0
  376. package/src/runtime/__tests__/agent-wake.test.ts +10 -6
  377. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  378. package/src/runtime/agent-wake.ts +2 -5
  379. package/src/runtime/assistant-event-hub.ts +37 -7
  380. package/src/runtime/{conversation-stream-state.ts → assistant-stream-state.ts} +132 -58
  381. package/src/runtime/channel-approvals.ts +1 -1
  382. package/src/runtime/http-router.ts +16 -21
  383. package/src/runtime/http-types.ts +16 -70
  384. package/src/runtime/interactive-ui.ts +1 -1
  385. package/src/runtime/pending-interactions.ts +1 -0
  386. package/src/runtime/routes/__tests__/acp-routes.test.ts +283 -55
  387. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +265 -2
  388. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  389. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +31 -1
  390. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +6 -2
  391. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
  392. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
  393. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  394. package/src/runtime/routes/acp-routes.test.ts +89 -25
  395. package/src/runtime/routes/acp-routes.ts +81 -29
  396. package/src/runtime/routes/app-management-routes.ts +6 -117
  397. package/src/runtime/routes/app-routes.ts +13 -15
  398. package/src/runtime/routes/approval-routes.ts +1 -1
  399. package/src/runtime/routes/attachment-routes.ts +26 -15
  400. package/src/runtime/routes/avatar-routes.ts +26 -0
  401. package/src/runtime/routes/browser-routes.ts +1 -1
  402. package/src/runtime/routes/browser-tabs-routes.ts +6 -10
  403. package/src/runtime/routes/btw-routes.ts +29 -23
  404. package/src/runtime/routes/consolidation-routes.ts +120 -20
  405. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  406. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  407. package/src/runtime/routes/conversation-query-routes.ts +3 -1
  408. package/src/runtime/routes/conversation-routes.ts +372 -185
  409. package/src/runtime/routes/conversation-starter-routes.ts +13 -7
  410. package/src/runtime/routes/conversations-import-routes.ts +24 -7
  411. package/src/runtime/routes/documents-routes.ts +4 -0
  412. package/src/runtime/routes/domain-routes.ts +51 -37
  413. package/src/runtime/routes/epoch-millis-range.ts +34 -0
  414. package/src/runtime/routes/events-routes.ts +28 -34
  415. package/src/runtime/routes/gateway-log-routes.ts +26 -4
  416. package/src/runtime/routes/heartbeat-routes.ts +32 -12
  417. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  418. package/src/runtime/routes/host-cu-routes.ts +1 -1
  419. package/src/runtime/routes/identity-intro-cache.ts +11 -34
  420. package/src/runtime/routes/identity-routes.ts +224 -18
  421. package/src/runtime/routes/image-generation-routes.ts +40 -2
  422. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  423. package/src/runtime/routes/index.ts +2 -0
  424. package/src/runtime/routes/integrations/a2a.ts +12 -10
  425. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +16 -0
  426. package/src/runtime/routes/integrations/slack/channel.ts +4 -0
  427. package/src/runtime/routes/integrations/slack/share.ts +27 -6
  428. package/src/runtime/routes/integrations/telegram.ts +6 -0
  429. package/src/runtime/routes/integrations/twilio.ts +42 -0
  430. package/src/runtime/routes/internal-telemetry-routes.ts +88 -0
  431. package/src/runtime/routes/log-export-routes.ts +8 -0
  432. package/src/runtime/routes/memory-v2-routes.ts +15 -8
  433. package/src/runtime/routes/memory-v3-routes.ts +66 -34
  434. package/src/runtime/routes/oauth-apps.ts +66 -12
  435. package/src/runtime/routes/oauth-providers.ts +44 -5
  436. package/src/runtime/routes/platform-routes.ts +81 -5
  437. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +6 -4
  438. package/src/runtime/routes/playground/force-compact.ts +1 -1
  439. package/src/runtime/routes/playground/helpers.ts +1 -1
  440. package/src/runtime/routes/rename-conversation-routes.ts +5 -0
  441. package/src/runtime/routes/schedule-routes.ts +152 -42
  442. package/src/runtime/routes/secret-routes.ts +14 -2
  443. package/src/runtime/routes/skills-routes.ts +43 -14
  444. package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
  445. package/src/runtime/routes/tool-call-confirmation-enrichment.test.ts +161 -0
  446. package/src/runtime/routes/tool-call-confirmation-enrichment.ts +107 -0
  447. package/src/runtime/routes/trust-rules-routes.ts +26 -2
  448. package/src/runtime/routes/tts-routes.ts +35 -0
  449. package/src/runtime/routes/types.ts +66 -8
  450. package/src/runtime/routes/usage-routes.ts +47 -39
  451. package/src/runtime/routes/webhook-routes.ts +41 -2
  452. package/src/runtime/routes/work-items-routes.ts +2 -4
  453. package/src/runtime/routes/workspace-routes.ts +4 -0
  454. package/src/runtime/services/__tests__/analyze-conversation.test.ts +6 -0
  455. package/src/runtime/services/analyze-conversation.ts +2 -2
  456. package/src/runtime/services/conversation-serializer.ts +1 -1
  457. package/src/schedule/schedule-store.ts +20 -1
  458. package/src/schedule/schedule-usage-store.ts +83 -0
  459. package/src/schedule/scheduler.ts +12 -5
  460. package/src/signals/cancel.ts +2 -4
  461. package/src/skills/catalog-files.ts +2 -2
  462. package/src/skills/catalog-install.ts +3 -0
  463. package/src/skills/categories-cache.ts +118 -0
  464. package/src/skills/clawhub-files.ts +1 -2
  465. package/src/skills/skillssh-files.ts +1 -2
  466. package/src/subagent/manager.ts +17 -5
  467. package/src/telemetry/types.ts +29 -1
  468. package/src/telemetry/usage-telemetry-reporter.test.ts +112 -3
  469. package/src/telemetry/usage-telemetry-reporter.ts +57 -2
  470. package/src/tools/acp/context.ts +20 -0
  471. package/src/tools/acp/list-agents.test.ts +7 -1
  472. package/src/tools/acp/spawn.test.ts +158 -55
  473. package/src/tools/acp/spawn.ts +47 -72
  474. package/src/tools/acp/steer.test.ts +105 -8
  475. package/src/tools/acp/steer.ts +48 -17
  476. package/src/tools/apps/executors.ts +13 -8
  477. package/src/tools/executor.ts +1 -53
  478. package/src/tools/filesystem/write.ts +34 -0
  479. package/src/tools/network/__tests__/web-search-metadata.test.ts +7 -1
  480. package/src/tools/network/__tests__/web-search.test.ts +11 -3
  481. package/src/tools/network/web-search-error.test.ts +248 -0
  482. package/src/tools/network/web-search-error.ts +267 -0
  483. package/src/tools/network/web-search.ts +207 -48
  484. package/src/tools/schedule/create.ts +2 -0
  485. package/src/tools/subagent/spawn.ts +2 -4
  486. package/src/tools/terminal/safe-env.ts +10 -1
  487. package/src/tools/ui-surface/definitions.ts +34 -5
  488. package/src/tts/__tests__/provider-catalog-consistency.test.ts +85 -1
  489. package/src/tts/provider-catalog.ts +76 -1
  490. package/src/util/mutex.ts +47 -0
  491. package/src/workspace/git-service.ts +1 -42
  492. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
  493. package/src/workspace/migrations/095-bump-heartbeat-interval-30m-to-60m.ts +51 -0
  494. package/src/workspace/migrations/096-reduce-quality-profile-effort.ts +72 -0
  495. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +117 -0
  496. package/src/workspace/migrations/registry.ts +6 -0
  497. package/docs/plugins.md +0 -836
  498. package/examples/plugins/echo/register.ts +0 -184
  499. package/src/__tests__/bootstrap-turn-cleanup.test.ts +0 -44
  500. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
  501. package/src/__tests__/compaction-pipeline.test.ts +0 -210
  502. package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
  503. package/src/__tests__/empty-response-pipeline.test.ts +0 -423
  504. package/src/__tests__/llm-call-pipeline.test.ts +0 -287
  505. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -418
  506. package/src/__tests__/persistence-pipeline.test.ts +0 -503
  507. package/src/__tests__/pipeline-runner.test.ts +0 -564
  508. package/src/__tests__/title-generate-pipeline.test.ts +0 -211
  509. package/src/__tests__/token-estimate-pipeline.test.ts +0 -479
  510. package/src/__tests__/tool-error-pipeline.test.ts +0 -241
  511. package/src/__tests__/tool-execute-pipeline.test.ts +0 -417
  512. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -341
  513. package/src/daemon/bootstrap-turn-cleanup.ts +0 -45
  514. package/src/gallery/default-gallery.ts +0 -1359
  515. package/src/gallery/gallery-manifest.ts +0 -28
  516. package/src/home/feature-gate.ts +0 -22
  517. package/src/memory/v3/provider-blocks.ts +0 -16
  518. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
  519. package/src/plugins/defaults/circuit-breaker/package.json +0 -15
  520. package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
  521. package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
  522. package/src/plugins/defaults/compaction/terminal.ts +0 -73
  523. package/src/plugins/defaults/empty-response/middlewares/emptyResponse.ts +0 -22
  524. package/src/plugins/defaults/empty-response/terminal.ts +0 -106
  525. package/src/plugins/defaults/injectors/package.json +0 -15
  526. package/src/plugins/defaults/llm-call/middlewares/llmCall.ts +0 -17
  527. package/src/plugins/defaults/llm-call/register.ts +0 -45
  528. package/src/plugins/defaults/memory-retrieval/middlewares/memoryRetrieval.ts +0 -17
  529. package/src/plugins/defaults/memory-retrieval/package.json +0 -15
  530. package/src/plugins/defaults/memory-retrieval/register.ts +0 -181
  531. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
  532. package/src/plugins/defaults/overflow-reduce/package.json +0 -15
  533. package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
  534. package/src/plugins/defaults/persistence/middlewares/persistence.ts +0 -19
  535. package/src/plugins/defaults/persistence/package.json +0 -15
  536. package/src/plugins/defaults/persistence/register.ts +0 -38
  537. package/src/plugins/defaults/persistence/terminal.ts +0 -83
  538. package/src/plugins/defaults/title-generate/terminal.ts +0 -31
  539. package/src/plugins/defaults/token-estimate/middlewares/tokenEstimate.ts +0 -23
  540. package/src/plugins/defaults/token-estimate/package.json +0 -15
  541. package/src/plugins/defaults/token-estimate/register.ts +0 -34
  542. package/src/plugins/defaults/token-estimate/terminal.ts +0 -40
  543. package/src/plugins/defaults/tool-error/middlewares/toolError.ts +0 -21
  544. package/src/plugins/defaults/tool-error/terminal.ts +0 -47
  545. package/src/plugins/defaults/tool-execute/middlewares/toolExecute.ts +0 -23
  546. package/src/plugins/defaults/tool-execute/package.json +0 -15
  547. package/src/plugins/defaults/tool-execute/register.ts +0 -49
  548. package/src/plugins/defaults/tool-result-truncate/middlewares/toolResultTruncate.ts +0 -23
  549. package/src/plugins/defaults/tool-result-truncate/types.ts +0 -22
  550. package/src/skills/category-inference.ts +0 -111
  551. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
  552. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
  553. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
  554. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
  555. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
  556. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
  557. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
  558. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +0 -0
  559. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
  560. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
  561. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
  562. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
  563. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
  564. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
  565. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
  566. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
  567. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
  568. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
  569. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
  570. /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
@@ -10,6 +10,7 @@ import {
10
10
  createAssistantMessage,
11
11
  createUserMessage,
12
12
  } from "../../agent/message-types.js";
13
+ import { ConversationMessageSchema } from "../../api/responses/conversation-message.js";
13
14
  import {
14
15
  CHANNEL_IDS,
15
16
  INTERFACE_IDS,
@@ -46,7 +47,11 @@ import {
46
47
  getCannedFirstGreeting,
47
48
  isWakeUpGreeting,
48
49
  } from "../../daemon/first-greeting.js";
49
- import { renderHistoryContent } from "../../daemon/handlers/shared.js";
50
+ import {
51
+ collectAttachmentRefs,
52
+ type HistoryAttachmentRef,
53
+ renderHistoryContent,
54
+ } from "../../daemon/handlers/shared.js";
50
55
  import { HostAppControlProxy } from "../../daemon/host-app-control-proxy.js";
51
56
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
52
57
  import {
@@ -105,10 +110,14 @@ import type { Provider } from "../../providers/types.js";
105
110
  import { checkIngressForSecrets } from "../../security/secret-ingress.js";
106
111
  import { getSubagentManager } from "../../subagent/index.js";
107
112
  import { getLogger } from "../../util/logger.js";
108
- import { getWorkspacePromptPath } from "../../util/platform.js";
113
+ import {
114
+ getWorkspaceDir,
115
+ getWorkspacePromptPath,
116
+ } from "../../util/platform.js";
109
117
  import { silentlyWithLog } from "../../util/silently.js";
110
118
  import { assistantEventHub, broadcastMessage } from "../assistant-event-hub.js";
111
119
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
120
+ import { getPersistedSeq } from "../assistant-stream-state.js";
112
121
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
113
122
  import { routeGuardianReply } from "../guardian-reply-router.js";
114
123
  import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
@@ -134,6 +143,10 @@ import {
134
143
  NotFoundError,
135
144
  RouteError,
136
145
  } from "./errors.js";
146
+ import {
147
+ collectPendingConfirmations,
148
+ enrichToolCallsWithConfirmation,
149
+ } from "./tool-call-confirmation-enrichment.js";
137
150
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
138
151
  import { RouteResponse } from "./types.js";
139
152
 
@@ -143,6 +156,125 @@ const log = getLogger("conversation-routes");
143
156
  const NO_RESPONSE_INLINE_RE = /<no_response\s*\/?>/g;
144
157
  const ATTACHMENT_ENTRY_RE = /^attachment:(\d+)$/;
145
158
 
159
+ /** Rewrites a rendered `contentOrder` to reflect attachment alignment. */
160
+ type ContentOrderRewrite = (contentOrder: string[]) => string[];
161
+
162
+ interface AlignedAttachments {
163
+ /** Hydrated rows, reordered to match the inline file-block order. */
164
+ attachments: RuntimeAttachmentMetadata[];
165
+ /**
166
+ * Resolves a content-walk attachment ref index to its hydrated DB row,
167
+ * mirroring the inline placement `rewriteContentOrder` encodes. Refs with no
168
+ * inline placement (unmatched ids, count mismatch, no DB rows) are absent, so
169
+ * `renderHistoryContent` emits no `attachment` block for them — the row still
170
+ * ships via the flat `attachments` array.
171
+ */
172
+ refIndexToAttachment: Map<number, RuntimeAttachmentMetadata>;
173
+ rewriteContentOrder: ContentOrderRewrite;
174
+ }
175
+
176
+ /**
177
+ * Align DB-hydrated attachment rows with the file-block refs `renderHistoryContent`
178
+ * captured. When a file block was persisted with `_attachmentId` (user-message
179
+ * uploads) we join on that id to position the chip inline; DB rows without a
180
+ * matching ref go to the tail as orphan chips, and unmatched refs drop their
181
+ * `attachment:N` entry. Assistant-authored file blocks carry no `_attachmentId`,
182
+ * so when no ids match we fall back to positional alignment if the ref and row
183
+ * counts agree; otherwise we strip the markers and let chips fall to the tail.
184
+ */
185
+ function alignAttachments(
186
+ attachmentRefs: HistoryAttachmentRef[],
187
+ attachments: RuntimeAttachmentMetadata[],
188
+ ): AlignedAttachments {
189
+ const refIndexToAttachment = new Map<number, RuntimeAttachmentMetadata>();
190
+ const identity: ContentOrderRewrite = (contentOrder) => contentOrder;
191
+ const stripAttachmentEntries: ContentOrderRewrite = (contentOrder) =>
192
+ contentOrder.filter((entry) => !ATTACHMENT_ENTRY_RE.test(entry));
193
+
194
+ if (attachmentRefs.length === 0) {
195
+ return { attachments, refIndexToAttachment, rewriteContentOrder: identity };
196
+ }
197
+ if (attachments.length === 0) {
198
+ // Refs were captured but no DB rows came back — drop the contentOrder
199
+ // entries to avoid out-of-bounds renders.
200
+ return {
201
+ attachments,
202
+ refIndexToAttachment,
203
+ rewriteContentOrder: stripAttachmentEntries,
204
+ };
205
+ }
206
+
207
+ const byId = new Map<string, number>();
208
+ attachments.forEach((att, idx) => {
209
+ if (att.id) byId.set(att.id, idx);
210
+ });
211
+ const consumed = new Set<number>();
212
+ const orderedRowIdx: Array<number | null> = attachmentRefs.map((ref) => {
213
+ if (!ref.attachmentId) return null;
214
+ const idx = byId.get(ref.attachmentId);
215
+ if (idx === undefined || consumed.has(idx)) return null;
216
+ consumed.add(idx);
217
+ return idx;
218
+ });
219
+ const matchedRows = orderedRowIdx.filter(
220
+ (idx): idx is number => idx !== null,
221
+ );
222
+
223
+ if (matchedRows.length > 0) {
224
+ const orphanRows: number[] = [];
225
+ for (let i = 0; i < attachments.length; i++) {
226
+ if (!consumed.has(i)) orphanRows.push(i);
227
+ }
228
+ const reordered = [
229
+ ...matchedRows.map((i) => attachments[i]),
230
+ ...orphanRows.map((i) => attachments[i]),
231
+ ];
232
+ const refToNewIdx = new Map<number, number>();
233
+ let nextIdx = 0;
234
+ orderedRowIdx.forEach((rowIdx, refIdx) => {
235
+ if (rowIdx !== null) {
236
+ refToNewIdx.set(refIdx, nextIdx);
237
+ refIndexToAttachment.set(refIdx, reordered[nextIdx]);
238
+ nextIdx++;
239
+ }
240
+ });
241
+ const rewriteContentOrder: ContentOrderRewrite = (contentOrder) =>
242
+ contentOrder
243
+ .map((entry) => {
244
+ const match = entry.match(ATTACHMENT_ENTRY_RE);
245
+ if (!match) return entry;
246
+ const remapped = refToNewIdx.get(Number(match[1]));
247
+ return remapped !== undefined ? `attachment:${remapped}` : undefined;
248
+ })
249
+ .filter((e): e is string => e !== undefined);
250
+ return {
251
+ attachments: reordered,
252
+ refIndexToAttachment,
253
+ rewriteContentOrder,
254
+ };
255
+ }
256
+
257
+ if (attachmentRefs.length !== attachments.length) {
258
+ // No ref carried an attachmentId we could match and the counts disagree, so
259
+ // positional mapping can't be trusted — strip any attachment:N entries so
260
+ // the client doesn't position attachments inline against a misaligned array
261
+ // (they fall to the tail instead).
262
+ return {
263
+ attachments,
264
+ refIndexToAttachment,
265
+ rewriteContentOrder: stripAttachmentEntries,
266
+ };
267
+ }
268
+
269
+ // No ref matched an id but the counts agree (the assistant-authored case):
270
+ // the Nth marker maps to the Nth row positionally, so the original
271
+ // contentOrder is left untouched.
272
+ attachmentRefs.forEach((_ref, refIdx) => {
273
+ refIndexToAttachment.set(refIdx, attachments[refIdx]);
274
+ });
275
+ return { attachments, refIndexToAttachment, rewriteContentOrder: identity };
276
+ }
277
+
146
278
  /** Feature flag gating the self-intro first message (see first-greeting.ts). */
147
279
  const SELF_INTRO_GREETING_FLAG = "self-intro-greeting" as const;
148
280
 
@@ -509,6 +641,7 @@ export function handleListMessages({
509
641
  hasMore: false,
510
642
  oldestTimestamp: null,
511
643
  oldestMessageId: null,
644
+ seq: null,
512
645
  };
513
646
  }
514
647
  return { messages: [] };
@@ -547,7 +680,14 @@ export function handleListMessages({
547
680
  // assistant message has tool_use blocks but its matching user tool_result
548
681
  // is left visible, the result will render as a standalone orphan because
549
682
  // `mergeToolResultsIntoAssistantMessages` has nothing to merge it into.
550
- const visibleFilter = (m: MessageRow) => !isHiddenMessage(m.metadata);
683
+ //
684
+ // Only renderable roles reach this UI-facing transcript. `system` rows (a
685
+ // permitted `MessageRole`, e.g. skill-authored context) are agent-context
686
+ // scaffolding, never a displayed turn, so they are dropped here at the
687
+ // source rather than narrowed away per-client.
688
+ const visibleFilter = (m: MessageRow) =>
689
+ !isHiddenMessage(m.metadata) &&
690
+ (m.role === "user" || m.role === "assistant");
551
691
 
552
692
  if (isPaginated) {
553
693
  const result = getMessagesPaginated(
@@ -581,7 +721,10 @@ export function handleListMessages({
581
721
  mergeConsecutiveAssistantMessages(mergedMessages);
582
722
  const assistantSlackDisplayName = getAssistantName()?.trim() || undefined;
583
723
 
584
- // Parse content blocks and extract text + tool calls
724
+ // Parse each row's stored content and per-message metadata. Rendering is
725
+ // deferred to the serializer pass below so it runs after attachment
726
+ // alignment, letting renderHistoryContent inline `attachment` blocks during
727
+ // its single content walk.
585
728
  const parsed = consolidatedMessages.map((msg) => {
586
729
  let content: unknown;
587
730
  try {
@@ -589,7 +732,6 @@ export function handleListMessages({
589
732
  } catch {
590
733
  content = msg.content;
591
734
  }
592
- const rendered = renderHistoryContent(content);
593
735
 
594
736
  // Extract sentAt from metadata for display timestamps. When a message
595
737
  // was queued or its persistence was delayed (long assistant generation),
@@ -637,95 +779,51 @@ export function handleListMessages({
637
779
  },
638
780
  );
639
781
 
640
- // Strip <no_response/> markers from assistant messages so web/API
641
- // clients never see the raw sentinel. Only assistant messages produce
642
- // this marker; user messages are left untouched.
643
- if (msg.role === "assistant") {
644
- const originalSegments = rendered.textSegments;
645
- const keepIndices: number[] = [];
646
- const filteredSegments: string[] = [];
647
- for (let i = 0; i < originalSegments.length; i++) {
648
- const cleaned = originalSegments[i]
649
- .replace(NO_RESPONSE_INLINE_RE, "")
650
- .trim();
651
- if (cleaned.length > 0) {
652
- keepIndices.push(i);
653
- filteredSegments.push(cleaned);
654
- }
655
- }
656
- // Remap contentOrder text:N indices to account for removed segments
657
- const indexMap = new Map<number, number>();
658
- keepIndices.forEach((oldIdx, newIdx) => indexMap.set(oldIdx, newIdx));
659
- const filteredContentOrder = rendered.contentOrder
660
- .map((entry) => {
661
- const m = entry.match(/^text:(\d+)$/);
662
- if (!m) return entry;
663
- const newIdx = indexMap.get(Number(m[1]));
664
- return newIdx !== undefined ? `text:${newIdx}` : undefined;
665
- })
666
- .filter((e): e is string => e !== undefined);
667
-
668
- return {
669
- role: msg.role,
670
- text: rendered.text.replace(NO_RESPONSE_INLINE_RE, "").trim(),
671
- timestamp: msg.createdAt,
672
- sentAt,
673
- toolCalls: rendered.toolCalls,
674
- toolCallsBeforeText: rendered.toolCallsBeforeText,
675
- textSegments: filteredSegments,
676
- contentOrder: filteredContentOrder,
677
- surfaces: rendered.surfaces,
678
- attachmentRefs: rendered.attachments,
679
- slackMessage,
680
- ...(rendered.thinkingSegments.length > 0
681
- ? { thinkingSegments: rendered.thinkingSegments }
682
- : {}),
683
- id: msg.id,
684
- subagentNotification,
685
- };
686
- }
782
+ // `visibleFilter` has already dropped every non-renderable role, so the
783
+ // only values reaching here are `user` and `assistant`; narrow the raw DB
784
+ // `string` to the wire union.
785
+ const role: "user" | "assistant" =
786
+ msg.role === "assistant" ? "assistant" : "user";
687
787
 
688
788
  return {
689
- role: msg.role,
690
- text: rendered.text,
691
- timestamp: msg.createdAt,
692
- sentAt,
693
- toolCalls: rendered.toolCalls,
694
- toolCallsBeforeText: rendered.toolCallsBeforeText,
695
- textSegments: rendered.textSegments,
696
- contentOrder: rendered.contentOrder,
697
- surfaces: rendered.surfaces,
698
- attachmentRefs: rendered.attachments,
699
- slackMessage,
700
- ...(rendered.thinkingSegments.length > 0
701
- ? { thinkingSegments: rendered.thinkingSegments }
702
- : {}),
703
789
  id: msg.id,
790
+ role,
791
+ content,
792
+ createdAt: msg.createdAt,
793
+ sentAt,
704
794
  subagentNotification,
795
+ slackMessage,
705
796
  };
706
797
  });
707
798
 
799
+ // Confirmation context layered onto rendered tool calls at render time: the
800
+ // derived scope ladder for scope-aware tools, and any in-flight prompt read
801
+ // from the pending-interactions registry. Both are computed once per request
802
+ // and applied per message below.
803
+ const workspaceDir = getWorkspaceDir();
804
+ const pendingConfirmations = collectPendingConfirmations(
805
+ resolvedConversationId,
806
+ );
807
+
708
808
  const messages: RuntimeMessagePayload[] = parsed.map((m) => {
709
809
  const mergedMessageIds = m.id ? (mergedIdMap.get(m.id) ?? []) : [];
810
+
811
+ // Hydrate the row's attachments from the DB. A metadata-only query avoids
812
+ // loading large base64 blobs for non-image attachments (documents, audio);
813
+ // full data is fetched only for images so the client can generate
814
+ // thumbnails for inline display on history restore. Merged messages
815
+ // (consecutive assistant merge) are queried too so their attachments
816
+ // aren't lost before DB compaction relinks them.
710
817
  let msgAttachments: RuntimeAttachmentMetadata[] = [];
711
818
  if (m.id) {
712
- // Use metadata-only query first to avoid loading large base64
713
- // blobs for non-image attachments (documents, audio). Then
714
- // selectively fetch full data only for images so the client can
715
- // generate thumbnails for inline display on history restore.
716
- // Also query attachments for any messages that were merged into
717
- // this one (consecutive assistant merge), so their attachments
718
- // aren't lost before DB compaction relinks them.
719
- const idsToQuery = [m.id, ...(mergedIdMap.get(m.id) ?? [])];
819
+ const idsToQuery = [m.id, ...mergedMessageIds];
720
820
  const linked = idsToQuery.flatMap((id) =>
721
821
  getAttachmentMetadataForMessage(id),
722
822
  );
723
823
  if (linked.length > 0) {
724
824
  msgAttachments = linked.map((a) => {
725
825
  if (a.mimeType.startsWith("image/")) {
726
- const full = getAttachmentById(a.id, {
727
- hydrateFileData: true,
728
- });
826
+ const full = getAttachmentById(a.id, { hydrateFileData: true });
729
827
  return {
730
828
  id: a.id,
731
829
  filename: a.originalFilename,
@@ -752,107 +850,101 @@ export function handleListMessages({
752
850
  }
753
851
  }
754
852
 
755
- // Align msgAttachments order with the file-block order captured by
756
- // renderHistoryContent. When a file block was persisted with
757
- // `_attachmentId` (user-message uploads), we join on that id to position
758
- // the chip inline (the `attachment:N` entries in contentOrder index into
759
- // msgAttachments). DB rows without a matching ref go to the tail as orphan
760
- // chips; unmatched refs drop their contentOrder entry and trigger a remap.
761
- // Assistant-authored file blocks carry no `_attachmentId`, so when no ids
762
- // match we fall back to positional alignment if the ref and row counts
763
- // agree; otherwise we strip the markers and let chips fall to the tail.
764
- let alignedContentOrder = m.contentOrder;
765
- if (
766
- m.attachmentRefs.length > 0 &&
767
- msgAttachments.length > 0 &&
768
- m.contentOrder.length > 0
769
- ) {
770
- const byId = new Map<string, number>();
771
- msgAttachments.forEach((att, idx) => {
772
- if (att.id) byId.set(att.id, idx);
773
- });
774
- const consumed = new Set<number>();
775
- const orderedRowIdx: Array<number | null> = m.attachmentRefs.map(
776
- (ref) => {
777
- if (!ref.attachmentId) return null;
778
- const idx = byId.get(ref.attachmentId);
779
- if (idx === undefined || consumed.has(idx)) return null;
780
- consumed.add(idx);
781
- return idx;
782
- },
783
- );
784
- const matchedRows = orderedRowIdx.filter(
785
- (idx): idx is number => idx !== null,
786
- );
787
- if (matchedRows.length > 0) {
788
- const orphanRows: number[] = [];
789
- for (let i = 0; i < msgAttachments.length; i++) {
790
- if (!consumed.has(i)) orphanRows.push(i);
853
+ // Align the hydrated rows with the file-block refs, then render. Rendering
854
+ // after alignment lets renderHistoryContent inline each `attachment` block
855
+ // during its single content walk, so `contentBlocks` comes back ready to
856
+ // ship with no post-processing. The aligned reorder/rewrite keeps the
857
+ // legacy `attachments` array and `contentOrder` positions consistent.
858
+ const attachmentRefs = collectAttachmentRefs(m.content);
859
+ const aligned = alignAttachments(attachmentRefs, msgAttachments);
860
+ msgAttachments = aligned.attachments;
861
+ const attachmentBlocks = attachmentRefs.map(
862
+ (_ref, refIdx) => aligned.refIndexToAttachment.get(refIdx) ?? null,
863
+ );
864
+ const rendered = renderHistoryContent(
865
+ m.content,
866
+ attachmentBlocks,
867
+ m.id ?? undefined,
868
+ );
869
+
870
+ const toolCalls = enrichToolCallsWithConfirmation(rendered.toolCalls, {
871
+ workspaceDir,
872
+ pendingConfirmations,
873
+ });
874
+
875
+ // Strip <no_response/> markers from assistant messages so web/API clients
876
+ // never see the raw sentinel. Only assistant messages produce it; user
877
+ // messages are untouched. The filter is applied consistently to the flat
878
+ // text, the segments, the contentOrder text refs, and the text blocks of
879
+ // contentBlocks.
880
+ let text = rendered.text;
881
+ let textSegments = rendered.textSegments;
882
+ let contentOrder = rendered.contentOrder;
883
+ let contentBlocks = rendered.contentBlocks;
884
+ if (m.role === "assistant") {
885
+ const keepIndices: number[] = [];
886
+ const filteredSegments: string[] = [];
887
+ for (let i = 0; i < rendered.textSegments.length; i++) {
888
+ const cleaned = rendered.textSegments[i]
889
+ .replace(NO_RESPONSE_INLINE_RE, "")
890
+ .trim();
891
+ if (cleaned.length > 0) {
892
+ keepIndices.push(i);
893
+ filteredSegments.push(cleaned);
791
894
  }
792
- msgAttachments = [
793
- ...matchedRows.map((i) => msgAttachments[i]),
794
- ...orphanRows.map((i) => msgAttachments[i]),
795
- ];
796
- const refToNewIdx = new Map<number, number>();
797
- let nextIdx = 0;
798
- orderedRowIdx.forEach((rowIdx, refIdx) => {
799
- if (rowIdx !== null) {
800
- refToNewIdx.set(refIdx, nextIdx);
801
- nextIdx++;
802
- }
803
- });
804
- alignedContentOrder = m.contentOrder
805
- .map((entry) => {
806
- const match = entry.match(ATTACHMENT_ENTRY_RE);
807
- if (!match) return entry;
808
- const remapped = refToNewIdx.get(Number(match[1]));
809
- return remapped !== undefined
810
- ? `attachment:${remapped}`
811
- : undefined;
812
- })
813
- .filter((e): e is string => e !== undefined);
814
- } else if (m.attachmentRefs.length !== msgAttachments.length) {
815
- // No ref carried an attachmentId we could match and the counts
816
- // disagree, so positional mapping can't be trusted — strip any
817
- // attachment:N entries so the client doesn't position attachments
818
- // inline against a misaligned array (they fall to the tail instead).
819
- alignedContentOrder = m.contentOrder.filter(
820
- (entry) => !ATTACHMENT_ENTRY_RE.test(entry),
821
- );
822
895
  }
823
- // Otherwise no ref matched an id but the counts agree (the
824
- // assistant-authored case): the Nth marker maps to the Nth row
825
- // positionally, so the original contentOrder is left untouched.
826
- } else if (m.attachmentRefs.length > 0 && msgAttachments.length === 0) {
827
- // Refs were captured but no DB rows came back — drop the
828
- // contentOrder entries to avoid out-of-bounds renders.
829
- alignedContentOrder = m.contentOrder.filter(
830
- (entry) => !ATTACHMENT_ENTRY_RE.test(entry),
831
- );
896
+ const indexMap = new Map<number, number>();
897
+ keepIndices.forEach((oldIdx, newIdx) => indexMap.set(oldIdx, newIdx));
898
+ contentOrder = rendered.contentOrder
899
+ .map((entry) => {
900
+ const tm = entry.match(/^text:(\d+)$/);
901
+ if (!tm) return entry;
902
+ const newIdx = indexMap.get(Number(tm[1]));
903
+ return newIdx !== undefined ? `text:${newIdx}` : undefined;
904
+ })
905
+ .filter((e): e is string => e !== undefined);
906
+ textSegments = filteredSegments;
907
+ text = rendered.text.replace(NO_RESPONSE_INLINE_RE, "").trim();
908
+ contentBlocks = rendered.contentBlocks
909
+ .map((block) =>
910
+ block.type === "text"
911
+ ? {
912
+ type: "text" as const,
913
+ text: block.text.replace(NO_RESPONSE_INLINE_RE, "").trim(),
914
+ }
915
+ : block,
916
+ )
917
+ .filter((block) => block.type !== "text" || block.text.length > 0);
832
918
  }
833
919
 
834
- // Use sentAt (actual event time) for the display timestamp when
835
- // available, falling back to createdAt (persistence time).
836
- // Note: clients use this display timestamp as their pagination cursor
837
- // after memory-pressure trimming, while server-side pagination filters
838
- // on createdAt. The mismatch is benign it may return slightly extra
839
- // data on a page boundary but never loses messages.
840
- const displayTimestamp = m.sentAt ?? m.timestamp;
920
+ const alignedContentOrder = aligned.rewriteContentOrder(contentOrder);
921
+
922
+ // Use sentAt (actual event time) for the display timestamp when available,
923
+ // falling back to createdAt (persistence time). Clients use this display
924
+ // timestamp as their pagination cursor after memory-pressure trimming,
925
+ // while server-side pagination filters on createdAt. The mismatch is
926
+ // benign it may return slightly extra data on a page boundary but never
927
+ // loses messages.
928
+ const displayTimestamp = m.sentAt ?? m.createdAt;
841
929
  return {
842
930
  id: m.id ?? "",
843
931
  ...(mergedMessageIds.length > 0 ? { mergedMessageIds } : {}),
844
932
  role: m.role,
933
+ // Flat plain-text body the legacy Swift client reads directly; see the
934
+ // `content` field on ConversationMessageSchema for why this must stay.
935
+ content: text,
845
936
  timestamp: new Date(displayTimestamp).toISOString(),
846
937
  attachments: msgAttachments,
847
- ...(m.toolCalls.length > 0 ? { toolCalls: m.toolCalls } : {}),
848
- ...(m.surfaces.length > 0 ? { surfaces: m.surfaces } : {}),
849
- ...(m.textSegments.length > 0 ? { textSegments: m.textSegments } : {}),
850
- ...(m.thinkingSegments?.length
851
- ? { thinkingSegments: m.thinkingSegments }
938
+ ...(toolCalls.length > 0 ? { toolCalls } : {}),
939
+ ...(rendered.surfaces.length > 0 ? { surfaces: rendered.surfaces } : {}),
940
+ ...(textSegments.length > 0 ? { textSegments } : {}),
941
+ ...(rendered.thinkingSegments.length > 0
942
+ ? { thinkingSegments: rendered.thinkingSegments }
852
943
  : {}),
853
944
  ...(alignedContentOrder.length > 0
854
945
  ? { contentOrder: alignedContentOrder }
855
946
  : {}),
947
+ ...(contentBlocks.length > 0 ? { contentBlocks } : {}),
856
948
  ...(m.subagentNotification
857
949
  ? { subagentNotification: m.subagentNotification }
858
950
  : {}),
@@ -860,6 +952,13 @@ export function handleListMessages({
860
952
  };
861
953
  });
862
954
 
955
+ // Snapshot↔stream alignment token: the `seq` of the last event whose
956
+ // content is durably persisted for this conversation in the current
957
+ // daemon process. Returned on every resolved-conversation response so a
958
+ // client can apply only stream events with a higher `seq`. Null when
959
+ // nothing has been persisted in-process (cold/aged-out/post-restart).
960
+ const persistedSeq = getPersistedSeq(resolvedConversationId);
961
+
863
962
  if (isPaginated) {
864
963
  // Prefer the page's oldest visible row (the documented cursor semantic).
865
964
  // When a scan-cap-truncated page comes back empty there's no visible row
@@ -881,6 +980,7 @@ export function handleListMessages({
881
980
  hasMore,
882
981
  oldestTimestamp: oldestTimestamp ?? null,
883
982
  oldestMessageId: oldestMessageId ?? null,
983
+ seq: persistedSeq,
884
984
  };
885
985
  }
886
986
 
@@ -889,10 +989,11 @@ export function handleListMessages({
889
989
  hasMore,
890
990
  ...(oldestTimestamp != null ? { oldestTimestamp } : {}),
891
991
  ...(oldestMessageId != null ? { oldestMessageId } : {}),
992
+ seq: persistedSeq,
892
993
  };
893
994
  }
894
995
 
895
- return { messages };
996
+ return { messages, seq: persistedSeq };
896
997
  }
897
998
 
898
999
  /**
@@ -1446,7 +1547,7 @@ export async function handleSendMessage(
1446
1547
  } else if (isWakeUp) {
1447
1548
  const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
1448
1549
 
1449
- conversation.processing = true;
1550
+ conversation.setProcessing(true);
1450
1551
  let cleanupDeferred = false;
1451
1552
  try {
1452
1553
  const rawContent = content ?? "";
@@ -1522,7 +1623,7 @@ export async function handleSendMessage(
1522
1623
  persistedAssistant.id,
1523
1624
  );
1524
1625
  publishConversationMessagesChanged(conversationId, originClientId);
1525
- conversation.processing = false;
1626
+ conversation.setProcessing(false);
1526
1627
  silentlyWithLog(
1527
1628
  conversation.drainQueue(),
1528
1629
  "canned-greeting queue drain",
@@ -1538,8 +1639,8 @@ export async function handleSendMessage(
1538
1639
  cleanupDeferred = true;
1539
1640
  return response;
1540
1641
  } finally {
1541
- if (!cleanupDeferred && conversation.processing) {
1542
- conversation.processing = false;
1642
+ if (!cleanupDeferred && conversation.isProcessing()) {
1643
+ conversation.setProcessing(false);
1543
1644
  silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
1544
1645
  }
1545
1646
  }
@@ -1752,7 +1853,7 @@ export async function handleSendMessage(
1752
1853
  const slashResult = await resolveSlash(rawContent, slashContext);
1753
1854
 
1754
1855
  if (slashResult.kind === "unknown") {
1755
- conversation.processing = true;
1856
+ conversation.setProcessing(true);
1756
1857
  let cleanupDeferred = false;
1757
1858
  try {
1758
1859
  const slashMeta = {
@@ -1833,7 +1934,7 @@ export async function handleSendMessage(
1833
1934
  persistedAssistant.id,
1834
1935
  );
1835
1936
  publishConversationMessagesChanged(conversationId, originClientId);
1836
- conversation.processing = false;
1937
+ conversation.setProcessing(false);
1837
1938
  silentlyWithLog(conversation.drainQueue(), "slash-command queue drain");
1838
1939
  }, 0);
1839
1940
 
@@ -1842,15 +1943,15 @@ export async function handleSendMessage(
1842
1943
  } finally {
1843
1944
  // No-op for the slash-command early-return path (handled inside
1844
1945
  // setTimeout above), but still needed for error paths.
1845
- if (!cleanupDeferred && conversation.processing) {
1846
- conversation.processing = false;
1946
+ if (!cleanupDeferred && conversation.isProcessing()) {
1947
+ conversation.setProcessing(false);
1847
1948
  silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
1848
1949
  }
1849
1950
  }
1850
1951
  }
1851
1952
 
1852
1953
  if (slashResult.kind === "compact") {
1853
- conversation.processing = true;
1954
+ conversation.setProcessing(true);
1854
1955
  const slashMeta = {
1855
1956
  userMessageChannel: sourceChannel,
1856
1957
  assistantMessageChannel: sourceChannel,
@@ -1870,12 +1971,12 @@ export async function handleSendMessage(
1870
1971
  // The fire-and-forget compaction below owns clearing `processing`, but a
1871
1972
  // throw from this initial persist never reaches it — reset here so the
1872
1973
  // conversation isn't stranded in queued mode.
1873
- conversation.processing = false;
1974
+ conversation.setProcessing(false);
1874
1975
  silentlyWithLog(conversation.drainQueue(), "compact-command queue drain");
1875
1976
  throw err;
1876
1977
  }
1877
1978
  if (persisted.deduplicated) {
1878
- conversation.processing = false;
1979
+ conversation.setProcessing(false);
1879
1980
  silentlyWithLog(conversation.drainQueue(), "compact-dedup queue drain");
1880
1981
  return {
1881
1982
  accepted: true,
@@ -1904,9 +2005,7 @@ export async function handleSendMessage(
1904
2005
  });
1905
2006
  publishConversationMessagesChanged(conversationId, originClientId);
1906
2007
  conversation.emitActivityState("thinking", "context_compacting");
1907
- const result = await conversation.forceCompact({
1908
- targetInputTokensOverride: slashResult.targetInputTokensOverride,
1909
- });
2008
+ const result = await conversation.forceCompact();
1910
2009
  const responseText = formatCompactResult(result);
1911
2010
 
1912
2011
  const assistantMsg = createAssistantMessage(responseText);
@@ -1943,7 +2042,7 @@ export async function handleSendMessage(
1943
2042
  retryable: true,
1944
2043
  });
1945
2044
  } finally {
1946
- conversation.processing = false;
2045
+ conversation.setProcessing(false);
1947
2046
  silentlyWithLog(
1948
2047
  conversation.drainQueue(),
1949
2048
  "compact-command queue drain",
@@ -1959,7 +2058,7 @@ export async function handleSendMessage(
1959
2058
  }
1960
2059
 
1961
2060
  if (slashResult.kind === "clean") {
1962
- conversation.processing = true;
2061
+ conversation.setProcessing(true);
1963
2062
  const conversationId = mapping.conversationId;
1964
2063
  // Outer try/finally guarantees the processing flag is cleared (and the
1965
2064
  // queue drained) on every failure path — including a throw from the
@@ -2045,7 +2144,7 @@ export async function handleSendMessage(
2045
2144
  conversationId,
2046
2145
  };
2047
2146
  } finally {
2048
- conversation.processing = false;
2147
+ conversation.setProcessing(false);
2049
2148
  silentlyWithLog(conversation.drainQueue(), "clean-command queue drain");
2050
2149
  }
2051
2150
  }
@@ -2465,8 +2564,46 @@ export const ROUTES: RouteDefinition[] = [
2465
2564
  description:
2466
2565
  "Return messages for a conversation, including attachments and interface file metadata.",
2467
2566
  tags: ["messages"],
2567
+ queryParams: [
2568
+ {
2569
+ name: "conversationId",
2570
+ type: "string",
2571
+ required: false,
2572
+ description:
2573
+ "Conversation UUID. One of conversationId or conversationKey is required.",
2574
+ },
2575
+ {
2576
+ name: "conversationKey",
2577
+ type: "string",
2578
+ required: false,
2579
+ description:
2580
+ "Channel/external conversation key. One of conversationId or conversationKey is required.",
2581
+ },
2582
+ {
2583
+ name: "page",
2584
+ type: "string",
2585
+ required: false,
2586
+ description:
2587
+ "When set to 'latest', returns the most recent page of messages with pagination metadata.",
2588
+ },
2589
+ {
2590
+ name: "beforeTimestamp",
2591
+ type: "integer",
2592
+ required: false,
2593
+ description:
2594
+ "Return messages older than this timestamp (ms since epoch). Used for paging older history.",
2595
+ },
2596
+ {
2597
+ name: "limit",
2598
+ type: "integer",
2599
+ required: false,
2600
+ description: "Maximum number of messages to return.",
2601
+ },
2602
+ ],
2468
2603
  responseBody: z.object({
2469
- messages: z.array(z.unknown()).describe("Array of message objects"),
2604
+ messages: z
2605
+ .array(ConversationMessageSchema)
2606
+ .describe("Array of message objects"),
2470
2607
  hasMore: z
2471
2608
  .boolean()
2472
2609
  .optional()
@@ -2483,6 +2620,13 @@ export const ROUTES: RouteDefinition[] = [
2483
2620
  .nullable()
2484
2621
  .optional()
2485
2622
  .describe("ID of the oldest message in this page"),
2623
+ seq: z
2624
+ .number()
2625
+ .nullable()
2626
+ .optional()
2627
+ .describe(
2628
+ "Global SSE `seq` of the last event whose content is durably persisted for this conversation in the current daemon process. A client can align this snapshot with the `/events` stream by applying only events with `seq` greater than this value. Null when no events have been persisted in this process (cold conversation, after a daemon restart, or when the conversation has aged out of the in-memory map) — clients should cold-start in that case. Absent on older daemons that predate this field.",
2629
+ ),
2486
2630
  }),
2487
2631
  handler: (args) => handleListMessages(args),
2488
2632
  },
@@ -2500,17 +2644,60 @@ export const ROUTES: RouteDefinition[] = [
2500
2644
  tags: ["messages"],
2501
2645
  responseStatus: "202",
2502
2646
  requestBody: z.object({
2503
- conversationKey: z.string().optional(),
2647
+ conversationId: z
2648
+ .string()
2649
+ .nullable()
2650
+ .optional()
2651
+ .describe(
2652
+ "Internal conversation id (0.8.6+ strict lookup). Omit both id and key to mint a new conversation server-side.",
2653
+ ),
2654
+ conversationKey: z.string().nullable().optional(),
2504
2655
  content: z.string().describe("Message text content"),
2505
2656
  attachments: z
2506
2657
  .array(z.unknown())
2507
- .describe("Optional file attachments")
2658
+ .describe("Optional inline file attachments")
2659
+ .optional(),
2660
+ attachmentIds: z
2661
+ .array(z.string())
2662
+ .describe("Ids of previously uploaded attachments to attach")
2508
2663
  .optional(),
2664
+ sourceChannel: z
2665
+ .string()
2666
+ .describe('Originating channel id (e.g. "vellum")'),
2667
+ interface: z
2668
+ .string()
2669
+ .describe('Originating interface id (e.g. "vellum")'),
2509
2670
  conversationType: z.string().optional(),
2510
2671
  slashCommand: z.string().optional(),
2511
2672
  clientTimezone: z.string().optional(),
2512
2673
  inferenceProfile: z.string().nullable().optional(),
2513
2674
  riskThreshold: z.enum(VALID_RISK_THRESHOLDS).optional(),
2675
+ onboarding: z
2676
+ .object({
2677
+ tools: z.array(z.string()),
2678
+ tasks: z.array(z.string()),
2679
+ tone: z.string(),
2680
+ userName: z.string().optional(),
2681
+ assistantName: z.string().optional(),
2682
+ googleConnected: z.boolean().optional(),
2683
+ googleScopes: z.array(z.string()).optional(),
2684
+ priorAssistants: z.array(z.string()).optional(),
2685
+ cohort: z.string().optional(),
2686
+ websiteUrl: z.string().optional(),
2687
+ contentSourceUrl: z.string().optional(),
2688
+ bootstrapTemplate: z.string().optional(),
2689
+ initialMessage: z.string().optional(),
2690
+ skills: z.array(z.string()).optional(),
2691
+ })
2692
+ .describe("PreChat onboarding context, sent on the first message only")
2693
+ .optional(),
2694
+ }),
2695
+ responseBody: z.object({
2696
+ accepted: z.boolean(),
2697
+ conversationId: z.string().optional(),
2698
+ messageId: z.string().optional(),
2699
+ queued: z.boolean().optional(),
2700
+ requestId: z.string().optional(),
2514
2701
  }),
2515
2702
  handler: async (args) =>
2516
2703
  handleSendMessage(args, {