@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
@@ -37,10 +37,8 @@ import type {
37
37
  ProviderResponse,
38
38
  ToolDefinition,
39
39
  } from "../providers/types.js";
40
- import {
41
- isUntrustedTrustClass,
42
- type TrustClass,
43
- } from "../runtime/actor-trust-resolver.js";
40
+ import { type TrustClass } from "../runtime/actor-trust-resolver.js";
41
+ import { resolveCapabilities } from "../runtime/capabilities.js";
44
42
  import { getLogger } from "../util/logger.js";
45
43
  import { stripInjectionsForCompaction } from "./strip-injections.js";
46
44
  import {
@@ -387,7 +385,7 @@ export function collectImageManifest(
387
385
  actorTrustClass?: TrustClass,
388
386
  ): ManifestEntry[] {
389
387
  const allRows = getMessages(conversationId);
390
- const rows = isUntrustedTrustClass(actorTrustClass)
388
+ const rows = !resolveCapabilities(actorTrustClass).canAccessMemory
391
389
  ? filterMessagesForUntrustedActor(allRows)
392
390
  : allRows;
393
391
  const entries: ManifestEntry[] = [];
@@ -847,6 +845,69 @@ function buildCompactionRequest(
847
845
  return [...stripHistoricalWebSearchResults(history).messages, instruction];
848
846
  }
849
847
 
848
+ // Token headroom a compaction summary call reserves on top of its history: room
849
+ // for the instruction message and the summary the model emits, so a request
850
+ // front-truncated to `compactionPrefixBudget` still fits the context window.
851
+ const COMPACTION_INSTRUCTION_TOKEN_RESERVE = 800;
852
+ const COMPACTION_OUTPUT_BUDGET_RATIO = 0.15;
853
+
854
+ // Largest history (in estimated tokens) a compaction summary call may carry
855
+ // while leaving room for the instruction and the emitted summary within
856
+ // `maxInputTokens`.
857
+ function compactionPrefixBudget(maxInputTokens: number): number {
858
+ return (
859
+ maxInputTokens -
860
+ COMPACTION_INSTRUCTION_TOKEN_RESERVE -
861
+ Math.floor(maxInputTokens * COMPACTION_OUTPUT_BUDGET_RATIO)
862
+ );
863
+ }
864
+
865
+ // Front-truncate the history handed to a compaction summary call so the call
866
+ // itself fits the context window. Drops messages from the front until the
867
+ // estimated prompt fits `budgetTokens`, then prepends a marker noting how many
868
+ // were dropped so the model knows the summary covers only the visible portion.
869
+ // Returns the input untouched when it already fits or holds a single message —
870
+ // so a below-budget call keeps its prefix byte-aligned with the agent's warm
871
+ // cache and pays no extra cache write.
872
+ function truncateHistoryToBudget(args: {
873
+ messages: Message[];
874
+ systemPrompt: string;
875
+ budgetTokens: number;
876
+ providerName: string;
877
+ }): Message[] {
878
+ const { messages, systemPrompt, budgetTokens, providerName } = args;
879
+ let estimate = estimatePromptTokens(messages, systemPrompt, { providerName });
880
+ if (estimate <= budgetTokens || messages.length <= 1) {
881
+ return messages;
882
+ }
883
+ let dropCount = 0;
884
+ while (estimate > budgetTokens && dropCount < messages.length - 1) {
885
+ dropCount++;
886
+ estimate = estimatePromptTokens(messages.slice(dropCount), systemPrompt, {
887
+ providerName,
888
+ });
889
+ }
890
+ if (dropCount === 0) {
891
+ return messages;
892
+ }
893
+ log.info(
894
+ { dropCount, budgetTokens, totalMessages: messages.length },
895
+ "Compaction summary input exceeds context window — truncating from front",
896
+ );
897
+ return [
898
+ {
899
+ role: "user" as const,
900
+ content: [
901
+ {
902
+ type: "text" as const,
903
+ text: `[${dropCount} earlier messages truncated — summary covers only the visible portion]`,
904
+ },
905
+ ],
906
+ },
907
+ ...messages.slice(dropCount),
908
+ ];
909
+ }
910
+
850
911
  export async function runAssistantDrivenCompaction(
851
912
  args: CompactionRunArgs,
852
913
  ): Promise<CompactionRunResult> {
@@ -881,7 +942,19 @@ export async function runAssistantDrivenCompaction(
881
942
  args.targetTokens,
882
943
  );
883
944
 
884
- const requestMessages = buildCompactionRequest(args.messages, instruction);
945
+ // Bound the summary call's own input to the context window. With no tool
946
+ // pair to anchor an emergency split, an overflow recovery routes the full
947
+ // history straight here, so the summary call must front-truncate itself or
948
+ // it overflows in turn. `args.messages` stays intact for tail resolution
949
+ // below — only the outbound request is truncated. A below-budget history is
950
+ // returned untouched, keeping the prefix aligned with the agent's warm cache.
951
+ const summaryHistory = truncateHistoryToBudget({
952
+ messages: args.messages,
953
+ systemPrompt: args.systemPrompt,
954
+ budgetTokens: compactionPrefixBudget(args.maxInputTokens),
955
+ providerName: args.provider.tokenEstimationProvider ?? args.provider.name,
956
+ });
957
+ const requestMessages = buildCompactionRequest(summaryHistory, instruction);
885
958
 
886
959
  let response: ProviderResponse;
887
960
  try {
@@ -1296,55 +1369,16 @@ export async function runEmergencyCompaction(
1296
1369
  const keptTail = stripInjectionsForCompaction(
1297
1370
  args.messages.slice(splitIndex),
1298
1371
  );
1299
- let prefix = args.messages.slice(0, splitIndex);
1300
-
1301
- // If the prefix itself exceeds the context window, truncate messages
1302
- // from the front so the model can at least see the recent portion.
1303
- // Reserve budget for the instruction message + output.
1304
- const instructionBudget = 800; // ~tokens for the emergency prompt
1305
- const outputBudget = Math.floor(args.maxInputTokens * 0.15);
1306
- const prefixBudget = args.maxInputTokens - instructionBudget - outputBudget;
1307
-
1308
- let prefixEstimate = estimatePromptTokens(prefix, args.systemPrompt, {
1372
+ // Bound the prefix to the context window so the summary call fits, reserving
1373
+ // budget for the instruction message and the emitted summary. Truncates from
1374
+ // the front, keeping the recent portion the summary most needs.
1375
+ const prefix = truncateHistoryToBudget({
1376
+ messages: args.messages.slice(0, splitIndex),
1377
+ systemPrompt: args.systemPrompt,
1378
+ budgetTokens: compactionPrefixBudget(args.maxInputTokens),
1309
1379
  providerName: args.provider.tokenEstimationProvider ?? args.provider.name,
1310
1380
  });
1311
1381
 
1312
- if (prefixEstimate > prefixBudget && prefix.length > 1) {
1313
- log.info(
1314
- {
1315
- prefixEstimate,
1316
- prefixBudget,
1317
- prefixMessages: prefix.length,
1318
- },
1319
- "Emergency compaction: prefix exceeds context window — truncating from front",
1320
- );
1321
- // Drop messages from the front until we fit. Keep at least the first
1322
- // message (may be an existing summary) and try to preserve recent context.
1323
- let dropCount = 0;
1324
- while (prefixEstimate > prefixBudget && dropCount < prefix.length - 1) {
1325
- dropCount++;
1326
- const truncated = prefix.slice(dropCount);
1327
- prefixEstimate = estimatePromptTokens(truncated, args.systemPrompt, {
1328
- providerName:
1329
- args.provider.tokenEstimationProvider ?? args.provider.name,
1330
- });
1331
- }
1332
- if (dropCount > 0) {
1333
- prefix = [
1334
- {
1335
- role: "user" as const,
1336
- content: [
1337
- {
1338
- type: "text" as const,
1339
- text: `[${dropCount} earlier messages truncated — summary covers only the visible portion]`,
1340
- },
1341
- ],
1342
- },
1343
- ...prefix.slice(dropCount),
1344
- ];
1345
- }
1346
- }
1347
-
1348
1382
  const instruction: Message = {
1349
1383
  role: "user",
1350
1384
  content: [{ type: "text", text: EMERGENCY_COMPACTION_PROMPT }],
@@ -28,6 +28,33 @@ import type { Message } from "../providers/types.js";
28
28
  */
29
29
  export type InjectionMatcher = string | { prefix: string; suffix: string };
30
30
 
31
+ /**
32
+ * Whether an injected text block matches any of the given matchers. A plain
33
+ * string matches by prefix; a `{ prefix, suffix }` wrapper requires both ends
34
+ * so user-authored text merely opening with an injection-like tag is kept.
35
+ */
36
+ function textBlockMatchesInjection(
37
+ text: string,
38
+ matchers: InjectionMatcher[],
39
+ ): boolean {
40
+ return matchers.some((m) =>
41
+ typeof m === "string"
42
+ ? text.startsWith(m)
43
+ : text.startsWith(m.prefix) && text.endsWith(m.suffix),
44
+ );
45
+ }
46
+
47
+ /** Drop the matching injection text blocks from a single content array. */
48
+ function filterInjectionBlocks(
49
+ content: Message["content"],
50
+ matchers: InjectionMatcher[],
51
+ ): Message["content"] {
52
+ return content.filter(
53
+ (block) =>
54
+ block.type !== "text" || !textBlockMatchesInjection(block.text, matchers),
55
+ );
56
+ }
57
+
31
58
  /**
32
59
  * Remove text blocks from user messages that match any of the given matchers.
33
60
  * If stripping removes all content blocks from a message, the message itself
@@ -43,14 +70,7 @@ export function stripUserTextBlocksByPrefix(
43
70
  return messages
44
71
  .map((message) => {
45
72
  if (message.role !== "user") return message;
46
- const nextContent = message.content.filter((block) => {
47
- if (block.type !== "text") return true;
48
- return !matchers.some((m) =>
49
- typeof m === "string"
50
- ? block.text.startsWith(m)
51
- : block.text.startsWith(m.prefix) && block.text.endsWith(m.suffix),
52
- );
53
- });
73
+ const nextContent = filterInjectionBlocks(message.content, matchers);
54
74
  if (nextContent.length === message.content.length) return message;
55
75
  if (nextContent.length === 0) return null;
56
76
  return { ...message, content: nextContent };
@@ -60,6 +80,30 @@ export function stripUserTextBlocksByPrefix(
60
80
  );
61
81
  }
62
82
 
83
+ /**
84
+ * Tail-scoped variant of {@link stripUserTextBlocksByPrefix}: remove matching
85
+ * text blocks from ONLY the last message, and only when that message is a user
86
+ * message. This mirrors the runtime injection step, which applies every
87
+ * per-turn block to the tail user message, so clearing the tail's stale copies
88
+ * before re-injection makes re-injection idempotent.
89
+ *
90
+ * The tail message is retained even if all of its blocks are removed, so the
91
+ * role-`"user"` tail invariant the re-injection step relies on is preserved.
92
+ * Earlier messages are untouched, so per-message historical grounding (e.g.
93
+ * each turn's `<turn_context>`) survives and the stable cached prefix — which
94
+ * lives entirely before the tail — is never disturbed.
95
+ */
96
+ export function stripTailUserTextBlocksByPrefix(
97
+ messages: Message[],
98
+ matchers: InjectionMatcher[],
99
+ ): Message[] {
100
+ const last = messages[messages.length - 1];
101
+ if (!last || last.role !== "user") return messages;
102
+ const nextContent = filterInjectionBlocks(last.content, matchers);
103
+ if (nextContent.length === last.content.length) return messages;
104
+ return [...messages.slice(0, -1), { ...last, content: nextContent }];
105
+ }
106
+
63
107
  /**
64
108
  * Full-wrapper matcher for the memory-v3 ephemeral `<memory_spotlight>` block.
65
109
  * Shared by the per-turn scoped strip ({@link stripSpotlightInjections}) and
@@ -88,8 +132,12 @@ export const NOW_SCRATCHPAD_STRIP_PREFIXES: InjectionMatcher[] = [
88
132
  "<now_scratchpad>",
89
133
  ];
90
134
 
91
- /** Matchers stripped by the pipeline (order doesn't matter — single pass). */
92
- const RUNTIME_INJECTION_PREFIXES: InjectionMatcher[] = [
135
+ /**
136
+ * Matchers stripped by the compaction pipeline (order doesn't matter — single
137
+ * pass). Exported so the post-compaction re-injection path can reuse this set
138
+ * as the base of its idempotency strip without duplicating it.
139
+ */
140
+ export const RUNTIME_INJECTION_PREFIXES: InjectionMatcher[] = [
93
141
  "<channel_capabilities>",
94
142
  "<channel_command_context>",
95
143
  "<disk_pressure_warning>",
@@ -305,7 +305,7 @@ export function estimateMessagesTokens(
305
305
  }
306
306
 
307
307
  /** Estimate token cost for a single tool definition. */
308
- function estimateToolDefinitionTokens(tool: ToolDefinition): number {
308
+ export function estimateToolDefinitionTokens(tool: ToolDefinition): number {
309
309
  return (
310
310
  TOOL_DEFINITION_OVERHEAD_TOKENS +
311
311
  estimateTextTokens(tool.name) +
@@ -236,8 +236,9 @@ describe("disposeConversation — auto-analysis enqueue", () => {
236
236
 
237
237
  test("untrusted conversation — enqueues neither graph_extract nor conversation_analyze", () => {
238
238
  // `unknown` is the trust class used for untrusted actors. The disposal
239
- // code short-circuits on `isUntrustedTrustClass()` so neither enqueue
240
- // path should fire. This preserves the memory trust boundary.
239
+ // code short-circuits when `resolveCapabilities(trustClass).canAccessMemory`
240
+ // is false, so neither enqueue path should fire. This preserves the
241
+ // memory trust boundary.
241
242
  const ctx = makeDisposeContext({
242
243
  conversationId: "conv-untrusted",
243
244
  trustClass: "unknown",
@@ -1184,6 +1184,7 @@ export async function finalizePendingToolResultRow(
1184
1184
  let provenanceTrustClass:
1185
1185
  | "guardian"
1186
1186
  | "trusted_contact"
1187
+ | "unverified_contact"
1187
1188
  | "unknown"
1188
1189
  | undefined;
1189
1190
  let automated: boolean | undefined;
@@ -1845,6 +1846,7 @@ export async function handleMessageComplete(
1845
1846
  let provenanceTrustClass:
1846
1847
  | "guardian"
1847
1848
  | "trusted_contact"
1849
+ | "unverified_contact"
1848
1850
  | "unknown"
1849
1851
  | undefined;
1850
1852
  let automated: boolean | undefined;
@@ -70,9 +70,9 @@ import type { UserPromptSubmitContext } from "../plugin-api/types.js";
70
70
  import { runHook } from "../plugins/pipeline.js";
71
71
  import type { ContentBlock, Message } from "../providers/types.js";
72
72
  import type { Provider } from "../providers/types.js";
73
- import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
74
73
  import { broadcastMessage } from "../runtime/assistant-event-hub.js";
75
74
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
75
+ import { resolveCapabilities } from "../runtime/capabilities.js";
76
76
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
77
77
  import type { ActivationMomentParam } from "../telemetry/activation-funnel.js";
78
78
  import type { UsageActor } from "../usage/actors.js";
@@ -187,6 +187,67 @@ export interface AssistantSurface {
187
187
  activationMoment?: ActivationMomentParam;
188
188
  }
189
189
 
190
+ // ── abort watchdog ───────────────────────────────────────────────────
191
+
192
+ /**
193
+ * Generous backstop that drives an aborted turn to its `finally` even if some
194
+ * awaited operation fails to observe the abort signal.
195
+ *
196
+ * Abort is otherwise cooperative and already wired into the slow paths: the
197
+ * provider call forwards the signal to its HTTP/streaming fetch, and tool
198
+ * execution races the signal so a stuck tool can't block cancellation. This
199
+ * watchdog only fires when a future code path silently ignores abort — without
200
+ * it, such a path would hang the loop forever and latch the conversation's
201
+ * `processing` flag true (the wedged "Thinking…" indicator). It is
202
+ * defense-in-depth, not the primary mechanism, so the timeout is deliberately
203
+ * generous; in the common case abort settles in-flight work well before it.
204
+ */
205
+ const ABORT_WATCHDOG_MS = 45_000;
206
+
207
+ /**
208
+ * Race `work` against an abort watchdog. The watchdog stays disarmed until the
209
+ * signal fires; once it does, the turn has `timeoutMs` to settle before the
210
+ * watchdog rejects with the signal's own abort reason so the caller unwinds to
211
+ * its `finally` and the reason classifies as a user cancellation downstream.
212
+ * The abandoned `work` promise keeps running detached — its eventual rejection
213
+ * is swallowed so it can't surface as an unhandled rejection.
214
+ */
215
+ async function withAbortWatchdog<T>(
216
+ work: Promise<T>,
217
+ signal: AbortSignal,
218
+ timeoutMs: number,
219
+ onFire: () => void,
220
+ ): Promise<T> {
221
+ let timer: ReturnType<typeof setTimeout> | undefined;
222
+ let onAbort: (() => void) | undefined;
223
+ const watchdog = new Promise<never>((_, reject) => {
224
+ const arm = () => {
225
+ timer = setTimeout(() => {
226
+ onFire();
227
+ // Propagate the signal's own reason (an `AbortReason` for daemon-owned
228
+ // cancels) so the loop's catch classifies this as a user cancellation;
229
+ // fall back to a tagged AbortError when the signal carries no reason.
230
+ reject(
231
+ signal.reason ??
232
+ new DOMException("The operation was aborted", "AbortError"),
233
+ );
234
+ }, timeoutMs);
235
+ };
236
+ if (signal.aborted) arm();
237
+ else {
238
+ onAbort = arm;
239
+ signal.addEventListener("abort", onAbort, { once: true });
240
+ }
241
+ });
242
+ try {
243
+ return await Promise.race([work, watchdog]);
244
+ } finally {
245
+ if (timer) clearTimeout(timer);
246
+ if (onAbort) signal.removeEventListener("abort", onAbort);
247
+ void work.catch(() => {});
248
+ }
249
+ }
250
+
190
251
  // ── runAgentLoop ─────────────────────────────────────────────────────
191
252
 
192
253
  export async function runAgentLoopImpl(
@@ -902,22 +963,36 @@ export async function runAgentLoopImpl(
902
963
  msgs: Message[],
903
964
  compactInPlace = false,
904
965
  ): Promise<Message[]> => {
905
- const { history, exitReason, newMessages } = await ctx.agentLoop.run({
906
- messages: msgs,
907
- onEvent: eventHandler,
908
- signal: abortController.signal,
909
- requestId: reqId,
910
- onCheckpoint,
911
- callSite: turnCallSite,
912
- trust: loopTrust,
913
- overrideProfile: turnOverrideProfile,
914
- ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
915
- resolveOverrideProfile: resolveCurrentOverrideProfile,
916
- resolveContextWindow,
917
- compactInPlace,
918
- isNonInteractive,
919
- modelProfileKey,
920
- });
966
+ const watchdogMs = ctx.abortWatchdogMs ?? ABORT_WATCHDOG_MS;
967
+ const { history, exitReason, newMessages } = await withAbortWatchdog(
968
+ ctx.agentLoop.run({
969
+ messages: msgs,
970
+ onEvent: eventHandler,
971
+ signal: abortController.signal,
972
+ requestId: reqId,
973
+ onCheckpoint,
974
+ callSite: turnCallSite,
975
+ trust: loopTrust,
976
+ overrideProfile: turnOverrideProfile,
977
+ ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
978
+ resolveOverrideProfile: resolveCurrentOverrideProfile,
979
+ resolveContextWindow,
980
+ compactInPlace,
981
+ isNonInteractive,
982
+ modelProfileKey,
983
+ }),
984
+ abortController.signal,
985
+ watchdogMs,
986
+ () =>
987
+ rlog.error(
988
+ {
989
+ conversationId: ctx.conversationId,
990
+ requestId: reqId,
991
+ timeoutMs: watchdogMs,
992
+ },
993
+ "Abort watchdog fired — agent loop did not settle after cancel; forcing turn to finally",
994
+ ),
995
+ );
921
996
  lastRunNewMessages = newMessages;
922
997
  if (exitReason === "handoff") {
923
998
  yieldedForHandoff = true;
@@ -1450,6 +1525,12 @@ export async function runAgentLoopImpl(
1450
1525
 
1451
1526
  ctx.profiler.emitSummary(ctx.traceEmitter, reqId);
1452
1527
 
1528
+ // Tear down this turn's per-turn state. Abort reliably drives the loop to
1529
+ // this `finally` within a bounded time — cooperative signal propagation
1530
+ // (provider fetch + tool race) backed by the abort watchdog — so a
1531
+ // cancelled turn always unwinds before any resend can start a new one.
1532
+ // There is therefore only ever one turn alive, and clearing the shared
1533
+ // state below cannot clobber a concurrent turn.
1453
1534
  ctx.abortController = null;
1454
1535
  ctx.setProcessing(false);
1455
1536
  ctx.onConfirmationOutcome = undefined;
@@ -1589,9 +1670,9 @@ export async function applyCompactionResult(
1589
1670
  // in-context boundary rather than the raw mirror so the persisted count
1590
1671
  // stays consistent with what the new summary represents and never
1591
1672
  // double-counts an unsliced untrusted view.
1592
- const inContextCompactedCount = isUntrustedTrustClass(
1673
+ const inContextCompactedCount = !resolveCapabilities(
1593
1674
  ctx.trustContext?.trustClass,
1594
- )
1675
+ ).canAccessMemory
1595
1676
  ? 0
1596
1677
  : ctx.contextCompactedMessageCount;
1597
1678
  ctx.contextCompactedMessageCount =
@@ -23,7 +23,7 @@ const log = getLogger("conversation-history");
23
23
 
24
24
  // ── Helpers ──────────────────────────────────────────────────────────
25
25
 
26
- function isToolResultBlock(
26
+ export function isToolResultBlock(
27
27
  block: ContentBlock | Record<string, unknown>,
28
28
  ): boolean {
29
29
  return (
@@ -16,10 +16,8 @@ import type { PermissionPrompter } from "../permissions/prompter.js";
16
16
  import type { SecretPrompter } from "../permissions/secret-prompter.js";
17
17
  import { disposeContextWindowManager } from "../plugins/defaults/compaction/manager-store.js";
18
18
  import type { ContentBlock, Message } from "../providers/types.js";
19
- import {
20
- isUntrustedTrustClass,
21
- type TrustClass,
22
- } from "../runtime/actor-trust-resolver.js";
19
+ import { type TrustClass } from "../runtime/actor-trust-resolver.js";
20
+ import { resolveCapabilities } from "../runtime/capabilities.js";
23
21
  import { unregisterConversationSender } from "../tools/browser/browser-screencast.js";
24
22
  import { type AbortReason, createAbortReason } from "../util/abort-reasons.js";
25
23
  import { getLogger } from "../util/logger.js";
@@ -147,7 +145,7 @@ export function disposeConversation(ctx: DisposeContext): void {
147
145
  // Trigger graph extraction for end-of-conversation sweep.
148
146
  // Only extract from guardian conversations to preserve the memory trust
149
147
  // boundary — untrusted content must not influence future memory retrieval.
150
- if (!isUntrustedTrustClass(ctx.trustContext?.trustClass)) {
148
+ if (resolveCapabilities(ctx.trustContext?.trustClass).canAccessMemory) {
151
149
  // Recursion guard: skip graph_extract for auto-analysis conversations.
152
150
  // The analysis agent writes memory directly via tools, so extracting
153
151
  // from its reflective musings would double-write into the memory graph.
@@ -28,7 +28,10 @@ import {
28
28
  import { extractPreferences } from "../notifications/preference-extractor.js";
29
29
  import { createPreference } from "../notifications/preferences-store.js";
30
30
  import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
31
- import { routeGuardianReply } from "../runtime/guardian-reply-router.js";
31
+ import {
32
+ type GuardianPendingScope,
33
+ routeGuardianReply,
34
+ } from "../runtime/guardian-reply-router.js";
32
35
  import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
33
36
  import { getLogger } from "../util/logger.js";
34
37
  import type { CleanResult, Conversation } from "./conversation.js";
@@ -153,7 +156,7 @@ function resolveQueuedTurnInterfaceContext(
153
156
  }
154
157
 
155
158
  /** Build a SlashContext from the current conversation state and config. */
156
- function buildSlashContext(
159
+ export function buildSlashContext(
157
160
  content: string,
158
161
  conversation: Conversation,
159
162
  ): SlashContext | undefined {
@@ -1407,9 +1410,14 @@ export async function processMessage(
1407
1410
  "vellum",
1408
1411
  ).map((request) => request.id)
1409
1412
  : [];
1410
- const canonicalPendingRequestIdsForConversation =
1413
+ // Empty hints → leave the scope unset (identity-fallback): the desktop
1414
+ // guardian can still resolve their pending work by identity/principal.
1415
+ const pendingScope: GuardianPendingScope | undefined =
1411
1416
  canonicalPendingRequestHintIdsForConversation.length > 0
1412
- ? canonicalPendingRequestHintIdsForConversation
1417
+ ? {
1418
+ mode: "scoped",
1419
+ requestIds: canonicalPendingRequestHintIdsForConversation,
1420
+ }
1413
1421
  : undefined;
1414
1422
 
1415
1423
  // ── Canonical guardian reply router (desktop/conversation path) ──
@@ -1428,7 +1436,7 @@ export async function processMessage(
1428
1436
  conversation.trustContext?.guardianPrincipalId ?? undefined,
1429
1437
  },
1430
1438
  conversationId: conversation.conversationId,
1431
- pendingRequestIds: canonicalPendingRequestIdsForConversation,
1439
+ pendingScope,
1432
1440
  // Desktop path: disable NL classification to avoid consuming non-decision
1433
1441
  // messages while a tool confirmation is pending. Deterministic code-prefix
1434
1442
  // and callback parsing remain active.
@@ -9,9 +9,9 @@ import { statSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  import { type ChannelId, parseInterfaceId } from "../channels/types.js";
12
- import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
13
12
  import { resolveCallSiteConfig } from "../config/llm-resolver.js";
14
13
  import { getConfig } from "../config/loader.js";
14
+ import { isMemoryV3Live } from "../config/memory-v3-gate.js";
15
15
  import type { LLMCallSite, LLMConfig } from "../config/schemas/llm.js";
16
16
  import {
17
17
  NOW_SCRATCHPAD_STRIP_PREFIXES,
@@ -62,11 +62,11 @@ import type {
62
62
  import type { ContentBlock, Message } from "../providers/types.js";
63
63
  import {
64
64
  type ActorTrustContext,
65
- isUntrustedTrustClass,
66
65
  resolveActorTrust,
67
66
  type TrustClass,
68
67
  } from "../runtime/actor-trust-resolver.js";
69
68
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
69
+ import { resolveCapabilities } from "../runtime/capabilities.js";
70
70
  import { channelStatusToMemberStatus } from "../runtime/routes/inbound-stages/acl-enforcement.js";
71
71
  import { getSubagentManager } from "../subagent/index.js";
72
72
  import type { SubagentState } from "../subagent/types.js";
@@ -125,8 +125,8 @@ export interface InboundActorContext {
125
125
  actorSenderDisplayName?: string;
126
126
  /** Guardian-managed display name from the contact record. */
127
127
  actorMemberDisplayName?: string;
128
- /** Trust classification: guardian, trusted_contact, or unknown. */
129
- trustClass: "guardian" | "trusted_contact" | "unknown";
128
+ /** Trust classification: see TrustClass. */
129
+ trustClass: TrustClass;
130
130
  /** Guardian identity for this (assistant, channel) binding. */
131
131
  guardianIdentity?: string;
132
132
  /** Member status when the actor has a contact record. */
@@ -916,7 +916,7 @@ function filterSlackConversationRowsForActor(
916
916
  rows: MessageRow[],
917
917
  trustClass: TrustClass | undefined,
918
918
  ): MessageRow[] {
919
- if (!isUntrustedTrustClass(trustClass)) return rows;
919
+ if (resolveCapabilities(trustClass).canAccessMemory) return rows;
920
920
  const nonSlackVisibleRows = filterMessagesForUntrustedActor(rows);
921
921
  const nonSlackVisibleIds = new Set(nonSlackVisibleRows.map((row) => row.id));
922
922
  return rows.filter((row) => {
@@ -1009,6 +1009,7 @@ function rowToRenderable(row: SlackTranscriptInputRow): RenderableSlackMessage {
1009
1009
  if (
1010
1010
  outer.provenanceTrustClass === "guardian" ||
1011
1011
  outer.provenanceTrustClass === "trusted_contact" ||
1012
+ outer.provenanceTrustClass === "unverified_contact" ||
1012
1013
  outer.provenanceTrustClass === "unknown"
1013
1014
  ) {
1014
1015
  provenanceTrustClass = outer.provenanceTrustClass;
@@ -1322,9 +1323,9 @@ export function loadSlackChronologicalContext(
1322
1323
  options,
1323
1324
  );
1324
1325
  return assembleSlackChronologicalContext(rows, capabilities, {
1325
- contextSummary: isUntrustedTrustClass(options.trustClass)
1326
- ? null
1327
- : options.contextSummary,
1326
+ contextSummary: resolveCapabilities(options.trustClass).canAccessMemory
1327
+ ? options.contextSummary
1328
+ : null,
1328
1329
  });
1329
1330
  }
1330
1331
 
@@ -1536,8 +1537,8 @@ export interface RuntimeInjectionBlocks {
1536
1537
  */
1537
1538
  memoryV3InjectedBlock?: string;
1538
1539
  /**
1539
- * True when memory-v3 superseded v2 as this turn's `<memory>` source — the
1540
- * `memory-v3-live` flag is on AND the v3 injector produced a block (possibly
1540
+ * True when memory-v3 superseded v2 as this turn's `<memory>` source —
1541
+ * `memory.v3.live` is on AND the v3 injector produced a block (possibly
1541
1542
  * empty-text on an all-repeat turn), i.e. exactly when assembly stripped
1542
1543
  * v2's fresh tail block. The user-prompt-submit hook keys v2's
1543
1544
  * `memoryInjectedBlock` metadata persist off this so a stripped v2 block is
@@ -2177,7 +2178,7 @@ export async function applyRuntimeInjections(
2177
2178
  // no-op, keeping the v2 path bit-for-bit identical.
2178
2179
  let runMessagesForAssembly = stripSpotlightInjections(runMessages);
2179
2180
 
2180
- // v2 suppression: when the `memory-v3-live` flag is on AND the v3 injector
2181
+ // v2 suppression: when `memory.v3.live` is on AND the v3 injector
2181
2182
  // produced a block this turn (possibly empty-text on an all-repeat turn), v3
2182
2183
  // owns the `<memory>` layer. v2's `prepareMemory` already prepended its own
2183
2184
  // fresh `<memory>` block to the tail user message — strip the TAIL's v2
@@ -2196,10 +2197,7 @@ export async function applyRuntimeInjections(
2196
2197
  // (`produce()` → null) leaves v2's block intact — fallback rather than a
2197
2198
  // memory-less turn. Idempotent: re-injection sites that already stripped
2198
2199
  // see no change. Flag off → bit-for-bit identical to the v2 path.
2199
- const suppressV2MemoryForV3 = isAssistantFeatureFlagEnabled(
2200
- "memory-v3-live",
2201
- getConfig(),
2202
- );
2200
+ const suppressV2MemoryForV3 = isMemoryV3Live(getConfig());
2203
2201
  const v3ProducedBlock = afterMemory.some((b) => b.id === MEMORY_V3_BLOCK_ID);
2204
2202
  const memoryV3Active = suppressV2MemoryForV3 && v3ProducedBlock;
2205
2203
  if (memoryV3Active) {