@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
@@ -26,6 +26,8 @@ mock.module("../../assistant-event-hub.js", () => ({
26
26
  broadcastMessage: () => {},
27
27
  }));
28
28
 
29
+ import { eq } from "drizzle-orm";
30
+
29
31
  import { getDb } from "../../../memory/db-connection.js";
30
32
  import { initializeDb } from "../../../memory/db-init.js";
31
33
  import { conversations } from "../../../memory/schema.js";
@@ -71,6 +73,10 @@ function findHandler(routes: RouteDefinition[], operationId: string) {
71
73
  return route.handler;
72
74
  }
73
75
 
76
+ const createHandler = findHandler(
77
+ CONVERSATION_MANAGEMENT_ROUTES,
78
+ "createConversation",
79
+ );
74
80
  const putHandler = findHandler(
75
81
  CONVERSATION_MANAGEMENT_ROUTES,
76
82
  "setConversationInferenceProfile",
@@ -112,6 +118,67 @@ function seedConversation(id: string): void {
112
118
  // Tests
113
119
  // ---------------------------------------------------------------------------
114
120
 
121
+ describe("POST /v1/conversations (createConversation)", () => {
122
+ beforeEach(() => {
123
+ clearConversations();
124
+ });
125
+
126
+ function readConversation(id: string) {
127
+ return getDb()
128
+ .select({
129
+ title: conversations.title,
130
+ isAutoTitle: conversations.isAutoTitle,
131
+ })
132
+ .from(conversations)
133
+ .where(eq(conversations.id, id))
134
+ .get();
135
+ }
136
+
137
+ test("with a title → persists it as a user-set title (isAutoTitle = 0)", async () => {
138
+ const result = (await createHandler({
139
+ body: { conversationType: "standard", title: "Setting up your check-in" },
140
+ })) as { id: string; created: boolean };
141
+
142
+ expect(result.created).toBe(true);
143
+ const row = readConversation(result.id);
144
+ expect(row?.title).toBe("Setting up your check-in");
145
+ // isAutoTitle = 0 keeps the async LLM titler from overwriting it.
146
+ expect(row?.isAutoTitle).toBe(0);
147
+ });
148
+
149
+ test("blank title falls back to the replaceable 'New Conversation' placeholder", async () => {
150
+ const result = (await createHandler({
151
+ body: { conversationType: "standard", title: " " },
152
+ })) as { id: string; created: boolean };
153
+
154
+ expect(result.created).toBe(true);
155
+ const row = readConversation(result.id);
156
+ expect(row?.title).toBe("New Conversation");
157
+ // Default auto-title flag (1) leaves it replaceable by the auto-titler.
158
+ expect(row?.isAutoTitle).toBe(1);
159
+ });
160
+
161
+ test("no title → 'New Conversation' placeholder", async () => {
162
+ const result = (await createHandler({
163
+ body: { conversationType: "standard" },
164
+ })) as { id: string; created: boolean };
165
+
166
+ expect(result.created).toBe(true);
167
+ const row = readConversation(result.id);
168
+ expect(row?.title).toBe("New Conversation");
169
+ expect(row?.isAutoTitle).toBe(1);
170
+ });
171
+
172
+ test("non-string title → BadRequestError (not a 500), no row created", () => {
173
+ // The shared route adapter doesn't runtime-validate the body, so the
174
+ // handler must reject a malformed title before `.trim()` throws.
175
+ expect(() =>
176
+ createHandler({ body: { conversationType: "standard", title: 123 } }),
177
+ ).toThrow(/title must be a string/);
178
+ expect(getDb().select().from(conversations).all()).toHaveLength(0);
179
+ });
180
+ });
181
+
115
182
  describe("PUT /v1/conversations/:id/inference-profile", () => {
116
183
  beforeEach(() => {
117
184
  clearConversations();
@@ -75,7 +75,6 @@ import {
75
75
  PluginMergeBaselineError,
76
76
  PluginNotUpgradableError,
77
77
  type PluginUpgradeResult,
78
- PluginUpgradeStrategyUnsupportedError,
79
78
  type UpgradePluginDeps,
80
79
  type UpgradePluginOptions,
81
80
  } from "../../../cli/lib/upgrade-plugin.js";
@@ -183,7 +182,6 @@ const upgradeSpy = mock(
183
182
  mock.module("../../../cli/lib/upgrade-plugin.js", () => ({
184
183
  PluginMergeBaselineError,
185
184
  PluginNotUpgradableError,
186
- PluginUpgradeStrategyUnsupportedError,
187
185
  upgradePlugin: upgradeSpy,
188
186
  }));
189
187
 
@@ -1004,6 +1002,10 @@ function inspection(
1004
1002
  }
1005
1003
  : overrides.remote,
1006
1004
  remoteError: overrides.remoteError ?? null,
1005
+ surfaces:
1006
+ overrides.surfaces === undefined
1007
+ ? { skills: [], hooks: ["post-model-call"], tools: [] }
1008
+ : overrides.surfaces,
1007
1009
  };
1008
1010
  }
1009
1011
 
@@ -1109,6 +1111,8 @@ function upgradeResult(
1109
1111
  fileCount: overrides.fileCount === undefined ? 12 : overrides.fileCount,
1110
1112
  dryRun: overrides.dryRun ?? false,
1111
1113
  strategy: overrides.strategy ?? "overwrite",
1114
+ conflicts: overrides.conflicts ?? [],
1115
+ binaryConflicts: overrides.binaryConflicts ?? [],
1112
1116
  provenanceWasUnknown: overrides.provenanceWasUnknown ?? false,
1113
1117
  };
1114
1118
  }
@@ -1124,6 +1128,8 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1124
1128
  fileCount: number | null;
1125
1129
  dryRun: boolean;
1126
1130
  strategy: string;
1131
+ conflicts: readonly string[];
1132
+ binaryConflicts: readonly string[];
1127
1133
  provenanceWasUnknown: boolean;
1128
1134
  }> {
1129
1135
  return (await upgradeHandler(args)) as {
@@ -1137,6 +1143,8 @@ async function invokeUpgrade(args: RouteHandlerArgs = {}): Promise<{
1137
1143
  fileCount: number | null;
1138
1144
  dryRun: boolean;
1139
1145
  strategy: string;
1146
+ conflicts: readonly string[];
1147
+ binaryConflicts: readonly string[];
1140
1148
  provenanceWasUnknown: boolean;
1141
1149
  };
1142
1150
  }
@@ -1168,6 +1176,8 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1168
1176
  fileCount: 12,
1169
1177
  dryRun: false,
1170
1178
  strategy: "overwrite",
1179
+ conflicts: [],
1180
+ binaryConflicts: [],
1171
1181
  provenanceWasUnknown: false,
1172
1182
  });
1173
1183
  // AND the name + dryRun are forwarded to the lib (strategy omitted)
@@ -1199,19 +1209,31 @@ describe("POST /v1/plugins/:name/upgrade", () => {
1199
1209
  });
1200
1210
  });
