@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
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Tests for guardian approval-by-reaction on the canonical pipeline.
3
+ *
4
+ * A Slack emoji reaction (✅ / ❌) on a delivered approval card is routed
5
+ * through `routeGuardianReply` exactly like a button press or text reply. The
6
+ * emoji maps to an action and the reacted card's message id (`reactedMessageTs`)
7
+ * resolves the target request via its canonical delivery record — so the
8
+ * decision is applied precisely even when several cards are pending in the same
9
+ * chat, with no clarification prompt.
10
+ */
11
+
12
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
13
+
14
+ mock.module("../util/logger.js", () => ({
15
+ getLogger: () =>
16
+ new Proxy({} as Record<string, unknown>, {
17
+ get: () => () => {},
18
+ }),
19
+ truncateForLog: (value: string) => value,
20
+ }));
21
+
22
+ const _conversationMocks = new Map<string, unknown>();
23
+ mock.module("../daemon/conversation-registry.js", () => ({
24
+ findConversation: (id: string) => _conversationMocks.get(id),
25
+ }));
26
+
27
+ import type { Conversation } from "../daemon/conversation.js";
28
+ import {
29
+ createCanonicalGuardianDelivery,
30
+ createCanonicalGuardianRequest,
31
+ getCanonicalGuardianRequest,
32
+ getPendingCanonicalRequestByDestinationMessage,
33
+ resolveCanonicalGuardianRequest,
34
+ } from "../memory/canonical-guardian-store.js";
35
+ import { getDb } from "../memory/db-connection.js";
36
+ import { initializeDb } from "../memory/db-init.js";
37
+ import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
38
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
39
+
40
+ initializeDb();
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Constants & helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const GUARDIAN_USER = "U_GUARDIAN";
47
+ const PRINCIPAL = "principal-1";
48
+ const CHAT_ID = "D_GUARDIAN_DM";
49
+ const TOOL_NAME = "execute_shell";
50
+
51
+ function resetTables(): void {
52
+ const db = getDb();
53
+ db.run("DELETE FROM canonical_guardian_deliveries");
54
+ db.run("DELETE FROM canonical_guardian_requests");
55
+ db.run("DELETE FROM scoped_approval_grants");
56
+ pendingInteractions.clear();
57
+ _conversationMocks.clear();
58
+ }
59
+
60
+ /**
61
+ * Seed a pending tool-approval (canonical request + delivery + pending
62
+ * interaction with a mock conversation) and return the mocked
63
+ * `handleConfirmationResponse` so callers can assert it was driven.
64
+ */
65
+ function seedApproval(opts: {
66
+ requestId: string;
67
+ ts: string;
68
+ chatId?: string;
69
+ principal?: string;
70
+ }): ReturnType<typeof mock> {
71
+ const chatId = opts.chatId ?? CHAT_ID;
72
+ const principal = opts.principal ?? PRINCIPAL;
73
+ const conversationId = `conv-${opts.requestId}`;
74
+
75
+ createCanonicalGuardianRequest({
76
+ id: opts.requestId,
77
+ kind: "tool_approval",
78
+ sourceType: "channel",
79
+ sourceChannel: "slack",
80
+ conversationId,
81
+ requesterExternalUserId: "requester-1",
82
+ requesterChatId: chatId,
83
+ guardianExternalUserId: GUARDIAN_USER,
84
+ guardianPrincipalId: principal,
85
+ toolName: TOOL_NAME,
86
+ status: "pending",
87
+ expiresAt: Date.now() + 300_000,
88
+ });
89
+
90
+ createCanonicalGuardianDelivery({
91
+ requestId: opts.requestId,
92
+ destinationChannel: "slack",
93
+ destinationChatId: chatId,
94
+ destinationMessageId: opts.ts,
95
+ });
96
+
97
+ const handleConfirmationResponse = mock(() => {});
98
+ _conversationMocks.set(conversationId, {
99
+ handleConfirmationResponse,
100
+ ensureActorScopedHistory: async () => {},
101
+ } as unknown as Conversation);
102
+ pendingInteractions.register(opts.requestId, {
103
+ conversationId,
104
+ kind: "confirmation",
105
+ confirmationDetails: {
106
+ toolName: TOOL_NAME,
107
+ input: { command: "echo hi" },
108
+ riskLevel: "high",
109
+ allowlistOptions: [{ label: "t", description: "t", pattern: "t" }],
110
+ scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
111
+ },
112
+ });
113
+ return handleConfirmationResponse;
114
+ }
115
+
116
+ function reaction(opts: {
117
+ emoji: string;
118
+ reactedMessageTs?: string;
119
+ chatId?: string;
120
+ principal?: string;
121
+ }): Parameters<typeof routeGuardianReply>[0] {
122
+ return {
123
+ messageText: "",
124
+ channel: "slack",
125
+ actor: {
126
+ actorPrincipalId: opts.principal ?? PRINCIPAL,
127
+ actorExternalUserId: GUARDIAN_USER,
128
+ channel: "slack",
129
+ guardianPrincipalId: opts.principal ?? PRINCIPAL,
130
+ },
131
+ conversationId: "guardian-conv",
132
+ callbackData: `reaction:${opts.emoji}`,
133
+ reactedMessageTs: opts.reactedMessageTs,
134
+ channelDeliveryContext: {
135
+ replyCallbackUrl: "https://gateway.test/deliver",
136
+ guardianChatId: opts.chatId ?? CHAT_ID,
137
+ assistantId: "self",
138
+ },
139
+ };
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Store lookup
144
+ // ---------------------------------------------------------------------------
145
+
146
+ describe("getPendingCanonicalRequestByDestinationMessage", () => {
147
+ beforeEach(resetTables);
148
+
149
+ test("resolves the request whose delivery matches channel + chat + message id", () => {
150
+ seedApproval({ requestId: "req-1", ts: "1700000000.000001" });
151
+ const found = getPendingCanonicalRequestByDestinationMessage(
152
+ "slack",
153
+ CHAT_ID,
154
+ "1700000000.000001",
155
+ );
156
+ expect(found?.id).toBe("req-1");
157
+ });
158
+
159
+ test("returns null when no delivery matches the message id", () => {
160
+ seedApproval({ requestId: "req-1", ts: "1700000000.000001" });
161
+ expect(
162
+ getPendingCanonicalRequestByDestinationMessage(
163
+ "slack",
164
+ CHAT_ID,
165
+ "9999.0",
166
+ ),
167
+ ).toBeNull();
168
+ });
169
+
170
+ test("returns null once the matched request is no longer pending", () => {
171
+ seedApproval({ requestId: "req-1", ts: "1700000000.000001" });
172
+ resolveCanonicalGuardianRequest("req-1", "pending", { status: "approved" });
173
+ expect(
174
+ getPendingCanonicalRequestByDestinationMessage(
175
+ "slack",
176
+ CHAT_ID,
177
+ "1700000000.000001",
178
+ ),
179
+ ).toBeNull();
180
+ });
181
+ });
182
+
183
+ // ---------------------------------------------------------------------------
184
+ // Reaction routing
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe("routeGuardianReply / reactions", () => {
188
+ beforeEach(resetTables);
189
+
190
+ test("✅ on a delivered card approves the matching request", async () => {
191
+ const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
192
+
193
+ const result = await routeGuardianReply(
194
+ reaction({ emoji: "white_check_mark", reactedMessageTs: "111.1" }),
195
+ );
196
+
197
+ expect(result.consumed).toBe(true);
198
+ expect(result.decisionApplied).toBe(true);
199
+ expect(result.type).toBe("canonical_decision_applied");
200
+ expect(getCanonicalGuardianRequest("req-1")?.status).toBe("approved");
201
+ expect(hcr).toHaveBeenCalledTimes(1);
202
+ expect(hcr.mock.calls[0]?.[0]).toBe("req-1");
203
+ expect(hcr.mock.calls[0]?.[1]).toBe("allow");
204
+ });
205
+
206
+ test("❌ on a delivered card rejects the matching request", async () => {
207
+ const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
208
+
209
+ const result = await routeGuardianReply(
210
+ reaction({ emoji: "-1", reactedMessageTs: "111.1" }),
211
+ );
212
+
213
+ expect(result.decisionApplied).toBe(true);
214
+ expect(getCanonicalGuardianRequest("req-1")?.status).toBe("denied");
215
+ expect(hcr.mock.calls[0]?.[1]).toBe("deny");
216
+ });
217
+
218
+ test("reacting on a specific card resolves only that card (disambiguates N>1)", async () => {
219
+ const hcrA = seedApproval({ requestId: "req-A", ts: "100.1" });
220
+ const hcrB = seedApproval({ requestId: "req-B", ts: "200.2" });
221
+
222
+ // React on card B's message — only B should resolve.
223
+ const result = await routeGuardianReply(
224
+ reaction({ emoji: "white_check_mark", reactedMessageTs: "200.2" }),
225
+ );
226
+
227
+ expect(result.requestId).toBe("req-B");
228
+ expect(getCanonicalGuardianRequest("req-B")?.status).toBe("approved");
229
+ expect(getCanonicalGuardianRequest("req-A")?.status).toBe("pending");
230
+ expect(hcrB).toHaveBeenCalledTimes(1);
231
+ expect(hcrA).not.toHaveBeenCalled();
232
+ });
233
+
234
+ test("a reaction on an unknown message is not consumed (left for transcript)", async () => {
235
+ seedApproval({ requestId: "req-1", ts: "111.1" });
236
+
237
+ const result = await routeGuardianReply(
238
+ reaction({ emoji: "white_check_mark", reactedMessageTs: "no-such-ts" }),
239
+ );
240
+
241
+ expect(result.consumed).toBe(false);
242
+ expect(result.type).toBe("not_consumed");
243
+ expect(getCanonicalGuardianRequest("req-1")?.status).toBe("pending");
244
+ });
245
+
246
+ test("an unknown emoji is not consumed", async () => {
247
+ seedApproval({ requestId: "req-1", ts: "111.1" });
248
+
249
+ const result = await routeGuardianReply(
250
+ reaction({ emoji: "tada", reactedMessageTs: "111.1" }),
251
+ );
252
+
253
+ expect(result.consumed).toBe(false);
254
+ expect(getCanonicalGuardianRequest("req-1")?.status).toBe("pending");
255
+ });
256
+
257
+ test("a reaction without a reacted message id is not consumed", async () => {
258
+ seedApproval({ requestId: "req-1", ts: "111.1" });
259
+
260
+ const result = await routeGuardianReply(
261
+ reaction({ emoji: "white_check_mark" }),
262
+ );
263
+
264
+ expect(result.consumed).toBe(false);
265
+ });
266
+
267
+ test("a reaction from a different principal is surfaced, not silently dropped", async () => {
268
+ const hcr = seedApproval({ requestId: "req-1", ts: "111.1" });
269
+
270
+ const result = await routeGuardianReply(
271
+ reaction({
272
+ emoji: "white_check_mark",
273
+ reactedMessageTs: "111.1",
274
+ principal: "someone-else",
275
+ }),
276
+ );
277
+
278
+ // Consumed with a user-facing reply, request untouched, side effect not run.
279
+ expect(result.consumed).toBe(true);
280
+ expect(result.decisionApplied).toBe(false);
281
+ expect(result.replyText).toMatch(/permission/i);
282
+ expect(getCanonicalGuardianRequest("req-1")?.status).toBe("pending");
283
+ expect(hcr).not.toHaveBeenCalled();
284
+ });
285
+ });
@@ -8,6 +8,7 @@ let mockGetMessages: (
8
8
  ) => Array<{ role: string; content: string }> | null = () => null;
9
9
  const mockProfiles = {
10
10
  balanced: {},
11
+ "cost-optimized": {},
11
12
  disabled: { status: "disabled" },
12
13
  "quality-optimized": {},
13
14
  };
@@ -476,6 +477,155 @@ describe("Subagent spawn success and failure", () => {
476
477
  }
477
478
  });
