@vellumai/assistant 0.8.1 → 0.8.2

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 (506) hide show
  1. package/ARCHITECTURE.md +2 -7
  2. package/Dockerfile +75 -1
  3. package/bun.lock +11 -1
  4. package/docker-entrypoint.sh +5 -0
  5. package/docker-init-apt-root.sh +94 -0
  6. package/docker-kata-apt-env.sh +39 -0
  7. package/docs/plugins.md +88 -47
  8. package/docs/skills.md +9 -7
  9. package/examples/plugins/echo/README.md +27 -27
  10. package/examples/plugins/echo/package.json +3 -0
  11. package/examples/plugins/echo/register.ts +31 -31
  12. package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
  13. package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
  14. package/openapi.yaml +325 -3
  15. package/package.json +3 -1
  16. package/scripts/generate-openapi.ts +83 -10
  17. package/scripts/sync-llm-catalog.ts +2 -2
  18. package/scripts/sync-web-search-catalog.ts +47 -25
  19. package/src/__tests__/agent-image-optimize.test.ts +11 -3
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
  21. package/src/__tests__/anthropic-provider.test.ts +45 -0
  22. package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
  23. package/src/__tests__/app-executors.test.ts +220 -4
  24. package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
  25. package/src/__tests__/bundled-asset.test.ts +6 -6
  26. package/src/__tests__/channel-availability-routes.test.ts +206 -0
  27. package/src/__tests__/channel-delivery-store.test.ts +289 -1
  28. package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
  29. package/src/__tests__/clawhub.test.ts +75 -16
  30. package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
  31. package/src/__tests__/config-schema.test.ts +21 -0
  32. package/src/__tests__/config-set-route.test.ts +80 -0
  33. package/src/__tests__/config-sounds-sync.test.ts +97 -0
  34. package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
  35. package/src/__tests__/context-search-conversations-source.test.ts +117 -2
  36. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
  37. package/src/__tests__/context-search-workspace-source.test.ts +7 -0
  38. package/src/__tests__/context-token-estimator.test.ts +1 -0
  39. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  40. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  41. package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
  42. package/src/__tests__/conversation-agent-loop.test.ts +2 -0
  43. package/src/__tests__/conversation-error.test.ts +42 -3
  44. package/src/__tests__/conversation-fork-crud.test.ts +82 -0
  45. package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
  46. package/src/__tests__/conversation-lifecycle.test.ts +173 -0
  47. package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
  48. package/src/__tests__/conversation-pairing.test.ts +54 -0
  49. package/src/__tests__/conversation-process-callsite.test.ts +4 -1
  50. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
  51. package/src/__tests__/conversation-queue.test.ts +4 -1
  52. package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
  53. package/src/__tests__/conversation-slash-queue.test.ts +59 -1
  54. package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
  55. package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
  56. package/src/__tests__/conversation-sync-tags.test.ts +235 -0
  57. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  58. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  59. package/src/__tests__/credential-security-invariants.test.ts +3 -2
  60. package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
  61. package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
  62. package/src/__tests__/disk-pressure-tools.test.ts +1 -0
  63. package/src/__tests__/dm-backfill.test.ts +121 -10
  64. package/src/__tests__/document-tool-security.test.ts +258 -0
  65. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  66. package/src/__tests__/edit-propagation.test.ts +33 -0
  67. package/src/__tests__/empty-response-pipeline.test.ts +0 -4
  68. package/src/__tests__/external-plugin-loader.test.ts +60 -36
  69. package/src/__tests__/filing-service.test.ts +140 -0
  70. package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
  71. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
  72. package/src/__tests__/helpers/tar-fixtures.ts +39 -0
  73. package/src/__tests__/helpers/wait-for.ts +21 -0
  74. package/src/__tests__/history-repair-pipeline.test.ts +0 -3
  75. package/src/__tests__/history-repair.test.ts +73 -0
  76. package/src/__tests__/host-app-control-proxy.test.ts +266 -10
  77. package/src/__tests__/image-credentials.test.ts +1 -1
  78. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  79. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
  80. package/src/__tests__/inference-profile-reaper.test.ts +4 -2
  81. package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
  82. package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
  83. package/src/__tests__/injector-chain.test.ts +10 -8
  84. package/src/__tests__/install-skill-routing.test.ts +155 -37
  85. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
  86. package/src/__tests__/list-messages-page-latest.test.ts +55 -0
  87. package/src/__tests__/llm-call-pipeline.test.ts +0 -3
  88. package/src/__tests__/llm-catalog-parity.test.ts +55 -13
  89. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
  90. package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
  91. package/src/__tests__/llm-usage-store.test.ts +114 -0
  92. package/src/__tests__/managed-profile-guard.test.ts +31 -29
  93. package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
  94. package/src/__tests__/managed-store.test.ts +84 -192
  95. package/src/__tests__/media-generate-image.test.ts +1 -1
  96. package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
  97. package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
  98. package/src/__tests__/oauth-commands-routes.test.ts +168 -16
  99. package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
  100. package/src/__tests__/openai-provider.test.ts +24 -0
  101. package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
  102. package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
  103. package/src/__tests__/persistence-pipeline.test.ts +0 -2
  104. package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
  105. package/src/__tests__/platform.test.ts +2 -0
  106. package/src/__tests__/plugin-api-shim.test.ts +125 -0
  107. package/src/__tests__/plugin-bootstrap.test.ts +10 -36
  108. package/src/__tests__/plugin-external-api.test.ts +68 -0
  109. package/src/__tests__/plugin-registry.test.ts +0 -77
  110. package/src/__tests__/plugin-route-contribution.test.ts +0 -1
  111. package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
  112. package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
  113. package/src/__tests__/plugin-types.test.ts +3 -13
  114. package/src/__tests__/process-message-background-slack.test.ts +8 -1
  115. package/src/__tests__/process-message-display-content.test.ts +421 -0
  116. package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
  117. package/src/__tests__/provider-error-scenarios.test.ts +111 -0
  118. package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
  119. package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
  120. package/src/__tests__/schedule-routes.test.ts +50 -3
  121. package/src/__tests__/schedule-store.test.ts +94 -0
  122. package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
  123. package/src/__tests__/schema-transforms.test.ts +20 -0
  124. package/src/__tests__/search-skills-unified.test.ts +0 -5
  125. package/src/__tests__/server-history-render.test.ts +43 -0
  126. package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
  127. package/src/__tests__/skill-load-tool.test.ts +27 -89
  128. package/src/__tests__/skill-memory.test.ts +23 -3
  129. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
  130. package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
  131. package/src/__tests__/skills-install-extract.test.ts +49 -38
  132. package/src/__tests__/skills-install-staging.test.ts +159 -0
  133. package/src/__tests__/skills-uninstall.test.ts +9 -41
  134. package/src/__tests__/skills.test.ts +51 -58
  135. package/src/__tests__/slack-channel-config.test.ts +9 -0
  136. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  137. package/src/__tests__/system-prompt.test.ts +737 -63
  138. package/src/__tests__/terminal-tools.test.ts +28 -1
  139. package/src/__tests__/thread-backfill.test.ts +557 -27
  140. package/src/__tests__/title-generate-pipeline.test.ts +0 -13
  141. package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
  142. package/src/__tests__/tool-error-pipeline.test.ts +0 -3
  143. package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
  145. package/src/__tests__/tool-executor.test.ts +16 -4
  146. package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
  147. package/src/__tests__/turn-events-store.test.ts +256 -0
  148. package/src/__tests__/twilio-routes.test.ts +4 -0
  149. package/src/__tests__/user-plugin-loader.test.ts +0 -7
  150. package/src/__tests__/voice-session-bridge.test.ts +198 -0
  151. package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
  152. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
  153. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
  154. package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
  155. package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
  156. package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
  157. package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
  158. package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
  159. package/src/acp/resolve-agent.ts +1 -1
  160. package/src/agent/image-optimize.ts +13 -5
  161. package/src/calls/voice-session-bridge.ts +61 -42
  162. package/src/channels/types.ts +108 -0
  163. package/src/cli/__tests__/unknown-command.test.ts +24 -0
  164. package/src/cli/commands/__tests__/changelog.test.ts +304 -319
  165. package/src/cli/commands/__tests__/schedules.test.ts +491 -0
  166. package/src/cli/commands/changelog.ts +106 -42
  167. package/src/cli/commands/conversations.ts +102 -17
  168. package/src/cli/commands/default-action.ts +10 -53
  169. package/src/cli/commands/notifications.ts +329 -317
  170. package/src/cli/commands/plugins.ts +185 -0
  171. package/src/cli/commands/schedules.ts +391 -0
  172. package/src/cli/commands/telemetry.ts +40 -0
  173. package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
  174. package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
  175. package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
  176. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
  177. package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
  178. package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
  179. package/src/cli/lib/cli-colors.ts +12 -0
  180. package/src/cli/lib/confirm-prompt.ts +79 -0
  181. package/src/cli/lib/install-from-github.ts +304 -0
  182. package/src/cli/lib/list-installed-plugins.ts +137 -0
  183. package/src/cli/lib/uninstall-plugin.ts +82 -0
  184. package/src/cli/lib/unknown-command.ts +111 -0
  185. package/src/cli/program.ts +38 -2
  186. package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
  187. package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
  188. package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
  189. package/src/config/bundled-skills/document/SKILL.md +23 -3
  190. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  191. package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
  192. package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
  193. package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
  194. package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
  195. package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
  196. package/src/config/bundled-tool-registry.ts +6 -0
  197. package/src/config/feature-flag-registry.json +41 -1
  198. package/src/config/loader.ts +64 -38
  199. package/src/config/schema.ts +7 -10
  200. package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
  201. package/src/config/schemas/channels.ts +8 -0
  202. package/src/config/schemas/compaction.ts +28 -0
  203. package/src/config/schemas/heartbeat.ts +9 -0
  204. package/src/config/schemas/llm-request-logs.ts +31 -7
  205. package/src/config/schemas/llm.ts +3 -0
  206. package/src/config/schemas/memory-retrieval.ts +18 -0
  207. package/src/config/schemas/tools.ts +14 -0
  208. package/src/config/skills.ts +3 -96
  209. package/src/context/compactor.ts +1047 -0
  210. package/src/context/token-estimator.ts +2 -2
  211. package/src/context/window-manager.ts +197 -1520
  212. package/src/credential-execution/managed-catalog.ts +37 -0
  213. package/src/credential-health/credential-health-service.ts +280 -19
  214. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
  215. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
  216. package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
  217. package/src/daemon/approval-generators.ts +8 -6
  218. package/src/daemon/config-watcher.ts +94 -31
  219. package/src/daemon/conversation-agent-loop.ts +169 -9
  220. package/src/daemon/conversation-error.ts +171 -37
  221. package/src/daemon/conversation-lifecycle.ts +53 -40
  222. package/src/daemon/conversation-messaging.ts +25 -6
  223. package/src/daemon/conversation-process.ts +49 -12
  224. package/src/daemon/conversation-runtime-assembly.ts +16 -1
  225. package/src/daemon/conversation-slash.ts +12 -5
  226. package/src/daemon/conversation-store.ts +11 -4
  227. package/src/daemon/conversation-tool-setup.ts +39 -7
  228. package/src/daemon/conversation.ts +33 -1
  229. package/src/daemon/external-plugins-bootstrap.ts +217 -181
  230. package/src/daemon/first-greeting.ts +22 -2
  231. package/src/daemon/handlers/config-model.ts +6 -5
  232. package/src/daemon/handlers/config-slack-channel.ts +15 -3
  233. package/src/daemon/handlers/shared.ts +14 -5
  234. package/src/daemon/handlers/skills.ts +111 -108
  235. package/src/daemon/history-repair.ts +28 -1
  236. package/src/daemon/host-app-control-proxy.ts +98 -23
  237. package/src/daemon/lifecycle.ts +45 -35
  238. package/src/daemon/meet-host-supervisor.ts +5 -4
  239. package/src/daemon/memory-v2-startup.ts +49 -0
  240. package/src/daemon/message-protocol.ts +1 -0
  241. package/src/daemon/message-types/conversations.ts +25 -0
  242. package/src/daemon/message-types/messages.ts +61 -0
  243. package/src/daemon/message-types/subagents.ts +1 -0
  244. package/src/daemon/message-types/sync.ts +1 -0
  245. package/src/daemon/pkb-reminder-builder.test.ts +1 -1
  246. package/src/daemon/pkb-reminder-builder.ts +1 -1
  247. package/src/daemon/plugin-source-watcher.ts +146 -0
  248. package/src/daemon/process-message.ts +21 -3
  249. package/src/daemon/server.ts +11 -2
  250. package/src/daemon/skill-memory-refresh.ts +29 -0
  251. package/src/documents/document-store.ts +221 -3
  252. package/src/embedded/plugin-api.ts +40 -0
  253. package/src/filing/filing-service.ts +39 -0
  254. package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
  255. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  256. package/src/heartbeat/heartbeat-service.ts +41 -0
  257. package/src/home/__tests__/feed-types.test.ts +40 -0
  258. package/src/home/feed-types.ts +22 -0
  259. package/src/home/post-connect-feed.ts +1 -0
  260. package/src/index.ts +18 -1
  261. package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
  262. package/src/mcp/client.ts +20 -4
  263. package/src/media/image-credentials.ts +3 -3
  264. package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
  265. package/src/memory/__tests__/conversation-queries.test.ts +263 -0
  266. package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
  267. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
  268. package/src/memory/__tests__/message-content.test.ts +35 -0
  269. package/src/memory/bookmark-crud.ts +42 -10
  270. package/src/memory/context-search/sources/conversations.ts +62 -2
  271. package/src/memory/context-search/sources/workspace.ts +4 -0
  272. package/src/memory/conversation-crud.ts +63 -19
  273. package/src/memory/conversation-queries.ts +110 -10
  274. package/src/memory/db-init.ts +6 -0
  275. package/src/memory/delivery-crud.ts +152 -5
  276. package/src/memory/embedding-backend.ts +4 -4
  277. package/src/memory/external-conversation-store.ts +66 -5
  278. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
  279. package/src/memory/graph/conversation-graph-memory.ts +31 -15
  280. package/src/memory/graph/tools.ts +3 -3
  281. package/src/memory/indexer.ts +34 -29
  282. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
  283. package/src/memory/jobs/embed-concept-page.ts +20 -11
  284. package/src/memory/jobs-worker.ts +6 -1
  285. package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
  286. package/src/memory/llm-request-log-source.ts +19 -52
  287. package/src/memory/llm-usage-store.ts +125 -5
  288. package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
  289. package/src/memory/message-content.ts +1 -1
  290. package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
  291. package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
  292. package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
  293. package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
  294. package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
  295. package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
  296. package/src/memory/migrations/index.ts +6 -0
  297. package/src/memory/migrations/registry.ts +8 -0
  298. package/src/memory/onboarding-events-store.ts +106 -0
  299. package/src/memory/schema/bookmarks.ts +0 -2
  300. package/src/memory/schema/calls.ts +1 -0
  301. package/src/memory/schema/inference.ts +1 -3
  302. package/src/memory/schema/infrastructure.ts +12 -0
  303. package/src/memory/turn-events-store.ts +127 -2
  304. package/src/memory/v2/__tests__/activation.test.ts +0 -8
  305. package/src/memory/v2/__tests__/injection.test.ts +98 -8
  306. package/src/memory/v2/__tests__/migration.test.ts +87 -0
  307. package/src/memory/v2/__tests__/page-index.test.ts +83 -0
  308. package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
  309. package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
  310. package/src/memory/v2/__tests__/router.test.ts +15 -0
  311. package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
  312. package/src/memory/v2/injection.ts +32 -6
  313. package/src/memory/v2/migration.ts +49 -19
  314. package/src/memory/v2/page-index.ts +35 -5
  315. package/src/memory/v2/prompts/router.ts +11 -8
  316. package/src/memory/v2/prompts/sweep.ts +2 -2
  317. package/src/memory/v2/qdrant.ts +135 -7
  318. package/src/memory/v2/router.ts +9 -8
  319. package/src/memory/v2/skill-store.ts +120 -35
  320. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
  321. package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
  322. package/src/messaging/providers/slack/adapter.ts +43 -5
  323. package/src/messaging/providers/slack/client.ts +27 -0
  324. package/src/messaging/providers/slack/deep-link.ts +65 -0
  325. package/src/messaging/providers/slack/download.ts +104 -0
  326. package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
  327. package/src/messaging/providers/slack/message-metadata.ts +27 -0
  328. package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
  329. package/src/messaging/providers/slack/render-transcript.ts +69 -5
  330. package/src/messaging/providers/slack/types.ts +20 -1
  331. package/src/notifications/conversation-pairing.ts +2 -1
  332. package/src/notifications/decision-engine.ts +2 -1
  333. package/src/notifications/emit-signal.ts +20 -1
  334. package/src/notifications/home-feed-side-effect.ts +54 -0
  335. package/src/notifications/signal.ts +3 -1
  336. package/src/oauth/connection-resolver.ts +8 -4
  337. package/src/oauth/platform-connection.ts +6 -2
  338. package/src/oauth/seed-providers.ts +10 -1
  339. package/src/permissions/checker.ts +2 -0
  340. package/src/permissions/ipc-risk-types.ts +1 -0
  341. package/src/permissions/question-prompter.test.ts +416 -0
  342. package/src/permissions/question-prompter.ts +294 -0
  343. package/src/platform/client.test.ts +1 -1
  344. package/src/platform/client.ts +1 -1
  345. package/src/plugin-api/constants.ts +26 -0
  346. package/src/plugin-api/index.ts +34 -1
  347. package/src/plugin-api/types.ts +104 -22
  348. package/src/plugins/defaults/circuit-breaker.ts +0 -5
  349. package/src/plugins/defaults/compaction.ts +0 -4
  350. package/src/plugins/defaults/empty-response.ts +0 -2
  351. package/src/plugins/defaults/history-repair.ts +0 -2
  352. package/src/plugins/defaults/injectors.ts +36 -3
  353. package/src/plugins/defaults/llm-call.ts +0 -2
  354. package/src/plugins/defaults/memory-retrieval.ts +0 -1
  355. package/src/plugins/defaults/overflow-reduce.ts +0 -1
  356. package/src/plugins/defaults/persistence.ts +0 -2
  357. package/src/plugins/defaults/title-generate.ts +0 -5
  358. package/src/plugins/defaults/token-estimate.ts +0 -2
  359. package/src/plugins/defaults/tool-error.ts +0 -7
  360. package/src/plugins/defaults/tool-execute.ts +0 -2
  361. package/src/plugins/defaults/tool-result-truncate.ts +0 -4
  362. package/src/plugins/ensure-plugin-api-shim.ts +96 -0
  363. package/src/plugins/external-api.ts +104 -0
  364. package/src/plugins/external-plugin-loader.ts +105 -32
  365. package/src/plugins/feature-gate.ts +22 -0
  366. package/src/plugins/pipeline.ts +37 -0
  367. package/src/plugins/registry.ts +48 -80
  368. package/src/plugins/types.ts +31 -26
  369. package/src/plugins/user-loader.ts +21 -2
  370. package/src/proactive-artifact/aux-message-injector.ts +11 -0
  371. package/src/proactive-artifact/job.test.ts +37 -5
  372. package/src/prompts/__tests__/system-prompt.test.ts +12 -0
  373. package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
  374. package/src/prompts/normalize-onboarding.ts +27 -0
  375. package/src/prompts/sections.ts +302 -0
  376. package/src/prompts/system-prompt.ts +63 -166
  377. package/src/prompts/templates/BOOTSTRAP.md +17 -1
  378. package/src/prompts/templates/system-sections.ts +173 -0
  379. package/src/providers/__tests__/inference.test.ts +22 -7
  380. package/src/providers/anthropic/client.ts +28 -28
  381. package/src/providers/connection-resolution.ts +7 -0
  382. package/src/providers/inference/adapter-factory.ts +41 -4
  383. package/src/providers/inference/connections.ts +74 -29
  384. package/src/providers/inference/resolve-auth.ts +12 -4
  385. package/src/providers/model-catalog.ts +294 -12
  386. package/src/providers/openai/chat-completions-provider.ts +10 -2
  387. package/src/providers/openrouter/client.ts +7 -0
  388. package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
  389. package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
  390. package/src/providers/provider-availability.ts +17 -2
  391. package/src/providers/provider-catalog-visibility.ts +36 -0
  392. package/src/providers/registry.ts +22 -14
  393. package/src/providers/retry.ts +47 -1
  394. package/src/runtime/__tests__/agent-wake.test.ts +152 -0
  395. package/src/runtime/agent-wake.ts +42 -14
  396. package/src/runtime/auth/route-policy.ts +8 -1
  397. package/src/runtime/btw-sidechain.ts +2 -0
  398. package/src/runtime/http-types.ts +19 -0
  399. package/src/runtime/migrations/origin-mode.ts +1 -1
  400. package/src/runtime/pending-interactions.ts +1 -0
  401. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
  402. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
  403. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
  404. package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
  405. package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
  406. package/src/runtime/routes/acp-routes-list.test.ts +143 -0
  407. package/src/runtime/routes/acp-routes.ts +5 -3
  408. package/src/runtime/routes/auth-routes.ts +1 -1
  409. package/src/runtime/routes/bookmark-routes.ts +5 -3
  410. package/src/runtime/routes/btw-routes.ts +5 -1
  411. package/src/runtime/routes/channel-availability-routes.ts +121 -0
  412. package/src/runtime/routes/conversation-cli-routes.ts +44 -3
  413. package/src/runtime/routes/conversation-list-routes.ts +3 -20
  414. package/src/runtime/routes/conversation-management-routes.ts +17 -42
  415. package/src/runtime/routes/conversation-query-routes.ts +40 -35
  416. package/src/runtime/routes/conversation-routes.ts +90 -11
  417. package/src/runtime/routes/documents-routes.ts +25 -86
  418. package/src/runtime/routes/group-routes.ts +5 -0
  419. package/src/runtime/routes/inbound-conversation.ts +28 -8
  420. package/src/runtime/routes/inbound-message-handler.ts +236 -41
  421. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
  422. package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
  423. package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
  424. package/src/runtime/routes/index.ts +6 -0
  425. package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
  426. package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
  427. package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
  428. package/src/runtime/routes/integrations/slack/share.ts +4 -52
  429. package/src/runtime/routes/integrations/slack/token.ts +43 -0
  430. package/src/runtime/routes/integrations/twilio.ts +6 -13
  431. package/src/runtime/routes/notification-routes.ts +1 -1
  432. package/src/runtime/routes/oauth-commands-routes.ts +105 -15
  433. package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
  434. package/src/runtime/routes/question-routes.ts +259 -0
  435. package/src/runtime/routes/rename-conversation-routes.ts +2 -33
  436. package/src/runtime/routes/schedule-routes.ts +4 -7
  437. package/src/runtime/routes/subagents-routes.ts +57 -18
  438. package/src/runtime/routes/telemetry-routes.ts +27 -0
  439. package/src/runtime/routes/tts-routes.ts +27 -2
  440. package/src/runtime/routes/workspace-routes.test.ts +43 -0
  441. package/src/runtime/routes/workspace-routes.ts +28 -0
  442. package/src/runtime/services/conversation-serializer.ts +39 -7
  443. package/src/runtime/sync/resource-sync-events.ts +93 -1
  444. package/src/schedule/schedule-store.ts +27 -2
  445. package/src/schedule/scheduler.ts +9 -1
  446. package/src/security/__tests__/untrusted-content.test.ts +86 -0
  447. package/src/security/untrusted-content.ts +93 -8
  448. package/src/skills/catalog-files.ts +1 -1
  449. package/src/skills/catalog-install.ts +233 -116
  450. package/src/skills/clawhub.ts +70 -13
  451. package/src/skills/managed-store.ts +4 -119
  452. package/src/skills/skillssh-registry.ts +27 -48
  453. package/src/subagent/manager.ts +15 -7
  454. package/src/telemetry/types.ts +113 -1
  455. package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
  456. package/src/telemetry/usage-telemetry-reporter.ts +113 -7
  457. package/src/tools/apps/executors.ts +58 -7
  458. package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
  459. package/src/tools/ask-question/ask-question-tool.ts +304 -0
  460. package/src/tools/browser/browser-execution.ts +15 -11
  461. package/src/tools/computer-use/definitions.ts +3 -3
  462. package/src/tools/credentials/vault.ts +1 -1
  463. package/src/tools/document/document-tool.ts +124 -1
  464. package/src/tools/filesystem/edit.ts +1 -1
  465. package/src/tools/filesystem/list.ts +1 -1
  466. package/src/tools/filesystem/read.ts +1 -1
  467. package/src/tools/filesystem/write.ts +5 -2
  468. package/src/tools/host-filesystem/transfer.ts +1 -1
  469. package/src/tools/host-terminal/host-shell.ts +1 -1
  470. package/src/tools/permission-checker.ts +1 -1
  471. package/src/tools/registry.ts +17 -7
  472. package/src/tools/schedule/create.ts +2 -2
  473. package/src/tools/schema-transforms.ts +7 -2
  474. package/src/tools/side-effects.ts +1 -0
  475. package/src/tools/skills/delete-managed.ts +4 -4
  476. package/src/tools/skills/execute.ts +1 -1
  477. package/src/tools/skills/scaffold-managed.ts +3 -2
  478. package/src/tools/subagent/notify-parent.ts +1 -1
  479. package/src/tools/system/request-permission.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +60 -1
  481. package/src/tools/tool-manifest.ts +2 -0
  482. package/src/tools/types.ts +72 -21
  483. package/src/tools/ui-surface/definitions.ts +6 -5
  484. package/src/tts/__tests__/provider-adapters.test.ts +76 -2
  485. package/src/tts/providers/elevenlabs-provider.ts +75 -1
  486. package/src/types/onboarding-context.ts +2 -0
  487. package/src/util/errors.ts +17 -0
  488. package/src/util/platform.ts +10 -0
  489. package/src/watcher/__tests__/engine.test.ts +22 -0
  490. package/src/watcher/engine.ts +6 -2
  491. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
  492. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
  493. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
  494. package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
  495. package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
  496. package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
  497. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
  498. package/src/workspace/migrations/registry.ts +8 -0
  499. package/src/workspace/migrations/runner.ts +39 -9
  500. package/src/workspace/migrations/types.ts +4 -0
  501. package/examples/plugins/echo/bun.lock +0 -25
  502. package/src/__tests__/context-window-manager.test.ts +0 -2481
  503. package/src/context/__tests__/compact-prompt.test.ts +0 -63
  504. package/src/context/prompts/compact.md +0 -26
  505. package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
  506. /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
