@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * Route handlers for conversation messages and suggestions.
3
3
  */
4
- import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import {
5
+ existsSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ statSync,
9
+ writeFileSync,
10
+ } from "node:fs";
5
11
  import { join, relative } from "node:path";
6
12
 
7
13
  import { z } from "zod";
@@ -48,10 +54,12 @@ import type {
48
54
  NonHostProxyTransportMetadata,
49
55
  } from "../../daemon/message-types/conversations.js";
50
56
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
57
+ import { emitFeedEvent } from "../../home/emit-feed-event.js";
51
58
  import {
52
59
  writeOnboardingSidecar,
53
60
  writeRelationshipState,
54
61
  } from "../../home/relationship-state-writer.js";
62
+ import { rewriteCommandPreview } from "../../home/rewrite-command-preview.js";
55
63
  import * as attachmentsStore from "../../memory/attachments-store.js";
56
64
  import {
57
65
  createCanonicalGuardianRequest,
@@ -88,6 +96,7 @@ import { buildAssistantEvent } from "../assistant-event.js";
88
96
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
89
97
  import type { AuthContext } from "../auth/types.js";
90
98
  import { getChromeExtensionRegistry } from "../chrome-extension-registry.js";
99
+ import { getClientRegistry } from "../client-registry.js";
91
100
  import { bridgeConfirmationRequestToGuardian } from "../confirmation-request-guardian-bridge.js";
92
101
  import { routeGuardianReply } from "../guardian-reply-router.js";
93
102
  import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
@@ -998,6 +1007,54 @@ function makeHubPublisher(
998
1007
  },
999
1008
  });
1000
1009
 
1010
+ const inputRecord = msg.input as Record<string, unknown>;
1011
+ const commandPreview =
1012
+ redactSecrets(summarizeToolInput(msg.toolName, inputRecord)) ||
1013
+ undefined;
1014
+ const technicalTitle = commandPreview
1015
+ ? `Requesting permission: ${commandPreview}`
1016
+ : `Requesting approval to use ${msg.toolName}.`;
1017
+ const dedupKey = `tool-approval:${msg.requestId}`;
1018
+
1019
+ // Emit immediately with the technical preview.
1020
+ void emitFeedEvent({
1021
+ source: "assistant",
1022
+ title: technicalTitle,
1023
+ summary: technicalTitle,
1024
+ dedupKey,
1025
+ urgency: msg.riskLevel === "high" ? "high" : "medium",
1026
+ conversationId,
1027
+ }).catch((err) => {
1028
+ log.warn(
1029
+ { err, requestId: msg.requestId },
1030
+ "Failed to emit tool approval request feed event",
1031
+ );
1032
+ });
1033
+
1034
+ // Background: rewrite into prose and update the feed item.
1035
+ if (commandPreview) {
1036
+ void rewriteCommandPreview(msg.toolName, commandPreview)
1037
+ .then((prose) => {
1038
+ if (prose) {
1039
+ const proseTitle = `Requesting permission: ${prose}`;
1040
+ return emitFeedEvent({
1041
+ source: "assistant",
1042
+ title: proseTitle,
1043
+ summary: proseTitle,
1044
+ dedupKey,
1045
+ urgency: msg.riskLevel === "high" ? "high" : "medium",
1046
+ conversationId,
1047
+ });
1048
+ }
1049
+ })
1050
+ .catch((err) => {
1051
+ log.warn(
1052
+ { err, requestId: msg.requestId },
1053
+ "Failed to update feed event with prose rewrite",
1054
+ );
1055
+ });
1056
+ }
1057
+
1001
1058
  // Create a canonical guardian request so HTTP handlers can find it
1002
1059
  // via applyCanonicalGuardianDecision.