478
479
 
480
+ test("spawn inherits the invoking call site's default profile when no override is present", async () => {
481
+ const manager = getSubagentManager();
482
+ const originalSpawn = manager.spawn.bind(manager);
483
+ let capturedConfig: Record<string, unknown> | undefined;
484
+
485
+ manager.spawn = async (config: Record<string, unknown>) => {
486
+ capturedConfig = config;
487
+ return "inherit-default-id";
488
+ };
489
+
490
+ try {
491
+ const result = await executeSubagentSpawn(
492
+ { label: "Inherit default", objective: "Do it" },
493
+ makeContext("sess-inherit-default", {
494
+ sendToClient: () => {},
495
+ invokingCallSite: "mainAgent",
496
+ }),
497
+ );
498
+
499
+ expect(result.isError).toBe(false);
500
+ // No explicit profile and no per-turn override → the child matches the
501
+ // invoking call site's resolved default profile (balanced for mainAgent
502
+ // in the test config).
503
+ expect(capturedConfig!.overrideProfile).toBe("balanced");
504
+ expect(capturedConfig!.forceOverrideProfile).toBeUndefined();
505
+ } finally {
506
+ manager.spawn = originalSpawn;
507
+ }
508
+ });
509
+
510
+ test("spawn inherits a non-main invoker's call-site default profile", async () => {
511
+ const manager = getSubagentManager();
512
+ const originalSpawn = manager.spawn.bind(manager);
513
+ let capturedConfig: Record<string, unknown> | undefined;
514
+
515
+ manager.spawn = async (config: Record<string, unknown>) => {
516
+ capturedConfig = config;
517
+ return "inherit-heartbeat-id";
518
+ };
519
+
520
+ try {
521
+ const result = await executeSubagentSpawn(
522
+ { label: "Heartbeat child", objective: "Do it" },
523
+ makeContext("sess-inherit-heartbeat", {
524
+ sendToClient: () => {},
525
+ invokingCallSite: "heartbeatAgent",
526
+ }),
527
+ );
528
+
529
+ expect(result.isError).toBe(false);
530
+ // A subagent spawned from a heartbeat turn matches heartbeatAgent's own
531
+ // cost-optimized default, not the mainAgent default.
532
+ expect(capturedConfig!.overrideProfile).toBe("cost-optimized");
533
+ } finally {
534
+ manager.spawn = originalSpawn;
535
+ }
536
+ });
537
+
538
+ test("spawn prefers a per-turn override profile over the invoker default", async () => {
539
+ const manager = getSubagentManager();
540
+ const originalSpawn = manager.spawn.bind(manager);
541
+ let capturedConfig: Record<string, unknown> | undefined;
542
+
543
+ manager.spawn = async (config: Record<string, unknown>) => {
544
+ capturedConfig = config;
545
+ return "inherit-override-id";
546
+ };
547
+
548
+ try {
549
+ const result = await executeSubagentSpawn(
550
+ { label: "Override child", objective: "Do it" },
551
+ makeContext("sess-inherit-override", {
552
+ sendToClient: () => {},
553
+ invokingCallSite: "mainAgent",
554
+ overrideProfile: "quality-optimized",
555
+ }),
556
+ );
557
+
558
+ expect(result.isError).toBe(false);
559
+ // The live per-turn override (per-conversation or tool-routed) wins over
560
+ // the call-site default, and is forwarded non-forced.
561
+ expect(capturedConfig!.overrideProfile).toBe("quality-optimized");
562
+ expect(capturedConfig!.forceOverrideProfile).toBeUndefined();
563
+ } finally {
564
+ manager.spawn = originalSpawn;
565
+ }
566
+ });
567
+
568
+ test("spawn skips the auto profile so the child keeps its own default", async () => {
569
+ const manager = getSubagentManager();
570
+ const originalSpawn = manager.spawn.bind(manager);
571
+ let capturedConfig: Record<string, unknown> | undefined;
572
+
573
+ manager.spawn = async (config: Record<string, unknown>) => {
574
+ capturedConfig = config;
575
+ return "inherit-auto-id";
576
+ };
577
+
578
+ try {
579
+ const result = await executeSubagentSpawn(
580
+ { label: "Auto child", objective: "Do it" },
581
+ makeContext("sess-inherit-auto", {
582
+ sendToClient: () => {},
583
+ invokingCallSite: "mainAgent",
584
+ // "auto" is metadata-only; forwarding it would collapse the child to
585
+ // llm.default, so the inherited path drops it and the child keeps its
586
+ // own subagentSpawn default.
587
+ overrideProfile: "auto",
588
+ }),
589
+ );
590
+
591
+ expect(result.isError).toBe(false);
592
+ expect(capturedConfig!.overrideProfile).toBeUndefined();
593
+ } finally {
594
+ manager.spawn = originalSpawn;
595
+ }
596
+ });
597
+
598
+ test("spawn still forces an explicit inference_profile over the invoker default", async () => {
599
+ const manager = getSubagentManager();
600
+ const originalSpawn = manager.spawn.bind(manager);
601
+ let capturedConfig: Record<string, unknown> | undefined;
602
+
603
+ manager.spawn = async (config: Record<string, unknown>) => {
604
+ capturedConfig = config;
605
+ return "inherit-explicit-id";
606
+ };
607
+
608
+ try {
609
+ const result = await executeSubagentSpawn(
610
+ {
611
+ label: "Explicit child",
612
+ objective: "Do it",
613
+ inference_profile: "cost-optimized",
614
+ },
615
+ makeContext("sess-inherit-explicit", {
616
+ sendToClient: () => {},
617
+ invokingCallSite: "mainAgent",
618
+ }),
619
+ );
620
+
621
+ expect(result.isError).toBe(false);
622
+ expect(capturedConfig!.overrideProfile).toBe("cost-optimized");
623
+ expect(capturedConfig!.forceOverrideProfile).toBe(true);
624
+ } finally {
625
+ manager.spawn = originalSpawn;
626
+ }
627
+ });
628
+
479
629
  test("spawn returns error for unknown inference_profile", async () => {
480
630
  const manager = getSubagentManager();
481
631
  const originalSpawn = manager.spawn.bind(manager);
@@ -104,7 +104,7 @@ function currentResponse(toolUseId: string): ToolResultContent {
104
104
  };
105
105
  }
106
106
 
107
- /** A weak-model id that matches WEAK_MODEL_PATTERN (the gated population). */
107
+ /** A weak-model id that matches WEAK_OPEN_MODEL_PATTERN (the gated population). */
108
108
  const WEAK_MODEL = "minimax/minimax-m3";
109
109
 
110
110
  function makeCtx(
@@ -7,8 +7,8 @@
7
7
  * - `user-prompt-submit` — first-pass generation from the submitted prompt,
8
8
  * scheduled on a later macrotask so the main agent-loop LLM request is
9
9
  * issued first.
10
- * - `stop` — second-pass regeneration once the conversation reaches its third
11
- * user turn (turn count derived from the user prompts in history).
10
+ * - `stop` — fallback retry for replaceable titles and second-pass
11
+ * regeneration once the conversation reaches its third user turn.
12
12
  *
13
13
  * Both let the title service resolve the provider, persist the title, and
14
14
  * broadcast the resulting `conversation_title_updated` / `sync_changed`
@@ -31,13 +31,42 @@ const queueGenerateConversationTitleMock = mock(
31
31
  }): void => undefined,
32
32
  );