@@ -15,6 +15,8 @@ type ConvRow = {
15
15
  id: string;
16
16
  source: string | null;
17
17
  last_message_at: number | null;
18
+ fork_parent_conversation_id: string | null;
19
+ created_at: number;
18
20
  };
19
21
  type JobRow = {
20
22
  type: string;
@@ -58,11 +60,26 @@ mock.module("../db-connection.js", () => ({
58
60
  return { conversationId: convId };
59
61
  });
60
62
  }
61
- // Otherwise, this is the conversation query.
62
- // The test harness applies its OWN filter logic below since the
63
- // production code uses drizzle's combinator. We expose all rows
64
- // tagged with the right source/last_message_at, and the test
65
- // post-filters them to mirror the production query.
63
+ // The "all retros" query (used to compute most-recent-per-source
64
+ // preservation) requests id + forkParentConversationId + createdAt
65
+ // with only the source + isNotNull(forkParent) predicate.
66
+ if (
67
+ colKeys.includes("forkParentConversationId") &&
68
+ colKeys.includes("createdAt")
69
+ ) {
70
+ return mockConversations
71
+ .filter((c) => c.source === "memory-retrospective")
72
+ .filter((c) => c.fork_parent_conversation_id !== null)
73
+ .map((c) => ({
74
+ id: c.id,
75
+ forkParentConversationId: c.fork_parent_conversation_id,
76
+ createdAt: c.created_at,
77
+ }));
78
+ }
79
+ // Otherwise, this is the orphan-candidate query. The production
80
+ // predicate compares `forkParentConversationId` (the source ID
81
+ // encoded on the background conversation row) against the set
82
+ // of source IDs extracted from active jobs.
66
83
  return mockConversations
67
84
  .filter((c) => c.source === "memory-retrospective")
68
85
  .filter(
@@ -70,7 +87,11 @@ mock.module("../db-connection.js", () => ({
70
87
  c.last_message_at !== null &&
71
88
  c.last_message_at < injectedNowMinusOrphanAgeMs,
72
89
  )
73
- .filter((c) => !activeJobConvIds.has(c.id))
90
+ .filter(
91
+ (c) =>
92
+ c.fork_parent_conversation_id === null ||
93
+ !activeJobSourceConvIds.has(c.fork_parent_conversation_id),
94
+ )
74
95
  .map((c) => ({ id: c.id }));
75
96
  },
76
97
  }),
@@ -79,7 +100,7 @@ mock.module("../db-connection.js", () => ({
79
100
  }),
80
101
  }));
