@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.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 (433) hide show
  1. package/docs/activation-funnel-telemetry.md +24 -18
  2. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  3. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  4. package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
  5. package/openapi.yaml +852 -15
  6. package/package.json +1 -1
  7. package/src/__tests__/access-request-card-view.test.ts +98 -0
  8. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  9. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
  10. package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  12. package/src/__tests__/app-compiler.test.ts +15 -1
  13. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  14. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  15. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  16. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  17. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  18. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  19. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  20. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +174 -30
  22. package/src/__tests__/config-schema.test.ts +35 -0
  23. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  24. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  25. package/src/__tests__/contacts-tools.test.ts +29 -0
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  27. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  28. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  29. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  30. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  31. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  32. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  33. package/src/__tests__/conversation-title-service.test.ts +62 -0
  34. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  35. package/src/__tests__/credential-prompt-route.test.ts +1 -0
  36. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  37. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  38. package/src/__tests__/disk-usage.test.ts +65 -0
  39. package/src/__tests__/dynamic-page-surface.test.ts +51 -0
  40. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  41. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  42. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  43. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  44. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  45. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  46. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  47. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  48. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  49. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  50. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  51. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  52. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  53. package/src/__tests__/invite-redemption-service.test.ts +0 -3
  54. package/src/__tests__/llm-catalog-parity.test.ts +30 -1
  55. package/src/__tests__/llm-resolver.test.ts +21 -0
  56. package/src/__tests__/llm-schema.test.ts +1 -0
  57. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  58. package/src/__tests__/mcp-health-check.test.ts +6 -7
  59. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  60. package/src/__tests__/path-policy.test.ts +34 -0
  61. package/src/__tests__/persona-resolver.test.ts +38 -0
  62. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  63. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  64. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  65. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  66. package/src/__tests__/reaction-persistence.test.ts +150 -29
  67. package/src/__tests__/relay-server.test.ts +285 -0
  68. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  69. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  70. package/src/__tests__/skill-execute-input.test.ts +5 -0
  71. package/src/__tests__/skills.test.ts +51 -0
  72. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  73. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  74. package/src/__tests__/subagent-tools.test.ts +150 -0
  75. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  76. package/src/__tests__/title-generate-hook.test.ts +100 -3
  77. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  78. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  79. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  80. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  81. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  82. package/src/__tests__/trusted-contact-verification.test.ts +2 -4
  83. package/src/__tests__/twilio-routes.test.ts +81 -1
  84. package/src/__tests__/voice-invite-redemption.test.ts +0 -1
  85. package/src/__tests__/weak-open-model.test.ts +30 -0
  86. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  87. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  88. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  89. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  90. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  91. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  92. package/src/agent/loop.ts +33 -33
  93. package/src/api/events/tool-result.ts +6 -0
  94. package/src/api/events/workflow-completed.ts +53 -0
  95. package/src/api/events/workflow-leaf-finished.ts +38 -0
  96. package/src/api/events/workflow-leaf-started.ts +35 -0
  97. package/src/api/events/workflow-progress.ts +32 -0
  98. package/src/api/events/workflow-started.ts +31 -0
  99. package/src/api/index.ts +40 -0
  100. package/src/api/responses/conversation-message.ts +26 -0
  101. package/src/api/responses/home.ts +26 -0
  102. package/src/api/responses/workflow-journal.ts +53 -0
  103. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  104. package/src/approvals/guardian-decision-primitive.ts +26 -3
  105. package/src/approvals/guardian-request-resolvers.ts +181 -78
  106. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  107. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  108. package/src/calls/call-pointer-messages.ts +10 -4
  109. package/src/calls/channel-admission-reader.ts +104 -0
  110. package/src/calls/guardian-dispatch.ts +17 -45
  111. package/src/calls/media-stream-server.ts +84 -2
  112. package/src/calls/relay-server.ts +66 -0
  113. package/src/calls/relay-setup-router.ts +82 -1
  114. package/src/calls/twilio-routes.ts +17 -8
  115. package/src/cli/commands/clients.ts +3 -0
  116. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  117. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  118. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  119. package/src/cli/commands/memory/index.ts +30 -0
  120. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  121. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  122. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  123. package/src/cli/commands/oauth/status.test.ts +36 -0
  124. package/src/cli/commands/oauth/status.ts +23 -3
  125. package/src/cli/commands/plugins.ts +57 -5
  126. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  127. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
  128. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  129. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
  130. package/src/cli/lib/inspect-plugin.ts +12 -1
  131. package/src/cli/lib/merge-plugin-tree.ts +149 -49
  132. package/src/cli/lib/plugin-surfaces.ts +104 -0
  133. package/src/cli/lib/upgrade-plugin.ts +64 -36
  134. package/src/cli/program.ts +2 -4
  135. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  136. package/src/config/assistant-feature-flags.ts +22 -7
  137. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  138. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  139. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  140. package/src/config/bundled-skills/workflows/SKILL.md +14 -7
  141. package/src/config/call-site-defaults.ts +3 -0
  142. package/src/config/feature-flag-registry.json +49 -18
  143. package/src/config/llm-resolver.ts +3 -0
  144. package/src/config/memory-v3-gate.ts +11 -0
  145. package/src/config/schema.ts +8 -6
  146. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  147. package/src/config/schemas/call-site-catalog.ts +7 -0
  148. package/src/config/schemas/channels.ts +11 -0
  149. package/src/config/schemas/llm.ts +31 -0
  150. package/src/config/schemas/memory-lifecycle.ts +3 -7
  151. package/src/config/schemas/memory-v3.ts +6 -0
  152. package/src/config/schemas/services.ts +18 -0
  153. package/src/config/seed-inference-profiles.ts +94 -34
  154. package/src/config/skills.ts +21 -0
  155. package/src/config/sync-gated-profiles.ts +220 -0
  156. package/src/contacts/contact-store.ts +2 -10
  157. package/src/contacts/contacts-write.ts +1 -2
  158. package/src/contacts/types.ts +0 -1
  159. package/src/context/compactor.ts +86 -52
  160. package/src/context/strip-injections.ts +58 -10
  161. package/src/context/token-estimator.ts +1 -1
  162. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  163. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  164. package/src/daemon/conversation-agent-loop.ts +100 -19
  165. package/src/daemon/conversation-history.ts +1 -1
  166. package/src/daemon/conversation-lifecycle.ts +3 -5
  167. package/src/daemon/conversation-process.ts +13 -5
  168. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  169. package/src/daemon/conversation-surfaces.ts +26 -0
  170. package/src/daemon/conversation-tool-setup.ts +16 -11
  171. package/src/daemon/conversation.ts +64 -14
  172. package/src/daemon/disk-pressure-policy.ts +5 -3
  173. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  174. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  175. package/src/daemon/handlers/config-a2a.ts +0 -2
  176. package/src/daemon/handlers/config-channels.ts +5 -10
  177. package/src/daemon/handlers/config-slack-channel.ts +20 -0
  178. package/src/daemon/handlers/conversations.ts +107 -0
  179. package/src/daemon/host-browser-proxy.ts +41 -0
  180. package/src/daemon/lifecycle.ts +55 -20
  181. package/src/daemon/message-provenance.ts +2 -0
  182. package/src/daemon/message-types/contacts.ts +0 -1
  183. package/src/daemon/message-types/web-activity.ts +7 -1
  184. package/src/daemon/message-types/workflows.ts +83 -1
  185. package/src/daemon/tool-setup-types.ts +4 -0
  186. package/src/daemon/trust-context.ts +1 -1
  187. package/src/events/tool-audit-listener.ts +2 -2
  188. package/src/home/feed-source-enrichment.test.ts +151 -0
  189. package/src/home/feed-source-enrichment.ts +176 -0
  190. package/src/instrument.ts +18 -6
  191. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  192. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  193. package/src/ipc/assistant-server.ts +37 -4
  194. package/src/ipc/gateway-flag-listener.ts +18 -2
  195. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  196. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  197. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  198. package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
  199. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  200. package/src/memory/auth-fallback-events-store.ts +2 -2
  201. package/src/memory/auto-analysis-enqueue.ts +3 -5
  202. package/src/memory/canonical-guardian-store.ts +39 -1
  203. package/src/memory/conversation-crud.ts +9 -4
  204. package/src/memory/conversation-key-store.ts +17 -2
  205. package/src/memory/conversation-title-service.ts +64 -7
  206. package/src/memory/db-init.ts +10 -0
  207. package/src/memory/embedding-backend.ts +15 -1
  208. package/src/memory/jobs-worker.ts +2 -1
  209. package/src/memory/lifecycle-events-store.ts +2 -2
  210. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  211. package/src/memory/memory-retrospective-job.ts +9 -0
  212. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  213. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  214. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  215. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
  216. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  217. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  218. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  219. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  220. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  221. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  222. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  223. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  224. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  225. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
  226. package/src/memory/migrations/index.ts +5 -0
  227. package/src/memory/onboarding-events-store.ts +3 -3
  228. package/src/memory/schema/contacts.ts +0 -1
  229. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  230. package/src/memory/skill-loaded-events-store.ts +2 -2
  231. package/src/memory/tool-executed-events-store.test.ts +7 -7
  232. package/src/memory/turn-trace-store.test.ts +736 -0
  233. package/src/memory/turn-trace-store.ts +364 -0
  234. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  235. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  236. package/src/memory/v2/consolidation-job.ts +2 -2
  237. package/src/memory/v2/skill-content.ts +25 -7
  238. package/src/memory/v2/skill-store.ts +7 -1
  239. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  240. package/src/memory/v3-eval/eval-packets.ts +546 -0
  241. package/src/messaging/providers/slack/api.ts +31 -0
  242. package/src/messaging/providers/slack/send.test.ts +114 -2
  243. package/src/messaging/providers/slack/send.ts +30 -7
  244. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  245. package/src/messaging/providers/slack/withdraw.ts +161 -0
  246. package/src/notifications/AGENTS.md +2 -0
  247. package/src/notifications/access-request-copy.ts +72 -59
  248. package/src/notifications/adapters/slack.ts +55 -73
  249. package/src/notifications/approval-card-data.ts +333 -0
  250. package/src/notifications/broadcaster.ts +6 -2
  251. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  252. package/src/notifications/copy-composer.ts +3 -3
  253. package/src/notifications/decision-engine.ts +4 -2
  254. package/src/notifications/destination-resolver.ts +4 -6
  255. package/src/notifications/guardian-question-mode.ts +10 -0
  256. package/src/notifications/home-feed-side-effect.ts +3 -13
  257. package/src/notifications/notification-utils.ts +2 -1
  258. package/src/notifications/signal.ts +79 -43
  259. package/src/notifications/types.ts +98 -128
  260. package/src/permissions/checker.test.ts +51 -0
  261. package/src/permissions/checker.ts +185 -26
  262. package/src/permissions/ipc-risk-types.ts +24 -0
  263. package/src/permissions/question-prompter.test.ts +27 -0
  264. package/src/permissions/question-prompter.ts +4 -0
  265. package/src/platform/client.test.ts +119 -0
  266. package/src/platform/client.ts +66 -0
  267. package/src/platform/consent-cache.test.ts +267 -0
  268. package/src/platform/consent-cache.ts +174 -0
  269. package/src/plugin-api/index.ts +27 -0
  270. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  271. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  272. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  273. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  274. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  275. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  276. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  277. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  278. package/src/plugins/defaults/advisor/config.ts +21 -0
  279. package/src/plugins/defaults/advisor/consult.ts +93 -0
  280. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  281. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  282. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  283. package/src/plugins/defaults/advisor/package.json +14 -0
  284. package/src/plugins/defaults/advisor/steering.ts +67 -0
  285. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  286. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  287. package/src/plugins/defaults/index.ts +35 -0
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  289. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  290. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  291. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  292. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  298. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  299. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
  300. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  301. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  302. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  303. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
  304. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  305. package/src/prompts/persona-resolver.ts +12 -2
  306. package/src/prompts/templates/system-sections.ts +7 -2
  307. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  308. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  309. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  310. package/src/providers/atlascloud/client.ts +85 -0
  311. package/src/providers/fetch-provider-catalog.ts +85 -0
  312. package/src/providers/inference/adapter-factory.ts +3 -0
  313. package/src/providers/model-catalog.ts +58 -0
  314. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  315. package/src/providers/openai/chat-completions-provider.ts +7 -0
  316. package/src/providers/openai/responses-provider.ts +10 -0
  317. package/src/providers/provider-send-message.ts +11 -3
  318. package/src/providers/retry.ts +53 -12
  319. package/src/providers/search-provider-catalog.ts +10 -0
  320. package/src/providers/weak-open-model.ts +22 -0
  321. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  322. package/src/runtime/__tests__/client-health.test.ts +44 -0
  323. package/src/runtime/access-request-helper.ts +21 -53
  324. package/src/runtime/actor-trust-resolver.ts +49 -21
  325. package/src/runtime/agent-wake.ts +52 -0
  326. package/src/runtime/assistant-event-hub.ts +18 -4
  327. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  328. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  329. package/src/runtime/capabilities.test.ts +120 -0
  330. package/src/runtime/capabilities.ts +197 -0
  331. package/src/runtime/channel-approval-types.ts +5 -1
  332. package/src/runtime/channel-retry-sweep.ts +1 -0
  333. package/src/runtime/channel-verification-service.ts +1 -2
  334. package/src/runtime/client-health.ts +26 -0
  335. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  336. package/src/runtime/effective-capabilities.test.ts +128 -0
  337. package/src/runtime/effective-capabilities.ts +84 -0
  338. package/src/runtime/guardian-reply-router.ts +106 -21
  339. package/src/runtime/invite-redemption-service.ts +6 -22
  340. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  341. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  342. package/src/runtime/pending-interactions.ts +15 -0
  343. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  344. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  345. package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
  346. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  347. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  348. package/src/runtime/routes/client-routes.ts +10 -0
  349. package/src/runtime/routes/contact-routes.ts +31 -8
  350. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  351. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  352. package/src/runtime/routes/conversation-routes.ts +37 -12
  353. package/src/runtime/routes/events-routes.ts +1 -3
  354. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  355. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  356. package/src/runtime/routes/home-feed-routes.ts +8 -3
  357. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  358. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
  359. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  360. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  361. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  362. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  363. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  364. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  365. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  366. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  367. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  368. package/src/runtime/routes/index.ts +2 -0
  369. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  370. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  371. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  372. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  373. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  374. package/src/runtime/routes/notification-routes.ts +122 -133
  375. package/src/runtime/routes/platform-routes.ts +2 -2
  376. package/src/runtime/routes/plugins-routes.ts +40 -7
  377. package/src/runtime/routes/secret-routes.ts +10 -0
  378. package/src/runtime/routes/surface-action-routes.ts +2 -1
  379. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  380. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  381. package/src/runtime/routes/workflow-routes.test.ts +225 -1
  382. package/src/runtime/routes/workflow-routes.ts +131 -1
  383. package/src/runtime/tool-grant-request-helper.ts +18 -16
  384. package/src/runtime/trust-context-resolver.ts +8 -5
  385. package/src/schedule/schedule-store.ts +1 -1
  386. package/src/schedule/scheduler-types.ts +5 -1
  387. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  388. package/src/security/secret-patterns.ts +3 -0
  389. package/src/subagent/manager.ts +11 -4
  390. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  391. package/src/telemetry/trace-collection-policy.ts +30 -0
  392. package/src/telemetry/types.ts +89 -0
  393. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  394. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  395. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  396. package/src/tools/browser/browser-execution.ts +29 -18
  397. package/src/tools/document/document-tool.ts +2 -3
  398. package/src/tools/executor.ts +5 -3
  399. package/src/tools/host-terminal/host-shell.ts +5 -4
  400. package/src/tools/memory/register.ts +2 -2
  401. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  402. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  403. package/src/tools/network/web-fetch.ts +372 -1
  404. package/src/tools/network/web-search.ts +213 -10
  405. package/src/tools/permission-checker.ts +3 -2
  406. package/src/tools/registry.ts +20 -0
  407. package/src/tools/schedule/create.ts +4 -3
  408. package/src/tools/schedule/update.ts +2 -1
  409. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  410. package/src/tools/skills/execute.ts +1 -2
  411. package/src/tools/subagent/spawn.ts +37 -13
  412. package/src/tools/terminal/shell.ts +10 -4
  413. package/src/tools/tool-approval-handler.ts +17 -10
  414. package/src/tools/types.ts +9 -0
  415. package/src/tools/ui-surface/definitions.ts +25 -2
  416. package/src/tools/verification-control-plane-policy.ts +3 -1
  417. package/src/tools/workflows/run-workflow.ts +1 -0
  418. package/src/util/disk-usage.ts +78 -23
  419. package/src/util/platform.ts +8 -1
  420. package/src/watcher/telemetry.ts +2 -2
  421. package/src/workflows/engine.test.ts +175 -1
  422. package/src/workflows/engine.ts +82 -0
  423. package/src/workflows/journal-store.test.ts +70 -0
  424. package/src/workflows/journal-store.ts +18 -3
  425. package/src/workflows/run-manager.test.ts +171 -3
  426. package/src/workflows/run-manager.ts +64 -0
  427. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  428. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  429. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  430. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  431. package/src/workspace/migrations/registry.ts +8 -0
  432. package/src/notifications/tool-approval-copy.ts +0 -142
  433. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