33
33
  const queueRegenerateConversationTitleMock = mock(
34
- (_params: { conversationId: string; provider?: unknown }): void => undefined,
34
+ (_params: {
35
+ conversationId: string;
36
+ provider?: unknown;
37
+ onlyIfReplaceable?: boolean;
38
+ }): void => undefined,
35
39
  );
36
40
  mock.module("../memory/conversation-title-service.js", () => ({
41
+ AUTO_TITLE_DETERMINISTIC: 2,
42
+ isReplaceableTitle: (title: string | null) =>
43
+ title == null ||
44
+ title === "" ||
45
+ title === "Generating title..." ||
46
+ title === "New Conversation" ||
47
+ title === "Untitled" ||
48
+ title === "Untitled Conversation" ||
49
+ title.startsWith("Runtime: "),
37
50
  queueGenerateConversationTitle: queueGenerateConversationTitleMock,
38
51
  queueRegenerateConversationTitle: queueRegenerateConversationTitleMock,
39
52
  }));
40
53
 
54
+ const mockGetConversation = mock(
55
+ (_conversationId: string) =>
56
+ ({
57
+ title: "Existing Title",
58
+ isAutoTitle: 1,
59
+ conversationType: "standard",
60
+ }) as {
61
+ title: string;
62
+ isAutoTitle: number;
63
+ conversationType: string;
64
+ },
65
+ );
66
+ mock.module("../memory/conversation-crud.js", () => ({
67
+ getConversation: mockGetConversation,
68
+ }));
69
+
41
70
  // The `stop` hook reads `conversations.skipAutoRetitling`; stub the loader so