1003
1060
  try {
@@ -1271,7 +1328,7 @@ function resolveHostBrowserSender(
1271
1328
  * failure. The route handler path must never reject because of a
1272
1329
  * best-effort persistence step.
1273
1330
  */
1274
- function persistOnboardingArtifacts(onboarding: {
1331
+ export function persistOnboardingArtifacts(onboarding: {
1275
1332
  tools: string[];
1276
1333
  tasks: string[];
1277
1334
  tone: string;
@@ -1283,31 +1340,49 @@ function persistOnboardingArtifacts(onboarding: {
1283
1340
  const assistantName = onboarding.assistantName?.trim();
1284
1341
  if (assistantName) {
1285
1342
  const identityPath = getWorkspacePromptPath("IDENTITY.md");
1286
- if (!existsSync(identityPath)) {
1287
- try {
1343
+ try {
1344
+ if (existsSync(identityPath)) {
1345
+ const content = readFileSync(identityPath, "utf-8");
1346
+ const updated = content.replace(
1347
+ /^- (?:\*\*)?Name:(?:\*\*)?\s*.*$/m,
1348
+ () => `- **Name:** ${assistantName}`,
1349
+ );
1350
+ if (updated !== content) {
1351
+ writeFileSync(identityPath, updated, "utf-8");
1352
+ }
1353
+ } else {
1288
1354
  writeFileSync(
1289
1355
  identityPath,
1290
- `# Identity\n\n- Name: ${assistantName}\n`,
1356
+ `# Identity\n\n- **Name:** ${assistantName}\n`,
1291
1357
  "utf-8",
1292
1358
  );
1293
- } catch (err) {
1294
- log.warn(
1295
- { err, identityPath },
1296
- "Failed to seed IDENTITY.md from onboarding",
1297
- );
1298
1359
  }
1360
+ } catch (err) {
1361
+ log.warn(
1362
+ { err, identityPath },
1363
+ "Failed to seed IDENTITY.md from onboarding",
1364
+ );
1299
1365
  }
1300
1366
  }
1301
1367
 
1302
1368
  const userName = onboarding.userName?.trim();
1303
1369
  if (userName) {
1304
1370
  const userPath = getWorkspacePromptPath("USER.md");
1305
- if (!existsSync(userPath)) {
1306
- try {
1307
- writeFileSync(userPath, `# User\n\n- Name: ${userName}\n`, "utf-8");
1308
- } catch (err) {
1309
- log.warn({ err, userPath }, "Failed to seed USER.md from onboarding");
1371
+ try {
1372
+ if (existsSync(userPath)) {
1373
+ const content = readFileSync(userPath, "utf-8");
1374
+ const updated = content.replace(
1375
+ /^- (?:\*\*)?Name:(?:\*\*)?\s*.*$/m,
1376
+ () => `- **Name:** ${userName}`,
1377
+ );
1378
+ if (updated !== content) {
1379
+ writeFileSync(userPath, updated, "utf-8");
1380
+ }
1381
+ } else {
1382
+ writeFileSync(userPath, `# User\n\n- **Name:** ${userName}\n`, "utf-8");
1310
1383
  }
1384
+ } catch (err) {
1385
+ log.warn({ err, userPath }, "Failed to seed USER.md from onboarding");
1311
1386
  }
1312
1387
  }
1313
1388
 
@@ -1339,6 +1414,7 @@ export async function handleSendMessage(
1339
1414
  bypassSecretCheck?: boolean;
1340
1415
  hostHomeDir?: string;
1341
1416
  hostUsername?: string;
1417
+ clientId?: string;
1342
1418
  clientMessageId?: string;
1343
1419
  onboarding?: {
1344
1420
  tools: string[];
@@ -1495,20 +1571,37 @@ export async function handleSendMessage(
1495
1571
  interfaceId: sourceInterface,
1496
1572
  } satisfies NonHostProxyTransportMetadata);
1497
1573
 
1574
+ // Register/refresh the client in the unified client registry so
1575
+ // `assistant clients list` can discover all connected interfaces.
1576
+ // Uses the client-supplied clientId when available (stable per-install
1577
+ // UUID), falling back to a synthetic key derived from interfaceId so
1578
+ // older clients that don't send clientId still appear in the registry.
1579
+ const effectiveClientId =
1580
+ typeof body.clientId === "string" && body.clientId.length > 0
1581
+ ? body.clientId
1582
+ : `synthetic:${sourceInterface}`;
1583
+ getClientRegistry().register({
1584
+ clientId: effectiveClientId,
1585
+ interfaceId: sourceInterface,
1586
+ hostHomeDir: body.hostHomeDir,
1587
+ hostUsername: body.hostUsername,
1588
+ });
1589
+
1498
1590
  const conversation = await smDeps.getOrCreateConversation(
1499
1591
  mapping.conversationId,
1500
1592
  { transport },
1501
1593
  );
1502
1594
 
1503
1595
  // Store pre-chat onboarding context on the conversation when this is the
1504
- // very first message (no prior messages loaded). Also persist the
1505
- // onboarding selections so the Home page shows onboarding-sourced
1506
- // chips immediately, and seed IDENTITY.md / USER.md so subsequent
1507
- // turn-boundary recomputes of relationship-state have a stable
1508
- // persona source beyond the in-memory conversation object.
1509
- if (body.onboarding && conversation.messages.length === 0) {
1510
- conversation.setOnboardingContext(body.onboarding);
1511
- persistOnboardingArtifacts(body.onboarding);
1596
+ // very first message (no prior messages loaded). Artifact persistence
1597
+ // (IDENTITY.md, USER.md, sidecar) is deferred: on the canned greeting
1598
+ // path it runs inside the setTimeout right before warmPromptCache() so
1599
+ // the warmed system prompt includes the identity; on the normal LLM
1600
+ // path it runs immediately before inference starts.
1601
+ const isFirstOnboarding =
1602
+ !!body.onboarding && conversation.messages.length === 0;
1603
+ if (isFirstOnboarding) {
1604
+ conversation.setOnboardingContext(body.onboarding!);
1512
1605
  }
1513
1606
 
1514
1607
  // Resolve guardian context from the AuthContext's actorPrincipalId.
@@ -1618,14 +1711,13 @@ export async function handleSendMessage(
1618
1711
  conversation.hostBrowserSenderOverride = undefined;
1619
1712
  }
1620
1713
 
1621
- // Provision the host browser proxy. For interfaces that natively support
1622
- // host_browser (chrome-extension), always provision it. For macOS, the
1623
- // static capability check returns false (supportsHostProxy("macos",
1624
- // "host_browser") === false) because the extension isn't guaranteed to be
1625
- // attached but when the registry confirms an active extension
1626
- // connection, we provision the proxy anyway so macOS turns can drive the
1627
- // user's real Chrome session. When no extension is connected, macOS skips
1628
- // provisioning and browser tools fall through to cdp-inspect/local.
1714
+ // Provision the host browser proxy. Both macOS and chrome-extension
1715
+ // natively support host_browser. For macOS, the proxy is wired to the
1716
+ // SSE sender by default so `host_browser_request` frames reach the
1717
+ // desktop client directly. When the guardian also has an active extension
1718
+ // connection (isRegistryRouted), the registry-routed sender is used
1719
+ // instead so browser tools route through the user's real Chrome session.
1720
+ // For chrome-extension, the registry sender is always used.
1629
1721
  const shouldProvisionBrowserProxy =
1630
1722
  supportsHostProxy(sourceInterface, "host_browser") ||
1631
1723
  (canServiceRegistryBrowser(sourceInterface) && isRegistryRouted);
@@ -1688,9 +1780,10 @@ export async function handleSendMessage(
1688
1780
  skipProxySenderUpdate: preservingProxies,
1689
1781
  });
1690
1782
  // Re-enable the browser proxy for turns that provisioned one. This covers:
1783
+ // - macOS: always provisioned (SSE sender or registry-routed when extension
1784
+ // is connected)
1691
1785
  // - chrome-extension: natively supports host_browser (non-interactive but
1692
1786
  // has a connected client for host_browser_request events)
1693
- // - macOS with extension: provisioned above when isRegistryRouted is true
1694
1787
  //
1695
1788
  // The helper bypasses the `hasNoClient` gate so chrome-extension turns can
1696
1789
  // drive the browser via CDP without leaking host_bash/host_file tool
@@ -1703,97 +1796,102 @@ export async function handleSendMessage(
1703
1796
 
1704
1797
  // ── Canned first-greeting fast path ──
1705
1798
  // On a completely fresh workspace, skip LLM inference for the macOS
1706
- // wake-up greeting and return a pre-written response. This eliminates
1707
- // 10-30s of inference latency on first boot.
1799
+ // wake-up greeting and return a pre-written response. When onboarding
1800
+ // context is present the greeting is personalized using the selections;
1801
+ // otherwise a generic greeting is served. Both paths are instant.
1708
1802
  if (isWakeUpGreeting(trimmedContent, conversation.getMessages().length)) {
1709
- const cannedGreeting = getCannedFirstGreeting();
1710
- if (cannedGreeting) {
1711
- conversation.processing = true;
1712
- let cleanupDeferred = false;
1713
- try {
1714
- const provenance = provenanceFromTrustContext(
1715
- conversation.trustContext,
1716
- );
1717
- const channelMeta = {
1718
- ...provenance,
1719
- userMessageChannel: sourceChannel,
1720
- assistantMessageChannel: sourceChannel,
1721
- userMessageInterface: sourceInterface,
1722
- assistantMessageInterface: sourceInterface,
1723
- };
1724
-
1725
- const rawContent = content ?? "";
1726
- const attachments = hasAttachments
1727
- ? smDeps.resolveAttachments(attachmentIds)
1728
- : [];
1729
- const userMsg = createUserMessage(rawContent, attachments);
1730
- const persisted = await addMessage(
1731
- mapping.conversationId,
1732
- "user",
1733
- JSON.stringify(userMsg.content),
1734
- channelMeta,
1735
- );
1736
- conversation.getMessages().push(userMsg);
1803
+ const cannedGreeting = getCannedFirstGreeting(body.onboarding ?? undefined);
1737
1804
 
1738
- setConversationOriginChannelIfUnset(
1739
- mapping.conversationId,
1740
- sourceChannel,
1741
- );
1742
- setConversationOriginInterfaceIfUnset(
1743
- mapping.conversationId,
1744
- sourceInterface,
1745
- );
1805
+ conversation.processing = true;
1806
+ let cleanupDeferred = false;
1807
+ try {
1808
+ const provenance = provenanceFromTrustContext(conversation.trustContext);
1809
+ const channelMeta = {
1810
+ ...provenance,
1811
+ userMessageChannel: sourceChannel,
1812
+ assistantMessageChannel: sourceChannel,
1813
+ userMessageInterface: sourceInterface,
1814
+ assistantMessageInterface: sourceInterface,
1815
+ };
1746
1816
 
1747
- const assistantMsg = createAssistantMessage(cannedGreeting);
1748
- await addMessage(
1749
- mapping.conversationId,
1750
- "assistant",
1751
- JSON.stringify(assistantMsg.content),
1752
- channelMeta,
1753
- );
1754
- conversation.getMessages().push(assistantMsg);
1817
+ const rawContent = content ?? "";
1818
+ const attachments = hasAttachments
1819
+ ? smDeps.resolveAttachments(attachmentIds)
1820
+ : [];
1821
+ const userMsg = createUserMessage(rawContent, attachments);
1822
+ const persisted = await addMessage(
1823
+ mapping.conversationId,
1824
+ "user",
1825
+ JSON.stringify(userMsg.content),
1826
+ channelMeta,
1827
+ );
1828
+ conversation.getMessages().push(userMsg);
1755
1829
 
1756
- const conversationId = mapping.conversationId;
1757
- const response = Response.json(
1758
- { accepted: true, messageId: persisted.id, conversationId },
1759
- { status: 202 },
1760
- );
1830
+ setConversationOriginChannelIfUnset(
1831
+ mapping.conversationId,
1832
+ sourceChannel,
1833
+ );
1834
+ setConversationOriginInterfaceIfUnset(
1835
+ mapping.conversationId,
1836
+ sourceInterface,
1837
+ );
1761
1838
 
1762
- // Defer event publishing to next tick (same pattern as unknown-slash
1763
- // fast path) so the HTTP response reaches the client before SSE
1764
- // events arrive.
1765
- setTimeout(() => {
1766
- onEvent({
1767
- type: "user_message_echo",
1768
- text: rawContent,
1769
- conversationId,
1770
- messageId: persisted.id,
1771
- clientMessageId,
1772
- });
1773
- onEvent({ type: "assistant_text_delta", text: cannedGreeting });
1774
- onEvent({ type: "message_complete", conversationId });
1775
- conversation.processing = false;
1776
- silentlyWithLog(
1777
- conversation.drainQueue(),
1778
- "canned-greeting queue drain",
1779
- );
1780
- }, 0);
1839
+ const conversationId = mapping.conversationId;
1840
+
1841
+ const assistantMsg = createAssistantMessage(cannedGreeting);
1842
+ await addMessage(
1843
+ mapping.conversationId,
1844
+ "assistant",
1845
+ JSON.stringify(assistantMsg.content),
1846
+ channelMeta,
1847
+ );
1848
+ conversation.getMessages().push(assistantMsg);
1849
+
1850
+ const response = Response.json(
1851
+ { accepted: true, messageId: persisted.id, conversationId },
1852
+ { status: 202 },
1853
+ );
1781
1854
 
1782
- log.info(
1783
- { conversationId },
1784
- "Served canned first greeting — skipped LLM inference",
1855
+ setTimeout(() => {
1856
+ onEvent({
1857
+ type: "user_message_echo",
1858
+ text: rawContent,
1859
+ conversationId,
1860
+ messageId: persisted.id,
1861
+ clientMessageId,
1862
+ });
1863
+ onEvent({ type: "assistant_text_delta", text: cannedGreeting });
1864
+ onEvent({ type: "message_complete", conversationId });
1865
+ conversation.processing = false;
1866
+ silentlyWithLog(
1867
+ conversation.drainQueue(),
1868
+ "canned-greeting queue drain",
1785
1869
  );
1786
- cleanupDeferred = true;
1787
- return response;
1788
- } finally {
1789
- if (!cleanupDeferred && conversation.processing) {
1790
- conversation.processing = false;
1791
- silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
1870
+
1871
+ if (isFirstOnboarding) {
1872
+ persistOnboardingArtifacts(body.onboarding!);
1792
1873
  }
1874
+ conversation.warmPromptCache();
1875
+ }, 0);
1876
+
1877
+ log.info(
1878
+ { conversationId, personalized: !!body.onboarding },
1879
+ "Served canned first greeting — skipped LLM inference",
1880
+ );
1881
+ cleanupDeferred = true;
1882
+ return response;
1883
+ } finally {
1884
+ if (!cleanupDeferred && conversation.processing) {
1885
+ conversation.processing = false;
1886
+ silentlyWithLog(conversation.drainQueue(), "error-path queue drain");
1793
1887
  }
1794
1888
  }
1795
1889
  }
1796
1890
 
1891
+ if (isFirstOnboarding) {
1892
+ persistOnboardingArtifacts(body.onboarding!);
1893
+ }
1894
+
1797
1895
  const attachments = hasAttachments
1798
1896
  ? smDeps.resolveAttachments(attachmentIds)
1799
1897
  : [];
@@ -2251,11 +2349,15 @@ async function generateLlmSuggestion(
2251
2349
  `Write the user's next reply, focusing on the LAST question or call-to-action in the assistant message. Keep it short (under 15 words), casual, and in the user's voice. Respond in this exact format:\n\n` +
2252
2350
  `<reply>YOUR_REPLY_HERE</reply>`;
2253
2351
 
2352
+ // Single user message only — no assistant-role prefill. Anthropic
2353
+ // rejects assistant prefill whenever the request triggers extended
2354
+ // thinking (e.g. Opus 4.x at `effort: "xhigh"`), and the call-site
2355
+ // config is user-controlled, so we can't statically guarantee a
2356
+ // prefill-safe model. Keep `stop_sequences: ["</reply>"]` as an
2357
+ // early-termination hint; the parser below handles both tagged and
2358
+ // untagged responses so untagged "casual answer" replies still work.
2254
2359
  const response = await provider.sendMessage(
2255
- [
2256
- { role: "user", content: [{ type: "text", text: userPrompt }] },
2257
- { role: "assistant", content: [{ type: "text", text: "<reply>" }] },
2258
- ],
2360
+ [{ role: "user", content: [{ type: "text", text: userPrompt }] }],
2259
2361
  [], // no tools
2260
2362
  systemPrompt,
2261
2363
  {
@@ -2270,7 +2372,12 @@ async function generateLlmSuggestion(
2270
2372
 
2271
2373
  const textBlock = response.content.find((b) => b.type === "text");
2272
2374
  const raw = textBlock && "text" in textBlock ? textBlock.text : "";
2273
- const stripped = raw
2375
+ // Prefer the content inside <reply>…</reply> when the model honors the
2376
+ // tag format. If the response has no tags, fall back to the raw text —
2377
+ // a plain "Sure, tomorrow works" without tags is still a valid chip.
2378
+ const tagMatch = raw.match(/<reply>([\s\S]*?)(?:<\/reply>|$)/i);
2379
+ const extracted = tagMatch ? tagMatch[1] : raw;
2380
+ const stripped = extracted
2274
2381
  .replace(/<\/?reply>/gi, "")
2275
2382
  .replace(/^["'`]+|["'`]+$/g, "")
2276
2383
  .trim();
@@ -21,9 +21,9 @@ import {
21
21
  } from "../../memory/conversation-attention-store.js";
22
22
  import {
23
23
  addMessage,
24
- countMessagesWithSlackMeta,
25
24
  getMessageById,
26
25
  getMessages,
26
+ selectSlackMetaCandidateMetadata,
27
27
  updateMessageMetadata,
28
28
  } from "../../memory/conversation-crud.js";
29
29
  import * as deliveryChannels from "../../memory/delivery-channels.js";
@@ -1049,14 +1049,21 @@ export async function handleChannelInbound(
1049
1049
  // longer trigger backfill. Failures are non-fatal — the new message
1050
1050
  // proceeds without backfilled history.
1051
1051
  if (sourceChannel === "slack" && sourceChatType === "im") {
1052
+ // Exclude the just-arrived webhook message from the history window —
1053
+ // the normal inbound persistence path writes it separately, so
1054
+ // including it here would produce duplicate user turns. Only pass a
1055
+ // bound when we actually have a Slack ts (`<secs>.<micros>`): the
1056
+ // fallback path writes `externalMessageId` into `channelTs`, but that
1057
+ // identifier is not guaranteed to be a Slack ts, and Slack's
1058
+ // `conversations.history` rejects anything that isn't a ts string.
1059
+ const boundingTs = isSlackTs(sourceMessageId)
1060
+ ? sourceMessageId
1061
+ : undefined;
1052
1062
  await tryBackfillSlackDmIfCold({
1053
1063
  conversationId: result.conversationId,
1054
1064
  channelId: conversationExternalId,
1055
1065
  account: slackAccount,
1056
- // Exclude the just-arrived webhook message from the history window —
1057
- // the normal inbound persistence path writes it separately, so
1058
- // including it here would produce duplicate user turns.
1059
- latestTs: slackInbound?.channelTs,
1066
+ latestTs: boundingTs,
1060
1067
  });
1061
1068
  }
1062
1069
 
@@ -1245,16 +1252,84 @@ async function persistSlackReactionAsMessage(params: {
1245
1252
  const SLACK_DM_BACKFILL_WARM_THRESHOLD = 3;
1246
1253
 
1247
1254
  /**
1248
- * Count messages in a conversation whose `metadata` carries a `slackMeta`
1249
- * envelope, capped at the warm threshold. Delegates to the DB helper which
1250
- * pushes both `LIKE` and `LIMIT` into SQL so a warm DM conversation never
1251
- * scans beyond the threshold's worth of rows on the webhook critical path.
1255
+ * Shape-check for a Slack `ts` value. Slack IDs messages by `<seconds>.<micros>`
1256
+ * strings (e.g. `"1700000000.000100"`). The daemon also stores an
1257
+ * `externalMessageId` derived from the gateway's dedupe key which follows a
1258
+ * different format, so any path that feeds a ts to Slack's API
1259
+ * (`conversations.history`'s `latest`, etc.) must shape-check first — Slack
1260
+ * rejects non-ts arguments with `invalid_arguments`, and passing a malformed
1261
+ * bound silently disables the intended history window.
1262
+ */
1263
+ function isSlackTs(value: string | null | undefined): value is string {
1264
+ return typeof value === "string" && /^\d+\.\d+$/.test(value);
1265
+ }
1266
+
1267
+ /**
1268
+ * Batch size used when pulling candidate rows from SQL. A bare
1269
+ * `LIKE '%"slackMeta"%'` match can include rows whose metadata JSON is
1270
+ * malformed or carries the literal under an unrelated key, so we fetch in
1271
+ * batches and re-validate each candidate with Zod. The threshold is tiny
1272
+ * (see `SLACK_DM_BACKFILL_WARM_THRESHOLD`), so a 10× batch is a trivial
1273
+ * scan while letting a handful of bad rows not starve the count.
1274
+ */
1275
+ const SLACK_DM_CANDIDATE_BATCH_SIZE = SLACK_DM_BACKFILL_WARM_THRESHOLD * 10;
1276
+
1277
+ /**
1278
+ * Absolute cap on candidate rows inspected per webhook to classify a DM as
1279
+ * warm. If this many substring matches have been examined without reaching
1280
+ * the valid-row threshold, treat the conversation as cold — a scan this
1281
+ * deep already dominates the critical-path budget and the cold-start
1282
+ * backfill path is itself idempotent against re-runs.
1283
+ */
1284
+ const SLACK_DM_CANDIDATE_MAX_SCAN = SLACK_DM_BACKFILL_WARM_THRESHOLD * 20;
1285
+
1286
+ /**
1287
+ * Count messages in a conversation whose `metadata` carries a well-formed
1288
+ * `slackMeta` envelope, capped at the warm threshold. SQL prefilters with
1289
+ * `LIKE` + `LIMIT`/`OFFSET` so warm DM conversations never scan the full
1290
+ * table on the webhook critical path, and each candidate is re-validated
1291
+ * through `readSlackMetadata` — a bare substring match would otherwise
1292
+ * wrongly count rows whose metadata is truncated, parses but fails schema
1293
+ * validation, or happens to contain the literal `"slackMeta"` under an
1294
+ * unrelated key. Pulls candidates in batches, continuing until either the
1295
+ * threshold of *valid* rows is reached or the per-call scan cap is hit, so
1296
+ * a cluster of malformed rows at the head of the scan cannot starve the
1297
+ * count and misclassify a warm conversation as cold.
1252
1298
  */
1253
1299
  function countSlackMetaMessages(conversationId: string): number {
1254
- return countMessagesWithSlackMeta(
1255
- conversationId,
1256
- SLACK_DM_BACKFILL_WARM_THRESHOLD,
1257
- );
1300
+ let count = 0;
1301
+ let offset = 0;
1302
+ while (offset < SLACK_DM_CANDIDATE_MAX_SCAN) {
1303
+ const remaining = SLACK_DM_CANDIDATE_MAX_SCAN - offset;
1304
+ const batchLimit = Math.min(SLACK_DM_CANDIDATE_BATCH_SIZE, remaining);
1305
+ const candidates = selectSlackMetaCandidateMetadata(
1306
+ conversationId,
1307
+ batchLimit,
1308
+ offset,
1309
+ );
1310
+ if (candidates.length === 0) return count;
1311
+ for (const raw of candidates) {
1312
+ let parent: Record<string, unknown> | null = null;
1313
+ try {
1314
+ const parsed = JSON.parse(raw) as unknown;
1315
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1316
+ parent = parsed as Record<string, unknown>;
1317
+ }
1318
+ } catch {
1319
+ continue;
1320
+ }
1321
+ if (!parent) continue;
1322
+ const inner = parent.slackMeta;
1323
+ if (typeof inner !== "string") continue;
1324
+ if (readSlackMetadata(inner)) {
1325
+ count++;
1326
+ if (count >= SLACK_DM_BACKFILL_WARM_THRESHOLD) return count;
1327
+ }
1328
+ }
1329
+ if (candidates.length < batchLimit) return count;
1330
+ offset += candidates.length;
1331
+ }
1332
+ return count;
1258
1333
  }
1259
1334
 
1260
1335
  /**
@@ -56,6 +56,7 @@ mock.module("../../memory/qdrant-client.js", () => ({
56
56
  searchWithFilter: async () => [...mockHybridSearchResults],
57
57
  }),
58
58
  initQdrantClient: () => {},
59
+ resolveQdrantUrl: () => "http://127.0.0.1:6333",
59
60
  }));
60
61
 
61
62
  mock.module("../../memory/qdrant-circuit-breaker.js", () => ({
@@ -255,9 +255,6 @@ export async function handleMigrationExport(req: Request): Promise<Response> {
255
255
  }
256
256
 
257
257
  const result = await streamExportVBundle({
258
- // hooksDir is intentionally omitted — hooks now live under workspace/hooks/
259
- // and are included in the workspace walk. Passing hooksDir separately would
260
- // export them twice (once as workspace/hooks/... and again as hooks/...).
261
258
  workspaceDir: getWorkspaceDir(),
262
259
  source: "runtime-export",
263
260
  description,