@@ -18,9 +18,10 @@
18
18
  * Each lane only ever ADDS candidates, so the pool is recall-safe by
19
19
  * construction.
20
20
  * 2. Build the candidate pool in CACHE ORDER: the stable prefix —
21
- * `[...core (file order), ...hot (score order), ...fresh (recency
22
- * order)]`, all computed at lane init — followed by the finder
23
- * candidates (needle → dense → edge surfacing order). The stable prefix
21
+ * `[...core (file order), ...hot (score order), ...fresh (recency order),
22
+ * ...always-candidate (skills pinned every turn)]`, all computed at lane
23
+ * init — followed by the finder candidates (needle → dense → edge
24
+ * surfacing order). The stable prefix
24
25
  * is identical across consecutive turns while the lanes are unchanged
25
26
  * (lane invalidation at consolidation is the recompute cadence), so the
26
27
  * selector input's leading segment rides the provider KV cache (the
@@ -82,6 +83,10 @@ export interface OrchestrateDeps {
82
83
  /** The modification-recency fresh set in recency order (computed at lane
83
84
  * init with core and hot excluded). Follows hot in the stable prefix. */
84
85
  freshSlugs: Slug[];
86
+ /** Skills pinned into the candidate pool every turn regardless of retrieval
87
+ * (existence-filtered and core/hot/fresh-excluded at lane init). Follow fresh
88
+ * in the stable prefix so the selector always sees them. */
89
+ alwaysCandidateSlugs?: Slug[];
85
90
  /** Pre-rendered FULL cards for the stable-prefix slugs, keyed by slug.
86
91
  * Rendered ONCE at lane init so the selector's stable prefix is
87
92
  * byte-identical across turns (the cache contract). Every core/hot/fresh
@@ -172,14 +177,19 @@ export async function orchestrate(
172
177
  const { sections } = deps.sectionIndex;
173
178
 
174
179
  // The stable prefix: core (file order), hot (score order), fresh (recency
175
- // order). Hot is computed with core excluded at lane init, fresh with both
176
- // excluded; the filters here are a cheap defensive dedup so a misbehaving
177
- // lane can never double-list a slug.
180
+ // order), then always-candidate skills. Hot is computed with core excluded at
181
+ // lane init, fresh with both excluded, always-candidate with all three
182
+ // excluded; the filters here are a cheap defensive dedup so a misbehaving lane
183
+ // can never double-list a slug.
178
184
  const core = deps.coreSlugs;
179
185
  const hot = deps.hotSlugs.filter((slug) => !core.includes(slug));
180
186
  const coreHot = new Set<Slug>([...core, ...hot]);
181
187
  const fresh = deps.freshSlugs.filter((slug) => !coreHot.has(slug));
182
- const stablePrefix = new Set<Slug>([...coreHot, ...fresh]);
188
+ const coreHotFresh = new Set<Slug>([...coreHot, ...fresh]);
189
+ const always = (deps.alwaysCandidateSlugs ?? []).filter(
190
+ (slug) => !coreHotFresh.has(slug),
191
+ );
192
+ const stablePrefix = new Set<Slug>([...coreHotFresh, ...always]);
183
193
 
184
194
  // Step 1: needle (sync BM25) and dense (async embed + Qdrant) lanes run in
185
195
  // parallel. Both return distinct articles each tagged with their best-scoring
@@ -342,19 +352,21 @@ export async function orchestrate(
342
352
  // its own snippet line so its CURRENT relevance stays visible; `selectPool`
343
353
  // dedupes selections by slug. Finder candidates with no match text fall
344
354
  // back to the page's lead-section snippet.
345
- const stable: StableCandidate[] = [...core, ...hot, ...fresh].map((slug) => {
346
- const card = deps.prefixCards.get(slug);
347
- if (card === undefined) {
348
- // Lane init renders a card for every core/hot slug; a hole here means
349
- // the lanes and the card map are out of sync. Throw rather than render
350
- // a degraded card the caller (observeTurn) logs and skips the turn,
351
- // which is better than silently breaking the byte-stable prefix.
352
- throw new Error(
353
- `memory-v3: no pre-rendered card for stable-prefix slug "${slug}"`,
354
- );
355
- }
356
- return { slug, card };
357
- });
355
+ const stable: StableCandidate[] = [...core, ...hot, ...fresh, ...always].map(
356
+ (slug) => {
357
+ const card = deps.prefixCards.get(slug);
358
+ if (card === undefined) {
359
+ // Lane init renders a card for every core/hot slug; a hole here means
360
+ // the lanes and the card map are out of sync. Throw rather than render
361
+ // a degraded card the caller (observeTurn) logs and skips the turn,
362
+ // which is better than silently breaking the byte-stable prefix.
363
+ throw new Error(
364
+ `memory-v3: no pre-rendered card for stable-prefix slug "${slug}"`,
365
+ );
366
+ }
367
+ return { slug, card };
368
+ },
369
+ );
358
370
  const finderTail: PoolCandidate[] = finder.map((c) => ({
359
371
  slug: c.slug,
360
372
  lane: c.lane,
@@ -19,6 +19,7 @@
19
19
  import type { MemoryV3SelectionLog } from "../../../api/responses/memory-v3-selection-log.js";
20
20
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
21
21
  import { getConfig } from "../../../config/loader.js";
22
+ import { isMemoryV3Live } from "../../../config/memory-v3-gate.js";
22
23
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
23
24
  import { readPage } from "../../../memory/v2/page-store.js";
24
25
  import { getWorkspaceDir } from "../../../util/platform.js";
@@ -35,7 +36,6 @@ import {
35
36
  } from "./types.js";
36
37
 
37
38
  const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
38
- const MEMORY_V3_LIVE = "memory-v3-live" as const;
39
39
 
40
40
  interface SelectionRow {
41
41
  turn: number;
@@ -74,6 +74,52 @@ function rowsForMessageIds(messageIds: string[]): SelectionRow[] {
74
74
  .all(...messageIds) as SelectionRow[];
75
75
  }
76
76
 
77
+ const MAX_FORK_HOPS = 64;
78
+
79
+ /**
80
+ * Read the `forkSourceMessageId` back-pointer that `cloneForkMessageMetadata`
81
+ * stamps onto every message a fork copies, for the given message ids.
82
+ */
83
+ function forkSourceIdsOf(messageIds: string[]): string[] {
84
+ if (messageIds.length === 0) return [];
85
+ const placeholders = messageIds.map(() => "?").join(", ");
86
+ const rows = getSqliteFrom(getDb())
87
+ .query(
88
+ /*sql*/ `
89
+ SELECT json_extract(metadata, '$.forkSourceMessageId') AS src
90
+ FROM messages
91
+ WHERE id IN (${placeholders})
92
+ `,
93
+ )
94
+ .all(...messageIds) as Array<{ src: string | null }>;
95
+ return rows
96
+ .map((r) => r.src)
97
+ .filter((src): src is string => typeof src === "string" && src.length > 0);
98
+ }
99
+
100
+ /**
101
+ * A fork copies the parent's messages under fresh ids but does not copy their
102
+ * `memory_v3_selections` rows, so an inherited turn has no rows under its own
103
+ * message ids. Each copied message preserves a `forkSourceMessageId` pointer to
104
+ * the message it was cloned from; walk that chain (a fork of a fork chains it
105
+ * again) to the nearest ancestor generation that logged selections and return
106
+ * those rows. Returns `[]` when no ancestor has v3 rows (or the ids aren't fork
107
+ * copies).
108
+ */
109
+ function rowsViaForkSource(messageIds: string[]): SelectionRow[] {
110
+ let frontier = messageIds;
111
+ const visited = new Set(messageIds);
112
+ for (let hop = 0; hop < MAX_FORK_HOPS; hop++) {
113
+ const sources = forkSourceIdsOf(frontier).filter((id) => !visited.has(id));
114
+ if (sources.length === 0) return [];
115
+ for (const id of sources) visited.add(id);
116
+ const rows = rowsForMessageIds(sources);
117
+ if (rows.length > 0) return rows;
118
+ frontier = sources;
119
+ }
120
+ return [];
121
+ }
122
+
77
123
  /**
78
124
  * Resolve each selection's persisted matched section `(slug, ordinal)` to the
79
125
  * concrete `Section` in the CURRENT page, so the injected block renders the
@@ -133,7 +179,7 @@ async function buildSelectionLog(
133
179
 
134
180
  return {
135
181
  turn: rows[0]!.turn,
136
- live: isAssistantFeatureFlagEnabled(MEMORY_V3_LIVE, config),
182
+ live: isMemoryV3Live(config),
137
183
  shadow: isAssistantFeatureFlagEnabled(MEMORY_V3_SHADOW, config),
138
184
  selections,
139
185
  injectedText,
@@ -150,13 +196,20 @@ async function buildSelectionLog(
150
196
  * conversation with no v3 data). Message ids are globally unique, so no
151
197
  * conversation scope is needed.
152
198
  *
199
+ * For turns inherited from a fork, the copied messages carry fresh ids with no
200
+ * selection rows of their own, so the lookup falls back to the parent's rows by
201
+ * following each message's `forkSourceMessageId` back-pointer.
202
+ *
153
203
  * Selection rows are stored in selection order, so rendering them in row order
154
204
  * reproduces the block v3 would inject.
155
205
  */
156
206
  export async function getMemoryV3SelectionForInspectorByMessageIds(
157
207
  messageIds: string[],
158
208
  ): Promise<MemoryV3SelectionLog | null> {
159
- return buildSelectionLog(rowsForMessageIds(messageIds));
209
+ const rows = rowsForMessageIds(messageIds);
210
+ return buildSelectionLog(
211
+ rows.length > 0 ? rows : rowsViaForkSource(messageIds),
212
+ );
160
213
  }
161
214
 
162
215
  /**
@@ -34,6 +34,7 @@ import { existsSync, readFileSync } from "node:fs";
34
34
  import { isAssistantFeatureFlagEnabled } from "../../../config/assistant-feature-flags.js";
35
35
  import { getConfig } from "../../../config/loader.js";
36
36
  import type { AssistantConfig } from "../../../config/schema.js";
37
+ import { loadSkillCatalog } from "../../../config/skills.js";
37
38
  import { getMessages } from "../../../memory/conversation-crud.js";
38
39
  import { getDb, getSqliteFrom } from "../../../memory/db-connection.js";
39
40
  import { stringifyMessageContent } from "../../../memory/message-content.js";
@@ -68,7 +69,6 @@ import {
68
69
  } from "./types.js";
69
70
 
70
71
  export const MEMORY_V3_SHADOW = "memory-v3-shadow" as const;
71
- export const MEMORY_V3_LIVE = "memory-v3-live" as const;
72
72
 
73
73
  const log = getLogger("memory-v3-shadow");
74
74
 
@@ -107,6 +107,9 @@ export interface ShadowLanes {
107
107
  /** Modification-recency fresh set in recency order: core and hot excluded,
108
108
  * filtered to pages in the section index. */
109
109
  freshSlugs: string[];
110
+ /** Skills pinned into the stable prefix every turn (`always-candidate: true`
111
+ * in SKILL.md), existence-filtered and core/hot/fresh-excluded. */
112
+ alwaysCandidateSlugs: string[];
110
113
  /** Learned-edge graph: co-selection NPMI associations over the selection
111
114
  * log, rebuilt with the lanes (the consolidation cadence). */
112
115
  learnedGraph: EdgeGraph;
@@ -216,6 +219,18 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
216
219
  excludeSlugs: new Set([...coreSlugs, ...hotSlugs]),
217
220
  }).filter((slug) => sectionIndex.byArticle.has(slug));