42
71
  // the opt-out is controllable per test.
43
72
  let skipAutoRetitling = false;
@@ -192,6 +221,19 @@ describe("title-generate stop hook", () => {
192
221
  resetPluginRegistryForTests();
193
222
  queueRegenerateConversationTitleMock.mockReset();
194
223
  queueRegenerateConversationTitleMock.mockImplementation(() => undefined);
224
+ mockGetConversation.mockReset();
225
+ mockGetConversation.mockImplementation(
226
+ (_conversationId: string) =>
227
+ ({
228
+ title: "Existing Title",
229
+ isAutoTitle: 1,
230
+ conversationType: "standard",
231
+ }) as {
232
+ title: string;
233
+ isAutoTitle: number;
234
+ conversationType: string;
235
+ },
236
+ );
195
237
  skipAutoRetitling = false;
196
238
  });
197
239
 
@@ -210,6 +252,61 @@ describe("title-generate stop hook", () => {
210
252
  expect(call?.conversationId).toBe("conv-1");
211
253
  expect(call).not.toHaveProperty("provider");
212
254
  expect(call).not.toHaveProperty("signal");
255
+ expect(call).not.toHaveProperty("onlyIfReplaceable");
256
+ });
257
+
258
+ test("retries a replaceable fallback title after a successful turn", async () => {
259
+ mockGetConversation.mockReturnValueOnce({
260
+ title: "Untitled Conversation",
261
+ isAutoTitle: 2,
262
+ conversationType: "standard",
263
+ });
264
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
265
+
266
+ await stop(ctx);
267
+ await flushMacrotasks();
268
+
269
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
270
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
271
+ conversationId: "conv-1",
272
+ onlyIfReplaceable: true,
273
+ });
274
+ });
275
+
276
+ test("preserves the third-turn retitle when the title is still replaceable", async () => {
277
+ mockGetConversation.mockReturnValueOnce({
278
+ title: "Untitled Conversation",
279
+ isAutoTitle: 2,
280
+ conversationType: "standard",
281
+ });
282
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(3) });
283
+
284
+ await stop(ctx);
285
+ await flushMacrotasks();
286
+
287
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
288
+ const call = queueRegenerateConversationTitleMock.mock.calls[0]?.[0];
289
+ expect(call?.conversationId).toBe("conv-1");
290
+ expect(call).not.toHaveProperty("onlyIfReplaceable");
291
+ });
292
+
293
+ test("fallback title retry is not blocked by the retitling opt-out", async () => {
294
+ skipAutoRetitling = true;
295
+ mockGetConversation.mockReturnValueOnce({
296
+ title: "Generating title...",
297
+ isAutoTitle: 1,
298
+ conversationType: "standard",
299
+ });
300
+ const ctx = makeStopCtx({ messages: historyWithUserTurns(1) });
301
+
302
+ await stop(ctx);
303
+ await flushMacrotasks();
304
+
305
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledTimes(1);
306
+ expect(queueRegenerateConversationTitleMock).toHaveBeenCalledWith({
307
+ conversationId: "conv-1",
308
+ onlyIfReplaceable: true,
309
+ });
213
310
  });