81
102
 
82
- let activeJobConvIds = new Set<string>();
103
+ let activeJobSourceConvIds = new Set<string>();
83
104
  let injectedNowMinusOrphanAgeMs = 0;
84
105
 
85
106
  mock.module("../conversation-crud.js", () => ({
@@ -94,7 +115,7 @@ import { sweepOrphanMemoryRetrospectiveConversations } from "../memory-retrospec
94
115
  const ORPHAN_AGE_MS = 60 * 60 * 1000;
95
116
 
96
117
  function rebuildActiveJobSet(): void {
97
- activeJobConvIds = new Set();
118
+ activeJobSourceConvIds = new Set();
98
119
  for (const j of mockJobs) {
99
120
  if (
100
121
  j.type !== "memory_retrospective" ||
@@ -105,7 +126,7 @@ function rebuildActiveJobSet(): void {
105
126
  try {
106
127
  const parsed = JSON.parse(j.payload) as { conversationId?: unknown };
107
128
  if (typeof parsed.conversationId === "string") {
108
- activeJobConvIds.add(parsed.conversationId);
129
+ activeJobSourceConvIds.add(parsed.conversationId);
109
130
  }
110
131
  } catch {
111
132
  // ignore
@@ -118,7 +139,7 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
118
139
  mockConversations = [];
119
140
  mockJobs = [];
120
141
  deletedIds = [];
121
- activeJobConvIds = new Set();
142
+ activeJobSourceConvIds = new Set();
122
143
  injectedNowMinusOrphanAgeMs = 0;
123
144
  });
124
145
 
@@ -127,7 +148,7 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
127
148
  mockJobs = [];
128
149
  });
129
150
 
130
- test("sweeps an old memory-retrospective conversation with no active job", () => {
151
+ test("sweeps an old memory-retrospective orphan that has been superseded by a newer retro for the same source", () => {
131
152
  const now = Date.now();
132
153
  injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
133
154
  mockConversations = [
@@ -135,6 +156,16 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
135
156
  id: "old-orphan",
136
157
  source: "memory-retrospective",
137
158
  last_message_at: now - 2 * ORPHAN_AGE_MS,
159
+ fork_parent_conversation_id: "source-A",
160
+ created_at: now - 3 * ORPHAN_AGE_MS,
161
+ },
162
+ // Newer successful retro for the same source — this one is preserved.
163
+ {
164
+ id: "newer-retro",
165
+ source: "memory-retrospective",
166
+ last_message_at: now - 90 * 60 * 1000,
167
+ fork_parent_conversation_id: "source-A",
168
+ created_at: now - 2 * ORPHAN_AGE_MS,
138
169
  },
139
170
  ];
140
171
  rebuildActiveJobSet();
@@ -153,6 +184,8 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
153
184
  id: "fresh-bg",
154
185
  source: "memory-retrospective",
155
186
  last_message_at: now - 60_000,
187
+ fork_parent_conversation_id: "source-A",
188
+ created_at: now - 60_000,
156
189
  },
157
190
  ];
158
191
  rebuildActiveJobSet();
@@ -171,6 +204,8 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
171
204
  id: "auto-analysis-old",
172
205
  source: "auto-analysis",
173
206
  last_message_at: now - 2 * ORPHAN_AGE_MS,
207
+ fork_parent_conversation_id: "source-A",
208
+ created_at: now - 2 * ORPHAN_AGE_MS,
174
209
  },
175
210
  ];
176
211
  rebuildActiveJobSet();
@@ -180,21 +215,91 @@ describe("sweepOrphanMemoryRetrospectiveConversations", () => {
180
215
  expect(result.swept).toBe(0);
181
216
  });
182
217
 
183
- test("does NOT sweep an orphan whose source conversation has an active job", () => {
218
+ // Regression test for the previously-broken active-job guard. Before the
219
+ // fix, the predicate compared `conversations.id` (the BACKGROUND-conv id)
220
+ // to source-conv ids extracted from job payloads — two different identifier
221
+ // spaces — so the guard never matched and in-flight retros were swept.
222
+ test("does NOT sweep a background conversation whose SOURCE has an active job (different identifier spaces)", () => {
184
223
  const now = Date.now();
185
224
  injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
225
+ // The background conv has its own id, distinct from the source it forks
226
+ // from. The active job's payload references the SOURCE, not the
227
+ // background.
186
228
  mockConversations = [
187
229
  {
188
- id: "orphan-but-protected",
230
+ id: "background-conv-id",
189
231
  source: "memory-retrospective",
190
232
  last_message_at: now - 2 * ORPHAN_AGE_MS,
233
+ fork_parent_conversation_id: "source-conv-id",
234
+ created_at: now - 2 * ORPHAN_AGE_MS,
191
235
  },
192
236
  ];
193
237
  mockJobs = [
194
238
  {
195
239
  type: "memory_retrospective",
196
240
  status: "pending",
197
- payload: JSON.stringify({ conversationId: "orphan-but-protected" }),
241
+ payload: JSON.stringify({ conversationId: "source-conv-id" }),
242
+ },
243
+ ];
244
+ rebuildActiveJobSet();
245
+
246
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
247
+
248
+ expect(result.swept).toBe(0);
249
+ expect(deletedIds).toEqual([]);
250
+ });
251
+
252
+ test("sweeps a superseded background conversation whose source has NO active job, even when another unrelated job is pending", () => {
253
+ const now = Date.now();
254
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
255
+ mockConversations = [
256
+ {
257
+ id: "background-A",
258
+ source: "memory-retrospective",
259
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
260
+ fork_parent_conversation_id: "source-A",
261
+ created_at: now - 3 * ORPHAN_AGE_MS,
262
+ },
263
+ {
264
+ id: "newer-A",
265
+ source: "memory-retrospective",
266
+ last_message_at: now - 90 * 60 * 1000,
267
+ fork_parent_conversation_id: "source-A",
268
+ created_at: now - 2 * ORPHAN_AGE_MS,
269
+ },
270
+ ];
271
+ // Active job references a DIFFERENT source — neither retro above is
272
+ // protected by the active-job guard.
273
+ mockJobs = [
274
+ {
275
+ type: "memory_retrospective",
276
+ status: "pending",
277
+ payload: JSON.stringify({ conversationId: "source-B" }),
278
+ },
279
+ ];
280
+ rebuildActiveJobSet();
281
+
282
+ const result = sweepOrphanMemoryRetrospectiveConversations(now);
283
+
284
+ expect(result.swept).toBe(1);
285
+ expect(deletedIds).toEqual(["background-A"]);
286
+ });
287
+
288
+ // Regression test for Devin's concern on PR #30331: the sweep used to
289
+ // delete every memory-retrospective conversation older than 1h, including
290
+ // the most-recent successful one per source. That broke
291
+ // `findMostRecentRetrospectiveFor` on the next run — the next retro had
292
+ // no dedup context and could re-save facts the prior pass already captured.
293
+ test("PRESERVES the most-recent retro per source even when older than the orphan cutoff", () => {
294
+ const now = Date.now();
295
+ injectedNowMinusOrphanAgeMs = now - ORPHAN_AGE_MS;
296
+ mockConversations = [
297
+ {
298
+ id: "successful-retro",
299
+ source: "memory-retrospective",
300
+ last_message_at: now - 2 * ORPHAN_AGE_MS,
301
+ fork_parent_conversation_id: "source-A",
302
+ created_at: now - 2 * ORPHAN_AGE_MS,
198
303
  },
199
304
  ];
200
305
  rebuildActiveJobSet();
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { stringifyMessageContent } from "../message-content.js";
4
+
5
+ describe("stringifyMessageContent", () => {
6
+ test("returns trimmed raw text for legacy plain-string rows", () => {
7
+ expect(stringifyMessageContent(" hello world ")).toBe("hello world");
8
+ });
9
+
10
+ test("returns trimmed inner string when JSON parses to a string", () => {
11
+ expect(stringifyMessageContent(JSON.stringify(" inner "))).toBe("inner");
12
+ });
13
+
14
+ test("concatenates text blocks from a ContentBlock[] payload", () => {
15
+ const raw = JSON.stringify([
16
+ { type: "text", text: "alpha" },
17
+ { type: "tool_use", id: "x", name: "noop", input: {} },
18
+ { type: "text", text: "beta" },
19
+ ]);
20
+ expect(stringifyMessageContent(raw)).toBe("alpha\nbeta");
21
+ });
22
+
23
+ test("falls back to raw trimmed text when JSON parses to a non-array object", () => {
24
+ const raw = ' {"type":"text","text":"hi"} ';
25
+ expect(stringifyMessageContent(raw)).toBe('{"type":"text","text":"hi"}');
26
+ });
27
+
28
+ test("falls back to raw trimmed text when JSON parses to a number", () => {
29
+ expect(stringifyMessageContent(" 42 ")).toBe("42");
30
+ });
31
+
32
+ test("returns trimmed raw text when JSON parsing fails", () => {
33
+ expect(stringifyMessageContent(" not json ")).toBe("not json");
34
+ });
35
+ });
@@ -106,22 +106,51 @@ export function listBookmarks(db: DrizzleDb): BookmarkSummary[] {
106
106
  }
107
107
 
108
108
  /**
109
- * Create a bookmark for the given message and return its JOIN-shaped
110
- * {@link BookmarkSummary}. Idempotent on the unique `message_id` index
111
- * if a bookmark already exists for `messageId`, the existing summary is
112
- * returned and no new row is inserted.
109
+ * Discriminated result returned by {@link createBookmark}. `inserted`
110
+ * distinguishes a brand-new row from an idempotent return of an existing
111
+ * one, so callers can suppress side effects (e.g. `bookmark.created` SSE
112
+ * publishes) on duplicate POSTs.
113
+ */
114
+ export type CreateBookmarkResult =
115
+ | { inserted: true; bookmark: BookmarkSummary }
116
+ | { inserted: false; bookmark: BookmarkSummary };
117
+
118
+ /**
119
+ * Create a bookmark for the given message and return a discriminated
120
+ * result indicating whether a new row was actually inserted. Idempotent
121
+ * on the unique `message_id` index — if a bookmark already exists for
122
+ * `messageId`, the existing summary is returned with `inserted: false`.
123
+ *
124
+ * `conversationId` is derived from the message row rather than trusted from
125
+ * the caller, so a buggy or malicious caller cannot persist a bookmark
126
+ * whose `conversationId` disagrees with the message's actual conversation.
113
127
  */
114
128
  export function createBookmark(
115
129
  db: DrizzleDb,
116
- params: { messageId: string; conversationId: string },
117
- ): BookmarkSummary {
118
- const { messageId, conversationId } = params;
130
+ params: { messageId: string },
131
+ ): CreateBookmarkResult {
132
+ const { messageId } = params;
133
+ const message = db
134
+ .select({ conversationId: messages.conversationId })
135
+ .from(messages)
136
+ .where(eq(messages.id, messageId))
137
+ .get();
138
+ if (!message) {
139
+ throw new Error(`Message ${messageId} not found`);
140
+ }
141
+ const conversationId = message.conversationId;
142
+
119
143
  const existing = db
120
144
  .select({ id: messageBookmarks.id })
121
145
  .from(messageBookmarks)
122
146
  .where(eq(messageBookmarks.messageId, messageId))
123
147
  .get();
124
- if (existing) return readBookmarkSummaryOrThrow(db, existing.id);
148
+ if (existing) {
149
+ return {
150
+ inserted: false,
151
+ bookmark: readBookmarkSummaryOrThrow(db, existing.id),
152
+ };
153
+ }
125
154
 
126
155
  const id = uuid();
127
156
  try {
@@ -137,9 +166,12 @@ export function createBookmark(
137
166
  .where(eq(messageBookmarks.messageId, messageId))
138
167
  .get();
139
168
  if (!winner) throw err;
140
- return readBookmarkSummaryOrThrow(db, winner.id);
169
+ return {
170
+ inserted: false,
171
+ bookmark: readBookmarkSummaryOrThrow(db, winner.id),
172
+ };
141
173
  }
142
- return readBookmarkSummaryOrThrow(db, id);
174
+ return { inserted: true, bookmark: readBookmarkSummaryOrThrow(db, id) };
143
175
  }
144
176
 
145
177
  function readBookmarkSummaryOrThrow(
@@ -1,7 +1,12 @@
1
+ import { readSlackMetadata } from "../../../messaging/providers/slack/message-metadata.js";
2
+ import {
3
+ parseExternalContentEnvelope,
4
+ wrapUntrustedContent,
5
+ } from "../../../security/untrusted-content.js";
1
6
  import { AUTO_ANALYSIS_SOURCE } from "../../auto-analysis-guard.js";
2
7
  import {
3
- buildExcerpt,
4
8
  buildFtsMatchQuery,
9
+ buildRecallEvidenceExcerpt,
5
10
  } from "../../conversation-queries.js";
6
11
  import { rawAll } from "../../raw-query.js";
7
12
  import type { RecallSearchContext, RecallSearchResult } from "../types.js";
@@ -15,6 +20,7 @@ interface ConversationEvidenceRow {
15
20
  role: string;
16
21
  content: string;
17
22
  created_at: number;
23
+ metadata: string | null;
18
24
  title: string | null;
19
25
  }
20
26
 
@@ -118,7 +124,7 @@ export async function searchConversationSource(
118
124
  source: "conversations",
119
125
  title: row.title?.trim() || "Untitled conversation",
120
126
  locator: `${row.conversation_id}#${row.message_id}`,
121
- excerpt: buildExcerpt(row.content, trimmedQuery),
127
+ excerpt: buildRecallExcerpt(row, trimmedQuery),
122
128
  timestampMs: row.created_at,
123
129
  score,
124
130
  metadata: {
@@ -142,6 +148,7 @@ function searchWithFts(
142
148
  m.role,
143
149
  m.content,
144
150
  m.created_at,
151
+ m.metadata,
145
152
  c.title
146
153
  FROM messages_fts
147
154
  JOIN messages m ON m.id = messages_fts.message_id
@@ -175,6 +182,7 @@ function searchWithLike(
175
182
  m.role,
176
183
  m.content,
177
184
  m.created_at,
185
+ m.metadata,
178
186
  c.title
179
187
  FROM messages m
180
188
  JOIN conversations c ON c.id = m.conversation_id
@@ -194,6 +202,58 @@ function searchWithLike(
194
202
  );
195
203
  }
196
204
 
205
+ function buildRecallExcerpt(
206
+ row: ConversationEvidenceRow,
207
+ query: string,
208
+ ): string {
209
+ const excerpt = buildRecallEvidenceExcerpt(row.content, query);
210
+ const slackMeta = parseSlackRecallMetadata(row.metadata);
211
+ if (
212
+ row.role !== "user" ||
213
+ !slackMeta ||
214
+ slackMeta.provenanceTrustClass === "guardian" ||
215
+ excerpt.length === 0 ||
216
+ parseExternalContentEnvelope(excerpt)
217
+ ) {
218
+ return excerpt;
219
+ }
220
+
221
+ return wrapUntrustedContent(excerpt, {
222
+ source: "slack",
223
+ ...(slackMeta.displayName ? { sourceDetail: slackMeta.displayName } : {}),
224
+ });
225
+ }
226
+
227
+ function parseSlackRecallMetadata(rawMetadata: string | null): {
228
+ displayName?: string;
229
+ provenanceTrustClass?: string;
230
+ } | null {
231
+ if (!rawMetadata) return null;
232
+
233
+ let parsed: unknown;
234
+ try {
235
+ parsed = JSON.parse(rawMetadata);
236
+ } catch {
237
+ return null;
238
+ }
239
+
240
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
241
+ return null;
242
+ }
243
+
244
+ const metadata = parsed as Record<string, unknown>;
245
+ if (typeof metadata.slackMeta !== "string") return null;
246
+ const slackMeta = readSlackMetadata(metadata.slackMeta);
247
+ if (!slackMeta) return null;
248
+
249
+ return {
250
+ ...(slackMeta.displayName ? { displayName: slackMeta.displayName } : {}),
251
+ ...(typeof metadata.provenanceTrustClass === "string"
252
+ ? { provenanceTrustClass: metadata.provenanceTrustClass }
253
+ : {}),
254
+ };
255
+ }
256
+
197
257
  function buildRecallFtsMatchQueries(query: string): string[] {
198
258
  const queries: string[] = [];
199
259
  const exact = buildFtsMatchQuery(query);
@@ -68,6 +68,9 @@ const SECRET_SEGMENT_NAMES = new Set([
68
68
  const SECRET_TOKEN_PATTERN =
69
69
  /(?:^|[-_.])(?:keys?|secrets?|tokens?)(?:[-_.]|$)/i;
70
70
 
71
+ const SECRET_TOKEN_CAMEL_CASE_PATTERN =
72
+ /(?<=[a-z0-9])(?:Keys?|Secrets?|Tokens?)(?=[A-Z]|[-_.]|$)/;
73
+
71
74
  const QUERY_STOP_WORDS = new Set([
72
75
  "a",
73
76
  "about",
@@ -1185,6 +1188,7 @@ function shouldSkipSegmentName(name: string): boolean {
1185
1188
  GENERATED_OR_DEPENDENCY_DIR_NAMES.has(lowerName) ||
1186
1189
  lowerName.startsWith(".env") ||
1187
1190
  SECRET_TOKEN_PATTERN.test(lowerName) ||
1191
+ SECRET_TOKEN_CAMEL_CASE_PATTERN.test(name) ||
1188
1192
  lowerName.startsWith("credentials") ||
1189
1193
  SECRET_SEGMENT_NAMES.has(lowerName)
1190
1194
  );
@@ -14,6 +14,7 @@ import {
14
14
  like,
15
15
  lt,
16
16
  lte,
17
+ or,
17
18
  sql,
18
19
  } from "drizzle-orm";
19
20
  import { v4 as uuid } from "uuid";
@@ -86,6 +87,7 @@ const subagentNotificationSchema = z.object({
86
87
  status: z.enum(["running", "completed", "failed", "aborted"]),
87
88
  error: z.string().optional(),
88
89
  conversationId: z.string().optional(),
90
+ objective: z.string().optional(),
89
91
  });
90
92
 
91
93
  export const messageMetadataSchema = z
@@ -94,6 +96,16 @@ export const messageMetadataSchema = z
94
96
  assistantMessageChannel: channelIdSchema.optional(),
95
97
  userMessageInterface: interfaceIdSchema.optional(),
96
98
  assistantMessageInterface: interfaceIdSchema.optional(),
99
+ /**
100
+ * Optional client-side metadata bag attached to user messages by HTTP
101
+ * header middleware (reads `x-vellum-browser-family`,
102
+ * `x-vellum-browser-version`, `x-vellum-client-os`,
103
+ * `x-vellum-interface-version`). Forwarded verbatim onto
104
+ * `TurnTelemetryEvent.client` for downstream analytics. Kept as a
105
+ * permissive `record` so adding a new client field doesn't require a
106
+ * migration -- dbt can unpack later via JSON_VALUE.
107
+ */
108
+ client: z.record(z.string(), z.unknown()).optional(),
97
109
  subagentNotification: subagentNotificationSchema.optional(),
98
110
  /**
99
111
  * Trust class of the actor at the time this message was persisted.
@@ -727,9 +739,14 @@ export function forkConversation(params: {
727
739
 
728
740
  // Carry the parent's per-conversation memory state into the child so the
729
741
  // forked thread resumes with the same activation/injection log and
730
- // in-context tracker the parent had at fork time.
731
- forkActivationState(db, sourceConversation.id, fc.id);
732
- forkGraphMemoryState(sourceConversation.id, fc.id);
742
+ // in-context tracker the parent had at fork time. Only valid for
743
+ // full-history forks: a truncated fork would inherit activation/tracker
744
+ // entries for turns the child does not actually contain.
745
+ const isFullHistoryFork = copyBoundaryIndex === sourceMessages.length - 1;
746
+ if (isFullHistoryFork) {
747
+ forkActivationState(db, sourceConversation.id, fc.id);
748
+ forkGraphMemoryState(sourceConversation.id, fc.id);
749
+ }
733
750
  forkRetrospectiveState({
734
751
  database: db,
735
752
  sourceConversationId: sourceConversation.id,
@@ -1040,22 +1057,23 @@ export function getMessages(conversationId: string): MessageRow[] {
1040
1057
  }
1041
1058
 
1042
1059
  /**
1043
- * Return raw `metadata` strings for messages whose metadata contains the
1044
- * literal substring `"slackMeta"`, capped at `limit` and skipping the first
1045
- * `offset` matches. Pushes `LIKE` + `LIMIT`/`OFFSET` into SQL so warm Slack
1046
- * DM conversations don't require a full-table scan on the webhook critical
1047
- * path. The substring match is an indexable prefilter only — callers must
1048
- * parse and validate each returned string against the Slack metadata schema,
1049
- * because a malformed row (partial write, legacy format, unrelated key
1050
- * accidentally containing the literal) can still slip through the substring
1051
- * match. Callers that need a fixed number of *valid* rows should iterate
1052
- * with increasing offsets until the target is reached (capped at a
1053
- * reasonable maximum to bound scan cost).
1060
+ * Return raw `metadata` strings for messages whose metadata looks like it may
1061
+ * contain Slack metadata, capped at `limit` and skipping the first `offset`
1062
+ * matches. Pushes `LIKE` + `LIMIT`/`OFFSET` into SQL so warm Slack DM
1063
+ * conversations don't require a full-table scan on the webhook critical path.
1064
+ * The substring match is an indexable prefilter only — callers must parse and
1065
+ * validate each returned string against the Slack metadata schema, because a
1066
+ * malformed row (partial write, legacy format, unrelated key accidentally
1067
+ * containing the literal) can still slip through the substring match. Callers
1068
+ * that need a fixed number of *valid* rows should iterate with increasing
1069
+ * offsets until the target is reached (capped at a reasonable maximum to bound
1070
+ * scan cost).
1054
1071
  */
1055
1072
  export function selectSlackMetaCandidateMetadata(
1056
1073
  conversationId: string,
1057
1074
  limit: number,
1058
1075
  offset = 0,
1076
+ opts?: { includeFlatLegacy?: boolean },
1059
1077
  ): string[] {
1060
1078
  const db = getDb();
1061
1079
  const rows = db
@@ -1064,7 +1082,12 @@ export function selectSlackMetaCandidateMetadata(
1064
1082
  .where(
1065
1083
  and(
1066
1084
  eq(messages.conversationId, conversationId),
1067
- like(messages.metadata, '%"slackMeta"%'),
1085
+ opts?.includeFlatLegacy
1086
+ ? or(
1087
+ like(messages.metadata, '%"slackMeta"%'),
1088
+ like(messages.metadata, '%"source":"slack"%'),
1089
+ )
1090
+ : like(messages.metadata, '%"slackMeta"%'),
1068
1091
  ),
1069
1092
  )
1070
1093
  .orderBy(asc(messages.createdAt))
@@ -1110,13 +1133,23 @@ export function countMessagesAfter(
1110
1133
  .where(eq(messages.id, afterMessageId))
1111
1134
  .get();
1112
1135
  if (!ref) return 0;
1136
+ // Tie-breaker on `messages.id` so rows that share a millisecond timestamp
1137
+ // with the reference are not permanently skipped. Mirrors the
1138
+ // `(createdAt, id)` cursor pattern used by the backfill job-handler and
1139
+ // turn-events-store.
1113
1140
  const row = db
1114
1141
  .select({ c: count() })
1115
1142
  .from(messages)
1116
1143
  .where(
1117
1144
  and(
1118
1145
  eq(messages.conversationId, conversationId),
1119
- gt(messages.createdAt, ref.createdAt),
1146
+ or(
1147
+ gt(messages.createdAt, ref.createdAt),
1148
+ and(
1149
+ eq(messages.createdAt, ref.createdAt),
1150
+ gt(messages.id, afterMessageId),
1151
+ ),
1152
+ ),
1120
1153
  ),
1121
1154
  )
1122
1155
  .get();
@@ -1136,11 +1169,14 @@ export function getMessagesAfter(
1136
1169
  ): MessageRow[] {
1137
1170
  const db = getDb();
1138
1171
  if (afterMessageId === null || afterMessageId === "") {
1172
+ // Secondary `asc(messages.id)` matches the non-null path's cursor
1173
+ // ordering, so callers tracking `cutoffMessageId` across runs see a
1174
+ // consistent ordering when multiple rows share a millisecond timestamp.
1139
1175
  return db
1140
1176
  .select()
1141
1177
  .from(messages)
1142
1178
  .where(eq(messages.conversationId, conversationId))
1143
- .orderBy(asc(messages.createdAt))
1179
+ .orderBy(asc(messages.createdAt), asc(messages.id))
1144
1180
  .all()
1145
1181
  .map(parseMessage);
1146
1182
  }
@@ -1150,16 +1186,24 @@ export function getMessagesAfter(
1150
1186
  .where(eq(messages.id, afterMessageId))
1151
1187
  .get();
1152
1188
  if (!ref) return [];
1189
+ // Same `(createdAt, id)` cursor as `countMessagesAfter` — rows sharing
1190
+ // the reference's millisecond timestamp would otherwise be skipped.
1153
1191
  return db
1154
1192
  .select()
1155
1193
  .from(messages)
1156
1194
  .where(
1157
1195
  and(
1158
1196
  eq(messages.conversationId, conversationId),
1159
- gt(messages.createdAt, ref.createdAt),
1197
+ or(
1198
+ gt(messages.createdAt, ref.createdAt),
1199
+ and(
1200
+ eq(messages.createdAt, ref.createdAt),
1201
+ gt(messages.id, afterMessageId),
1202
+ ),
1203
+ ),
1160
1204
  ),
1161
1205
  )
1162
- .orderBy(asc(messages.createdAt))
1206
+ .orderBy(asc(messages.createdAt), asc(messages.id))
1163
1207
  .all()
1164
1208
  .map(parseMessage);
1165
1209
  }