218
221
 
222
+ // Always-candidate skills are pinned into the stable prefix every turn so the
223
+ // selector can choose a cross-cutting capability (e.g. workflows) even when no
224
+ // retrieval lane surfaces it — its relevance is a judgment the model makes,
225
+ // not something embedding similarity finds. Filtered to skills present in the
226
+ // section index and excluded from core/hot/fresh so the prefix never
227
+ // double-lists a slug.
228
+ const prefixSet = new Set([...coreSlugs, ...hotSlugs, ...freshSlugs]);
229
+ const alwaysCandidateSlugs = loadSkillCatalog()
230
+ .filter((summary) => summary.alwaysCandidate === true)
231
+ .map((summary) => `skills/${summary.id}`)
232
+ .filter((slug) => sectionIndex.byArticle.has(slug) && !prefixSet.has(slug));
233
+
219
234
  // Pre-render the stable-prefix cards ONCE per lane build: the selector's
220
235
  // stable prefix must be byte-identical across turns to ride the provider KV
221
236
  // cache, so the cards are frozen here (lane invalidation at consolidation is
@@ -228,7 +243,10 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
228
243
  const modifiedAtBySlug = new Map(
229
244
  pageIndex.entries.map((entry) => [entry.slug, entry.modifiedAt]),
230
245
  );