214
311
 
215
312
  test("defers the regeneration so the completed turn is persisted first", async () => {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { buildToolApprovalSeedContentBlocks } from "../notifications/tool-approval-copy.js";
3
+ import { buildToolApprovalSeedContentBlocks } from "../notifications/approval-card-data.js";
4
4
 
5
5
  describe("buildToolApprovalSeedContentBlocks", () => {
6
6
  const toolApprovalPayload: Record<string, unknown> = {
@@ -1,11 +1,11 @@
1
1
  import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
- // Toggle for the collectUsageData opt-out gate the listener consults when
3
+ // Toggle for the share_analytics opt-out gate the listener consults when
4
4
  // populating the telemetry columns.
5
- let collectUsageData = true;
5
+ let shareAnalytics = true;
6
6
 
7
- mock.module("../config/loader.js", () => ({
8
- getConfig: () => ({ collectUsageData }),
7
+ mock.module("../platform/consent-cache.js", () => ({
8
+ getCachedShareAnalytics: () => shareAnalytics,
9
9
  }));
10
10
 
11
11
  import { createToolAuditListener } from "../events/tool-audit-listener.js";
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  describe("tool audit listener", () => {
19
19
  beforeEach(() => {
20
- collectUsageData = true;
20
+ shareAnalytics = true;
21
21
  });
22
22
 
23
23
  test("records executed events with truncated output", () => {
@@ -361,8 +361,8 @@ describe("tool audit listener", () => {
361
361
  );
362
362
  });
363
363
 
364
- test("persists NULL telemetry columns when usage data collection is opted out", () => {
365
- collectUsageData = false;
364
+ test("persists NULL telemetry columns when share_analytics is opted out", () => {
365
+ shareAnalytics = false;
366
366
  const records: ToolInvocationRecord[] = [];
367
367
  const listener = createToolAuditListener((record) => records.push(record));
368
368
 
@@ -34,9 +34,6 @@ const mockConfig = {
34
34
  permissions: {
35
35
  mode: "workspace" as const,
36
36
  },
37
- // The audit listener nulls the telemetry columns when this is false; the
38
- // end-to-end listener tests below assert the opted-in sizing behavior.
39
- collectUsageData: true,
40
37
  };
41
38
 
42
39
  let checkerDecision: "allow" | "prompt" | "deny" = "allow";
@@ -56,6 +53,12 @@ mock.module("../config/loader.js", () => ({
56
53
  setNestedValue: () => {},
57
54
  }));
58
55
 
56
+ // Analytics consent is granted so the audit listener populates the telemetry
57
+ // columns; the end-to-end listener tests below assert the opted-in sizing.
58
+ mock.module("../platform/consent-cache.js", () => ({
59
+ getCachedShareAnalytics: () => true,
60
+ }));
61
+
59
62
  mock.module("../util/logger.js", () => ({
60
63
  getLogger: () =>
61
64
  new Proxy({} as Record<string, unknown>, {
@@ -169,8 +169,10 @@ async function simulateNotifierPoll(params: {
169
169
  notifiedRequestIds,
170
170
  } = params;
171
171
 
172
- // Gate check: only trusted contacts with guardian route
173
- if (trustClass !== "trusted_contact" || !guardianExternalUserId) {
172
+ // Gate check: only identity-known non-guardian contacts with guardian route
173
+ const isIdentityKnownNonGuardian =
174
+ trustClass === "trusted_contact" || trustClass === "unverified_contact";
175
+ if (!isIdentityKnownNonGuardian || !guardianExternalUserId) {
174
176
  return false;
175
177
  }
176
178