1201
1211
 
1202
- test("PluginUpgradeStrategyUnsupportedError \u2192 BadRequestError (400)", async () => {
1203
- // The `assistant` strategy is recognized but not yet implemented: a
1204
- // well-formed request the server cannot honor.
1205
- upgradeSpy.mockImplementation(async () => {
1206
- throw new PluginUpgradeStrategyUnsupportedError("assistant");
1212
+ test("projects conflicts + binaryConflicts from the assistant strategy onto the wire", async () => {
1213
+ // GIVEN upgradePlugin merges with conflict markers under `assistant`
1214
+ upgradeSpy.mockImplementation(async () =>
1215
+ upgradeResult({
1216
+ strategy: "assistant",
1217
+ conflicts: ["hooks/post-model-call.ts"],
1218
+ binaryConflicts: ["assets/icon.png"],
1219
+ }),
1220
+ );
1221
+
1222
+ // WHEN the handler runs with the `assistant` strategy
1223
+ const result = await invokeUpgrade({
1224
+ pathParams: { name: "level-up" },
1225
+ body: { strategy: "assistant" },
1207
1226
  });
1208
1227
 
1209
- await expect(
1210
- invokeUpgrade({
1211
- pathParams: { name: "level-up" },
1212
- body: { strategy: "assistant" },
1213
- }),
1214
- ).rejects.toBeInstanceOf(BadRequestError);
1228
+ // THEN the conflicted paths are surfaced for the assistant to resolve
1229
+ expect(result.strategy).toBe("assistant");
1230
+ expect(result.conflicts).toEqual(["hooks/post-model-call.ts"]);
1231
+ expect(result.binaryConflicts).toEqual(["assets/icon.png"]);
1232
+ expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
1233
+ name: "level-up",
1234
+ dryRun: undefined,
1235
+ strategy: "assistant",
1236
+ });
1215
1237
  });
1216
1238
 
1217
1239
  test("PluginMergeBaselineError \u2192 ConflictError (409)", async () => {
@@ -8,6 +8,7 @@
8
8
  import { z } from "zod";
9
9
 
10
10
  import { findConversation } from "../../daemon/conversation-registry.js";
11
+ import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
11
12
  import { getCdpClient } from "../../tools/browser/cdp-client/factory.js";
12
13
  import {
13
14
  clearPinnedTab,
@@ -51,6 +52,14 @@ async function handleBrowserTabs({ body = {} }: RouteHandlerArgs) {
51
52
 
52
53
  const cdpOptions = { mode: "extension" as const, targetClientId };
53
54
 
55
+ // Every tabs command pins extension mode. Absorb a brief extension SSE
56
+ // reconnect blip so a flapping connection doesn't surface as a hard
57
+ // "no Chrome Extension connected" error.
58
+ await HostBrowserProxy.instance.waitForExtensionClient(
59
+ context.sourceActorPrincipalId,
60
+ targetClientId,
61
+ );
62
+
54
63
  if (command === "list") {
55
64
  const cdp = getCdpClient(context, cdpOptions);
56
65
  try {
@@ -18,6 +18,7 @@
18
18
  * clients polling prompts) observe the expired status directly.
19
19
  */
20
20
 
21
+ import { withdrawGuardianRequestCards } from "../../approvals/guardian-card-withdrawal.js";
21
22
  import {
22
23
  listCanonicalGuardianRequests,
23
24
  resolveCanonicalGuardianRequest,
@@ -42,7 +43,7 @@ let sweepInProgress = false;
42
43
  * concurrent decision that wins the race is never overwritten by the
43
44
  * sweep. Returns the count of requests transitioned to expired.
44
45
  */
45
- function sweepExpiredCanonicalGuardianRequests(): number {
46
+ async function sweepExpiredCanonicalGuardianRequests(): Promise<number> {
46
47
  const pending = listCanonicalGuardianRequests({ status: "pending" });
47
48
  const now = Date.now();
48
49
  let expiredCount = 0;
@@ -71,6 +72,14 @@ function sweepExpiredCanonicalGuardianRequests(): number {
71
72
  },
72
73
  "Expired canonical guardian request via sweep",
73
74
  );
75
+
76
+ // Withdraw the now-stale approval cards on every surface. No origin
77
+ // channel — the expiry is system-driven, so all surfaces (including
78
+ // in-app) are withdrawn. Best-effort and non-throwing.
79
+ await withdrawGuardianRequestCards({
80
+ request: resolved,
81
+ status: "expired",
82
+ });
74
83
  }
75
84
  }
76
85
 
@@ -93,13 +102,13 @@ export function startCanonicalGuardianExpirySweep(): void {
93
102
  sweepTimer = setInterval(() => {
94
103
  if (sweepInProgress) return;
95
104
  sweepInProgress = true;
96
- try {
97
- sweepExpiredCanonicalGuardianRequests();
98
- } catch (err) {
99
- log.error({ err }, "Canonical guardian expiry sweep failed");
100
- } finally {
101
- sweepInProgress = false;
102
- }
105
+ void sweepExpiredCanonicalGuardianRequests()
106
+ .catch((err) => {
107
+ log.error({ err }, "Canonical guardian expiry sweep failed");
108
+ })
109
+ .finally(() => {
110
+ sweepInProgress = false;
111
+ });
103
112
  }, SWEEP_INTERVAL_MS);
104
113
  }
105
114
 
@@ -12,6 +12,10 @@ import { isHttpAuthDisabled } from "../../config/env.js";
12
12
  import { datesToISO } from "../../util/json.js";
13
13
  import { assistantEventHub } from "../assistant-event-hub.js";
14
14
  import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
15
+ import {
16
+ DEFAULT_HEARTBEAT_INTERVAL_MS,
17
+ isClientDegraded,
18
+ } from "../client-health.js";
15
19
  import { NotFoundError } from "./errors.js";
16
20
  import type { RouteDefinition } from "./types.js";
17
21
 
@@ -65,6 +69,7 @@ export const ROUTES: RouteDefinition[] = [
65
69
  c.actorPrincipalId === callerPrincipalId,
66
70
  );
67
71
 
72
+ const now = new Date();
68
73
  return {
69
74
  clients: filtered.map((c) =>
70
75
  datesToISO({
@@ -74,6 +79,11 @@ export const ROUTES: RouteDefinition[] = [
74
79
  machineName: c.machineName,
75
80
  connectedAt: c.connectedAt,
76
81
  lastActiveAt: c.lastActiveAt,
82
+ degraded: isClientDegraded(
83
+ c.lastActiveAt,
84
+ now,
85
+ DEFAULT_HEARTBEAT_INTERVAL_MS,
86
+ ),
77
87
  }),
78
88
  ),
79
89
  };
@@ -50,6 +50,30 @@ function withGuardianNameOverride<
50
50
  return contact;
51
51
  }
52
52
 
53
+ /** Adds `externalUserId` (= `address`) to each channel for older macOS clients. */
54
+ function withChannelCompat<T extends { channels: { address: string }[] }>(
55
+ contact: T,
56
+ ): T {
57
+ return {
58
+ ...contact,
59
+ channels: contact.channels.map((ch) => ({
60
+ ...ch,
61
+ externalUserId: ch.address,
62
+ })),
63
+ };
64
+ }
65
+
66
+ /** Compose both response transforms (guardian display name + channel compat). */
67
+ function prepareContactResponse<
68
+ T extends {
69
+ role: string;
70
+ displayName: string;
71
+ channels: { address: string }[];
72
+ },
73
+ >(contact: T): T {
74
+ return withChannelCompat(withGuardianNameOverride(contact));
75
+ }
76
+
53
77
  const VALID_CONTACT_TYPES: readonly ContactType[] = ["human", "assistant"];
54
78
 
55
79
  const VALID_CHANNEL_STATUSES: readonly ChannelStatus[] = [
@@ -87,6 +111,7 @@ const contactChannelSchema = z.object({
87
111
  type: z.string(),
88
112
  address: z.string(),
89
113
  isPrimary: z.boolean(),
114
+ /** @deprecated Echoes `address` for backwards compatibility with older macOS clients. */
90
115
  externalUserId: z.string().nullable(),
91
116
  status: z.string(),
92
117
  policy: z.string(),
@@ -143,14 +168,14 @@ function handleListContacts(queryParams: Record<string, string>) {
143
168
  });
144
169
  return {
145
170
  ok: true,
146
- contacts: contacts.map(withGuardianNameOverride),
171
+ contacts: contacts.map(prepareContactResponse),
147
172
  };
148
173
  }
149
174
 
150
175
  const contacts = listContacts(limit, role, contactType);
151
176
  return {
152
177
  ok: true,
153
- contacts: contacts.map(withGuardianNameOverride),
178
+ contacts: contacts.map(prepareContactResponse),
154
179
  };
155
180
  }
156
181
 
@@ -165,7 +190,7 @@ function handleGetContact(contactId: string) {
165
190
  : undefined;
166
191
  return {
167
192
  ok: true,
168
- contact: withGuardianNameOverride(contact),
193
+ contact: prepareContactResponse(contact),
169
194
  assistantMetadata: assistantMeta ?? undefined,
170
195
  };
171
196
  }
@@ -508,7 +533,7 @@ export const ROUTES: RouteDefinition[] = [
508
533
  limit: z.number().optional(),
509
534
  })
510
535
  .parse(body);
511
- return searchContacts(parsed);
536
+ return searchContacts(parsed).map(prepareContactResponse);
512
537
  },
513
538
  },
514
539
 
@@ -604,7 +629,7 @@ function handleMergeContactsRoute(args: RouteHandlerArgs) {
604
629
 
605
630
  try {
606
631
  const contact = mergeContacts(keepId, mergeId);
607
- return { ok: true, contact: withGuardianNameOverride(contact) };
632
+ return { ok: true, contact: prepareContactResponse(contact) };
608
633
  } catch (err) {
609
634
  const message = err instanceof Error ? err.message : String(err);
610
635
  throw new BadRequestError(message);
@@ -667,8 +692,6 @@ function handleUpdateContactChannelRoute(args: RouteHandlerArgs) {
667
692
  const parentContact = getContact(updated.contactId);
668
693
  return {
669
694
  ok: true,
670
- contact: parentContact
671
- ? withGuardianNameOverride(parentContact)
672
- : undefined,
695
+ contact: parentContact ? prepareContactResponse(parentContact) : undefined,
673
696
  };
674
697
  }
@@ -26,6 +26,7 @@ import {
26
26
  cancelGeneration,
27
27
  clearAllConversations,
28
28
  regenerateResponse,
29
+ resolveMetaSlashCommand,
29
30
  switchConversation,
30
31
  undoLastMessage,
31
32
  } from "../../daemon/handlers/conversations.js";
@@ -93,11 +94,26 @@ function cancelScheduleIfLast(conversationId: string): void {
93
94
  function handleCreateConversation({ body = {}, headers }: RouteHandlerArgs) {
94
95
  const conversationKey =
95
96
  (body.conversationKey as string | undefined) ?? crypto.randomUUID();
97
+ // The shared route adapter does not runtime-validate the body against the
98
+ // Zod requestBody (it's codegen-only), so guard the type before trimming —
99
+ // a malformed `{ title: 123 }` would otherwise throw on `.trim()` and 500.
100
+ if (body.title !== undefined && typeof body.title !== "string") {
101
+ throw new BadRequestError("title must be a string");
102
+ }
103
+ const customTitle = body.title?.trim() || undefined;
96
104
  const result = getOrCreateConversation(conversationKey, {
97
105
  conversationType: "standard",
98
106
  });
99
107
  if (result.created) {
100
- updateConversationTitle(result.conversationId, "New Conversation");
108
+ // A caller-supplied title is user-set: persist it with isAutoTitle = 0 so
109
+ // the async LLM titler's safe-overwrite check leaves it untouched. Without
110
+ // one, fall back to the neutral "New Conversation" placeholder, which stays
111
+ // replaceable by the auto-titler once messages arrive.
112
+ if (customTitle) {
113
+ updateConversationTitle(result.conversationId, customTitle, 0);
114
+ } else {
115
+ updateConversationTitle(result.conversationId, "New Conversation");
116
+ }
101
117
  publishConversationListAndMetadataChanged(
102
118
  "created",
103
119
  result.conversationId,
@@ -452,6 +468,29 @@ async function handleUndoLastMessage({ pathParams = {} }: RouteHandlerArgs) {
452
468
  };
453
469
  }
454
470
 
471
+ async function handleResolveMetaSlashCommand({
472
+ pathParams = {},
473
+ body = {},
474
+ }: RouteHandlerArgs) {
475
+ const command = body.command as string | undefined;
476
+ if (!command || typeof command !== "string") {
477
+ throw new BadRequestError("Missing command");
478
+ }
479
+ let result: Awaited<ReturnType<typeof resolveMetaSlashCommand>>;
480
+ try {
481
+ result = await resolveMetaSlashCommand(pathParams.id!, command);
482
+ } catch (err) {
483
+ if (err instanceof UserError) {
484
+ throw new BadRequestError(err.message);
485
+ }
486
+ throw err;
487
+ }
488
+ if (!result) {
489
+ throw new NotFoundError(`No conversation for ${pathParams.id}`);
490
+ }
491
+ return result;
492
+ }
493
+
455
494
  async function handleRegenerateResponse({ pathParams = {} }: RouteHandlerArgs) {
456
495
  const conversationId = pathParams.id!;
457
496
  try {
@@ -526,6 +565,12 @@ export const ROUTES: RouteDefinition[] = [
526
565
  .literal("standard")
527
566
  .optional()
528
567
  .describe("Only standard conversations are created by this endpoint"),
568
+ title: z
569
+ .string()
570
+ .optional()
571
+ .describe(
572
+ "Explicit title for the conversation. When provided on creation, it is persisted as a user-set title (never overwritten by the auto-titler). Used by flows that mint a conversation up-front and don't want an auto-generated title.",
573
+ ),
529
574
  }),
530
575
  responseBody: z.object({
531
576
  id: z
@@ -820,6 +865,40 @@ export const ROUTES: RouteDefinition[] = [
820
865
  }),
821
866
  handler: handleUndoLastMessage,
822
867
  },
868
+ {
869
+ operationId: "resolveConversationSlashCommand",
870
+ endpoint: "conversations/:id/slash",
871
+ method: "POST",
872
+ policy: {
873
+ requiredScopes: ["chat.write"],
874
+ allowedPrincipalTypes: ACTOR_PRINCIPALS,
875
+ },
876
+ summary: "Resolve a local meta slash command",
877
+ description:
878
+ "Run a local meta slash command (/clean, /status, /commands, /models) " +
879
+ "without starting a turn: no messages are persisted and no turn events " +
880
+ "are emitted. /clean also strips runtime injections from the history. " +
881
+ "Returns the text to render and, for /clean, the post-strip context usage.",
882
+ tags: ["conversations"],
883
+ pathParams: [{ name: "id", type: "uuid" }],
884
+ requestBody: z.object({
885
+ command: z
886
+ .string()
887
+ .describe("The slash command text, e.g. `/clean` or `/status`."),
888
+ }),
889
+ responseBody: z.object({
890
+ kind: z.enum(["clean", "info"]),
891
+ text: z.string(),
892
+ contextUsage: z
893
+ .object({
894
+ tokens: z.number(),
895
+ maxTokens: z.number().nullable(),
896
+ fillRatio: z.number().nullable(),
897
+ })
898
+ .optional(),
899
+ }),
900
+ handler: handleResolveMetaSlashCommand,
901
+ },
823
902
  {
824
903
  operationId: "regenerateResponse",
825
904
  endpoint: "conversations/:id/regenerate",
@@ -129,9 +129,16 @@ const INFERENCE_PROFILE_UI_KEYS = new Set([
129
129
  "speed",
130
130
  "verbosity",
131
131
  "temperature",
132
+ "topP",
132
133
  "thinking",
133
134
  ]);
134
135
 
136
+ // Fields a MANAGED profile may edit. Beyond `label` (display name) and
137
+ // `status` (enabled/disabled), users can tune `topP` — the seed contract
138
+ // owns provider/model/connection, but top_p is a per-profile sampling knob
139
+ // the UI exposes on the managed Balanced profile.
140
+ const MANAGED_PROFILE_EDITABLE_KEYS = new Set(["label", "status", "topP"]);
141
+
135
142
  function asMutablePlainObject(value: unknown): Record<string, unknown> | null {
136
143
  if (value == null || typeof value !== "object" || Array.isArray(value)) {
137
144
  return null;
@@ -508,6 +515,8 @@ const ConfigGetResponseSchema = z
508
515
  profiles: z.record(z.string(), WireProfileEntry).optional(),
509
516
  profileOrder: z.array(z.string()).optional(),
510
517
  activeProfile: z.string().optional(),
518
+ // The profile the advisor consults; excluded from chat-profile pickers.
519
+ advisorProfile: z.string().optional(),
511
520
  callSites: z
512
521
  .record(
513
522
  z.string(),
@@ -536,6 +545,13 @@ const ConfigGetResponseSchema = z
536
545
  })
537
546
  .passthrough()
538
547
  .optional(),
548
+ "web-fetch": z
549
+ .object({
550
+ mode: ServiceModeSchema.optional(),
551
+ provider: z.string().optional(),
552
+ })
553
+ .passthrough()
554
+ .optional(),
539
555
  "image-generation": z
540
556
  .object({ mode: ServiceModeSchema.optional() })
541
557
  .passthrough()
@@ -609,6 +625,7 @@ const ConfigPatchRequestSchema = z
609
625
  .optional(),
610
626
  profileOrder: z.array(z.string()).optional(),
611
627
  activeProfile: z.string().nullable().optional(),
628
+ advisorProfile: z.string().nullable().optional(),
612
629
  callSites: z
613
630
  .record(z.string(), CallSiteOverrideDraftSchema.nullable())
614
631
  .optional(),
@@ -633,6 +650,14 @@ const ConfigPatchRequestSchema = z
633
650
  .passthrough()
634
651
  .nullable()
635
652
  .optional(),
653
+ "web-fetch": z
654
+ .object({
655
+ mode: ServiceModeSchema.optional(),
656
+ provider: z.string().optional(),
657
+ })
658
+ .passthrough()
659
+ .nullable()
660
+ .optional(),
636
661
  "image-generation": z
637
662
  .object({ mode: ServiceModeSchema.optional() })
638
663
  .passthrough()
@@ -730,8 +755,14 @@ function rejectManagedProfileDeletion(body: Record<string, unknown>): void {
730
755
  }
731
756
  const profiles = asMutablePlainObject(llm.profiles);
732
757
  if (!profiles) return;
758
+ const existingProfiles = asMutablePlainObject(getConfig().llm.profiles) ?? {};
733
759
  for (const name of Object.keys(profiles)) {
734
- if (profiles[name] === null && MANAGED_PROFILE_NAMES.has(name)) {
760
+ if (profiles[name] !== null || !MANAGED_PROFILE_NAMES.has(name)) continue;
761
+ // Only block deletion when the on-disk entry is Vellum-managed. A
762
+ // user-owned profile sharing a managed name carries a non-managed `source`
763
+ // and is freely deletable.
764
+ const existing = asMutablePlainObject(existingProfiles[name]);
765
+ if (existing?.source === "managed") {
735
766
  throw new BadRequestError(`Cannot delete managed profile "${name}".`);
736
767
  }
737
768
  }
@@ -904,21 +935,39 @@ async function handleReplaceInferenceProfile({
904
935
  const detail = parsed.error.issues.map((issue) => issue.message).join("; ");
905
936
  throw new BadRequestError(`Invalid profile fragment: ${detail}`);
906
937
  }
907
- const isManaged = MANAGED_PROFILE_NAMES.has(name);
938
+ // A managed name with no existing entry stays protected so users can't
939
+ // shadow a seeded managed profile. An existing entry carrying a non-managed
940
+ // `source` is user-owned and remains fully editable.
941
+ const existingProfile = asMutablePlainObject(
942
+ getConfig().llm.profiles?.[name],
943
+ );
944
+ const isManaged =
945
+ MANAGED_PROFILE_NAMES.has(name) &&
946
+ (existingProfile == null || existingProfile.source === "managed");
947
+ // A managed profile name with no materialized entry (e.g. a flag-gated profile
948
+ // whose flag is off) cannot be patched: writing label/status here would persist
949
+ // a source-less stub that later blocks the real managed profile from being
950
+ // seeded. Reject rather than create a placeholder.
951
+ if (MANAGED_PROFILE_NAMES.has(name) && existingProfile == null) {
952
+ throw new BadRequestError(
953
+ `Profile "${name}" is not currently available and cannot be edited.`,
954
+ );
955
+ }
908
956
  if (isManaged) {
909
- // Managed profiles are daemon-seeded — provider, model, advanced params,
910
- // and the connection binding all belong to the seed contract and can't
911
- // be reshaped by the user. The two fields that ARE user policy (display
912
- // label and enabled status) are allowed through so users can rename a
913
- // managed profile or temporarily disable it without duplicating it.
957
+ // Managed profiles are daemon-seeded — provider, model, and the
958
+ // connection binding all belong to the seed contract and can't be
959
+ // reshaped by the user. The fields that ARE user policy (display label,
960
+ // enabled status, and the topP sampling knob) are allowed through so
961
+ // users can rename a managed profile, temporarily disable it, or tune
962
+ // top_p without duplicating it.
914
963
  const requestedKeys = Object.keys(parsed.data);
915
964
  const disallowed = requestedKeys.filter(
916
- (k) => k !== "label" && k !== "status",
965
+ (k) => !MANAGED_PROFILE_EDITABLE_KEYS.has(k),
917
966
  );
918
967
  if (disallowed.length > 0) {
919
968
  throw new BadRequestError(
920
969
  `Cannot edit managed profile "${name}" fields [${disallowed.join(", ")}]. ` +
921
- `Only label and status may be edited; duplicate to a custom profile to change other fields.`,
970
+ `Only label, status, and topP may be edited; duplicate to a custom profile to change other fields.`,
922
971
  );
923
972
  }
924
973
  }
@@ -1019,7 +1068,7 @@ async function handleReplaceInferenceProfile({
1019
1068
  }
1020
1069
 
1021
1070
  /**
1022
- * Apply a `{label?, status?}` patch to a managed profile entry, preserving
1071
+ * Apply a `{label?, status?, topP?}` patch to a managed profile entry, preserving
1023
1072
  * every other field already on disk (provider, model, advanced params, etc).
1024
1073
  * Caller is responsible for having already restricted the fragment to the
1025
1074
  * managed-allowed keys.
@@ -1039,19 +1088,16 @@ function patchManagedProfileFields(
1039
1088
 
1040
1089
  const existingProfile = asMutablePlainObject(profiles[name]) ?? {};
1041
1090
  const nextProfile: Record<string, unknown> = { ...existingProfile };
1042
- // Send `null` to clear; omit to leave untouched.
1043
- if ("label" in fragment) {
1044
- if (fragment.label === null) {
1045
- delete nextProfile.label;
1046
- } else {
1047
- nextProfile.label = fragment.label;
1048
- }
1049
- }
1050
- if ("status" in fragment) {
1051
- if (fragment.status === null) {
1052
- delete nextProfile.status;
1091
+ // For each managed-editable key: send `null` to clear, a value to set,
1092
+ // omit to leave untouched. Iterating the allowlist keeps persistence in
1093
+ // lock-step with the guard above — a key can't slip through the gate
1094
+ // without also being written.
1095
+ for (const key of MANAGED_PROFILE_EDITABLE_KEYS) {
1096
+ if (!(key in fragment)) continue;
1097
+ if (fragment[key] === null) {
1098
+ delete nextProfile[key];
1053
1099
  } else {
1054
- nextProfile.status = fragment.status;
1100
+ nextProfile[key] = fragment[key];
1055
1101
  }
1056
1102
  }
1057
1103
  profiles[name] = nextProfile;