231
- const laneAnnotation = (slug: Slug, lane: "core" | "hot" | "fresh") => {
246
+ const laneAnnotation = (
247
+ slug: Slug,
248
+ lane: "core" | "hot" | "fresh" | "always",
249
+ ) => {
232
250
  if (lane !== "fresh") return `[lane: ${lane}]`;
233
251
  const modifiedAt = modifiedAtBySlug.get(slug);
234
252
  if (
@@ -249,6 +267,7 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
249
267
  ["core", coreSlugs],
250
268
  ["hot", hotSlugs],
251
269
  ["fresh", freshSlugs],
270
+ ["always", alwaysCandidateSlugs],
252
271
  ] as const) {
253
272
  for (const slug of slugs) {
254
273
  const raw = await capabilityOrDiskBody(
@@ -303,6 +322,7 @@ async function initLanes(config: AssistantConfig): Promise<ShadowLanes> {
303
322
  coreSlugs,
304
323
  hotSlugs,
305
324
  freshSlugs,
325
+ alwaysCandidateSlugs,
306
326
  prefixCards,
307
327
  };
308
328
  }
@@ -545,6 +565,7 @@ export async function observeTurn(
545
565
  coreSlugs: lanes.coreSlugs,
546
566
  hotSlugs: lanes.hotSlugs,
547
567
  freshSlugs: lanes.freshSlugs,
568
+ alwaysCandidateSlugs: lanes.alwaysCandidateSlugs,
548
569
  prefixCards: lanes.prefixCards,
549
570
  needleK: v3.needleK,
550
571
  denseK: v3.denseK,
@@ -48,6 +48,7 @@
48
48
  import type { PluginHookFn, PostToolUseContext } from "@vellumai/plugin-api";
49
49
 
50
50
  import type { ContentBlock, Message } from "../../../../providers/types.js";
51
+ import { isWeakOpenModel } from "../../../../providers/weak-open-model.js";
51
52
 
52
53
  /**
53
54
  * Canonical nudge notice. Module-level constant so tests and wrapping plugins
@@ -66,17 +67,6 @@ export const TASK_PROGRESS_NUDGE_TEXT =
66
67
  */
67
68
  export const TASK_PROGRESS_NUDGE_ROUND_THRESHOLD = 3;
68
69
 
69
- /**
70
- * Weaker open models that disregard the static progress-card instruction and
71
- * so get the mid-turn nudge: Kimi, DeepSeek, and MiniMax. Family-level matching
72
- * spans provider naming conventions (OpenRouter `moonshotai/kimi-k2.6`,
73
- * `deepseek/deepseek-chat`, `minimax/minimax-m3`; Fireworks
74
- * `accounts/fireworks/models/minimax-m3`, `kimi-k2p6`). Extend as other models
75
- * show the same gap. Capable models (Claude, GPT) follow the prompt and are
76
- * intentionally excluded.
77
- */
78
- const WEAK_MODEL_PATTERN = /kimi|deepseek|minimax/i;
79
-
80
70
  /**
81
71
  * Round count at the last nudge, per conversation. A non-zero entry means the
82
72
  * turn has already been nudged; it resets when the round count drops below the
@@ -145,7 +135,7 @@ function scanTurn(messages: ReadonlyArray<Message>): {
145
135
  }
146
136
 
147
137
  const postToolUse: PluginHookFn<PostToolUseContext> = async (ctx) => {
148
- if (!WEAK_MODEL_PATTERN.test(ctx.model)) return;
138
+ if (!isWeakOpenModel(ctx.model)) return;
149
139
 
150
140
  const { rounds, taskProgressShown } = scanTurn(ctx.messages);
151
141
 
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Default `stop` hook: triggers the second-pass conversation-title
3
- * regeneration once a conversation has accumulated enough context.
2
+ * Default `stop` hook: triggers conversation-title retry/regeneration once a
3
+ * conversation has accumulated useful context.
4
4
  *
5
5
  * The first title is generated from the opening prompt alone (see
6
6
  * `./user-prompt-submit.ts`). After a few exchanges the conversation's real
7
7
  * topic is usually clearer, so a single second pass re-titles using the most
8
- * recent messages. This hook is the trigger — it fires the regeneration when
9
- * the conversation reaches its third user turn and delegates the title
10
- * itself to the service (`memory/conversation-title-service.ts`), which
11
- * re-checks that the title is still auto-generated, resolves the title
12
- * provider, persists, and broadcasts the `conversation_title_updated` /
13
- * `sync_changed` events.
8
+ * recent messages. This hook is the trigger — it retries placeholder titles
9
+ * after successful turns and fires the second-pass regeneration when the
10
+ * conversation reaches its third user turn. The service
11
+ * (`memory/conversation-title-service.ts`) re-checks that the title is still
12
+ * auto-generated, resolves the title provider, persists, and broadcasts the
13
+ * `conversation_title_updated` / `sync_changed` events.
14
14
  *
15
15
  * Turn count is read from history rather than an external counter: the number
16
16
  * of genuine user prompts — user-role messages that aren't purely tool results
@@ -22,7 +22,11 @@ import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
22
22
 
23
23
  import { getConfig } from "../../../../config/loader.js";
24
24
  import { getConversation } from "../../../../memory/conversation-crud.js";
25
- import { queueRegenerateConversationTitle } from "../../../../memory/conversation-title-service.js";
25
+ import {
26
+ AUTO_TITLE_DETERMINISTIC,
27
+ isReplaceableTitle,
28
+ queueRegenerateConversationTitle,
29
+ } from "../../../../memory/conversation-title-service.js";
26
30
  import type { Message } from "../../../../providers/types.js";
27
31
 
28
32
  /**
@@ -50,37 +54,68 @@ function countUserTurns(messages: ReadonlyArray<Message>): number {
50
54
  return turns;
51
55
  }
52
56
 
57
+ function shouldRetryFallbackTitle(conversation: {
58
+ title: string | null;
59
+ isAutoTitle: number;
60
+ }): boolean {
61
+ if (!conversation.isAutoTitle) return false;
62
+ return (
63
+ isReplaceableTitle(conversation.title) ||
64
+ conversation.isAutoTitle === AUTO_TITLE_DETERMINISTIC
65
+ );
66
+ }
67
+
53
68
  const stop: PluginHookFn<StopContext> = async (ctx) => {
54
69
  // Re-title only at a genuine successful turn end (the model returned a reply
55
70
  // with no tool calls). Any other terminal — a provider rejection, abort, or
56
71
  // an output-limit cutoff — produced no new topic to re-title from.
57
72
  if (ctx.exitReason !== "no_tool_calls") return;
58
73
 
59
- if (getConfig().conversations.skipAutoRetitling) return;
60
-
61
- if (countUserTurns(ctx.messages) !== SECOND_PASS_USER_TURN) return;
74
+ const userTurns = countUserTurns(ctx.messages);
75
+ if (userTurns === 0) return;
62
76
 
63
77
  // System conversations (background/scheduled) keep their deterministic
64
78
  // bootstrap title — multi-prompt background jobs can reach three user-role
65
79
  // turns with no human present, and a refined LLM title isn't worth the
66
80
  // tokens there. The lookup fails open: on a read error the hook behaves as
67
81
  // before (queues regeneration; the service re-checks isAutoTitle).
82
+ let retryFallbackTitle = false;
68
83
  try {
69
84
  const conversation = getConversation(ctx.conversationId);
70
85
  if (conversation && conversation.conversationType !== "standard") return;
86
+ retryFallbackTitle = conversation
87
+ ? shouldRetryFallbackTitle(conversation)
88
+ : false;
71
89
  } catch {
72
90
  // Fall through to queueing.
73
91
  }
74
92
 
75
- const { conversationId } = ctx;
76
- // Deferred to a later macrotask so the just-completed turn's persistence
77
- // settles first. The service regenerates from the most recent stored
78
- // messages, so it must run after the reply is persisted to reflect it. The
79
- // service is itself fire-and-forget and re-checks replaceability, owning
80
- // provider resolution, persistence, and the resulting broadcast.
81
- setTimeout(() => {
82
- queueRegenerateConversationTitle({ conversationId });
83
- }, 0);
93
+ if (
94
+ userTurns === SECOND_PASS_USER_TURN &&
95
+ !getConfig().conversations.skipAutoRetitling
96
+ ) {
97
+ const { conversationId } = ctx;
98
+ // Deferred to a later macrotask so the just-completed turn's persistence
99
+ // settles first. The service regenerates from the most recent stored
100
+ // messages, so it must run after the reply is persisted to reflect it. The
101
+ // service is itself fire-and-forget and re-checks replaceability, owning
102
+ // provider resolution, persistence, and the resulting broadcast.
103
+ setTimeout(() => {
104
+ queueRegenerateConversationTitle({ conversationId });
105
+ }, 0);
106
+ return;
107
+ }
108
+
109
+ if (retryFallbackTitle) {
110
+ const { conversationId } = ctx;
111
+ setTimeout(() => {
112
+ queueRegenerateConversationTitle({
113
+ conversationId,
114
+ onlyIfReplaceable: true,
115
+ });
116
+ }, 0);
117
+ return;
118
+ }
84
119
  };
85
120
 
86
121
  export default stop;
@@ -96,13 +96,23 @@ function resolveUserFilename(
96
96
  filename = contactWithChannels.userFile ?? null;
97
97
  } else if (trustContext.trustClass === "guardian") {
98
98
  // Managed desktop: the JWT principal ID used as requesterExternalUserId
99
- // may differ from the contact channel's external_user_id (they are
100
- // separate identity concepts). Fall back to the channel-type guardian.
99
+ // may differ from the contact channel's address (they are separate
100
+ // identity concepts). Fall back to the channel-type guardian.
101
101
  const guardian = findGuardianForChannel(trustContext.sourceChannel);
102
102
  if (guardian) {
103
103
  filename = guardian.contact.userFile ?? "guardian.md";
104
104
  }
105
105
  }
106
+ } else if (trustContext.trustClass === "guardian") {
107
+ // Guardian-trust turn carrying no requester identity — background and
108
+ // scheduled turns (heartbeat, scheduled pulses) run under the guardian
109
+ // trust class but have no per-actor address to look up. Resolve the
110
+ // channel's guardian user file so they load the same persona as a
111
+ // foreground guardian turn instead of falling back to users/default.md.
112
+ const guardian = findGuardianForChannel(trustContext.sourceChannel);
113
+ if (guardian) {
114
+ filename = guardian.contact.userFile ?? "guardian.md";
115
+ }
106
116
  }
107
117
  } catch (err) {
108
118
  // Contacts table may be absent — happens during early bootstrap
@@ -259,6 +259,13 @@ Meet your user where they are. If they are nontechnical, prefer "Gmail needs rec
259
259
  Err toward brevity; expand only when the user follows up or their style calls for more.
260
260
 
261
261
  These are default guidelines. Always prioritize communication preferences that you've established through your relationship with your human.
262
+ `,
263
+ },
264
+ {
265
+ id: "01-delegate-subagents",
266
+ body: `## Delegate independent work
267
+
268
+ When part of a task can run on its own — a research sweep, a multi-file investigation, a build-and-test loop — hand it off instead of grinding through it inline: load the \`subagent\` skill, then \`subagent_spawn\` early and in parallel. Make delegating that kind of work your default, not a last resort; an unnecessary subagent is cheaper than serialized work, and a long inline dig floods your own context.
262
269
  `,
263
270
  },
264
271
  {
@@ -267,8 +274,6 @@ These are default guidelines. Always prioritize communication preferences that y
267
274
  Batch independent tool calls into the same response. An extra LLM round trip costs orders of magnitude more than a few wasted tool calls — err on the side of parallelizing when calls are independent. Reading multiple files, \`glob\`/\`grep\`, \`ls\`, \`git status\`/\`diff\`/\`log\`, type-checks, and tests should be batched.
268
275
 
269
276
  Before emitting a single tool call, ask whether your next turn would be another tool call that doesn't consume this one's output — if so, they belong together. Serialized tool calls without a real data dependency are a bug.
270
-
271
- For non-trivial independent workstreams — research, coding, multi-step investigations — delegate to subagents (load the \`subagent\` skill) and spawn them early and in parallel; an unnecessary subagent is cheaper than serialized work.
272
277
  </use_parallel_tool_calls>
273
278
  `,
274
279
  },
@@ -35,6 +35,7 @@ describe("getLlmProviderEnvVar", () => {
35
35
  expect(getLlmProviderEnvVar("brave")).toBeUndefined();
36
36
  expect(getLlmProviderEnvVar("perplexity")).toBeUndefined();
37
37
  expect(getLlmProviderEnvVar("tavily")).toBeUndefined();
38
+ expect(getLlmProviderEnvVar("firecrawl")).toBeUndefined();
38
39
  });
39
40
 
40
41
  test("returns undefined for unknown provider", () => {
@@ -55,6 +56,10 @@ describe("getSearchProviderEnvVar", () => {
55
56
  expect(getSearchProviderEnvVar("tavily")).toBe("TAVILY_API_KEY");
56
57
  });
57
58
 
59
+ test("returns FIRECRAWL_API_KEY for firecrawl", () => {
60
+ expect(getSearchProviderEnvVar("firecrawl")).toBe("FIRECRAWL_API_KEY");
61
+ });
62
+
58
63
  test("returns undefined for LLM providers (out of scope)", () => {
59
64
  expect(getSearchProviderEnvVar("anthropic")).toBeUndefined();
60
65
  expect(getSearchProviderEnvVar("openai")).toBeUndefined();
@@ -75,6 +80,7 @@ describe("getAnyProviderEnvVar", () => {
75
80
  expect(getAnyProviderEnvVar("brave")).toBe("BRAVE_API_KEY");
76
81
  expect(getAnyProviderEnvVar("perplexity")).toBe("PERPLEXITY_API_KEY");
77
82
  expect(getAnyProviderEnvVar("tavily")).toBe("TAVILY_API_KEY");
83
+ expect(getAnyProviderEnvVar("firecrawl")).toBe("FIRECRAWL_API_KEY");
78
84
  });
79
85
 
80
86
  test("returns undefined for ollama (keyless LLM provider)", () => {
@@ -44,5 +44,6 @@ describe("API_KEY_PROVIDERS", () => {
44
44
  expect(API_KEY_PROVIDERS).toContain("brave");
45
45
  expect(API_KEY_PROVIDERS).toContain("perplexity");
46
46
  expect(API_KEY_PROVIDERS).toContain("tavily");
47
+ expect(API_KEY_PROVIDERS).toContain("firecrawl");
47
48
  });
48
49
  });