@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
@@ -135,7 +135,16 @@ export function resolveConversationId(idOrKey: string): string | null {
135
135
  */
136
136
  export function getOrCreateConversation(
137
137
  conversationKey: string,
138
- opts?: { conversationType?: "standard" },
138
+ opts?: {
139
+ conversationType?: "standard";
140
+ /**
141
+ * Caller-supplied title for the conversation, used only when this call
142
+ * actually creates the row. Treated as a user-set title (`isAutoTitle = 0`)
143
+ * so the async LLM title generator's safe-overwrite check leaves it
144
+ * untouched. Ignored when the conversation already exists.
145
+ */
146
+ title?: string;
147
+ },
139
148
  ): {
140
149
  conversationId: string;
141
150
  conversationType: string;
@@ -205,13 +214,19 @@ export function getOrCreateConversation(
205
214
 
206
215
  const now = Date.now();
207
216
  const conversationId = uuid();
208
- const title = GENERATING_TITLE;
217
+ const customTitle = opts?.title?.trim();
218
+ const title = customTitle || GENERATING_TITLE;
209
219
  const memoryScopeId = "default";
210
220
 
211
221
  tx.insert(conversations)
212
222
  .values({
213
223
  id: conversationId,
214
224
  title,
225
+ // A caller-supplied title is user-set: mark it non-auto (0) so the
226
+ // async LLM title generator's `canReplaceTitle` check won't overwrite
227
+ // it. Without one, omit the column so it takes its default
228
+ // (AUTO_TITLE_LLM) and follows the auto-generated placeholder flow.
229
+ ...(customTitle ? { isAutoTitle: 0 } : {}),
215
230
  createdAt: now,
216
231
  updatedAt: now,
217
232
  totalInputTokens: 0,
@@ -166,6 +166,7 @@ export async function generateAndPersistConversationTitle(
166
166
  const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
167
167
  updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
168
168
  publishConversationTitleChanged(conversationId, fallback);
169
+ logRetryableFallback(params, "no_provider");
169
170
  return { title: fallback, updated: true };
170
171
  }
171
172
 
@@ -206,6 +207,7 @@ export async function generateAndPersistConversationTitle(
206
207
  const fallback = deriveFallbackTitle(context) ?? UNTITLED_FALLBACK;
207
208
  updateConversationTitle(conversationId, fallback, AUTO_TITLE_DETERMINISTIC);
208
209
  publishConversationTitleChanged(conversationId, fallback);
210
+ logRetryableFallback(params, "empty_output");
209
211
  return { title: fallback, updated: true };
210
212
  }
211
213
 
@@ -230,7 +232,7 @@ export const titleMutex = new Mutex();
230
232
  /**
231
233
  * Fire-and-forget wrapper for title generation. Failures are logged
232
234
  * but do not propagate. On failure, replaces loading placeholder with
233
- * a stable fallback title so loading state is never permanent.
235
+ * a retryable fallback title so loading state is never permanent.
234
236
  *
235
237
  * Calls are serialized via {@link titleMutex} so burst conversation
236
238
  * creation does not overwhelm the LLM provider.
@@ -244,16 +246,20 @@ export function queueGenerateConversationTitle(
244
246
  })
245
247
  .catch((err) => {
246
248
  log.warn(
247
- { err, conversationId: params.conversationId },
248
- "Failed to generate conversation title (non-fatal)",
249
+ retryableFallbackLogFields(params, "generation_error", err),
250
+ "Conversation title generation used retryable fallback",
249
251
  );
250
- // Replace loading placeholder with stable fallback
252
+ // Replace loading placeholder with a retryable fallback.
251
253
  try {
252
254
  const conversation = getConversation(params.conversationId);
253
255
  if (conversation && conversation.title === GENERATING_TITLE) {
254
256
  const fallback =
255
257
  deriveFallbackTitle(params.context) ?? UNTITLED_FALLBACK;
256
- updateConversationTitle(params.conversationId, fallback);
258
+ updateConversationTitle(
259
+ params.conversationId,
260
+ fallback,
261
+ AUTO_TITLE_DETERMINISTIC,
262
+ );
257
263
  publishConversationTitleChanged(params.conversationId, fallback);
258
264
  }
259
265
  } catch {
@@ -268,6 +274,11 @@ export interface RegenerateTitleParams {
268
274
  conversationId: string;
269
275
  provider?: Provider;
270
276
  signal?: AbortSignal;
277
+ /**
278
+ * Limit regeneration to placeholder or deterministic titles. Used for retrying
279
+ * failed initial generation without racing against a successful initial title.
280
+ */
281
+ onlyIfReplaceable?: boolean;
271
282
  }
272
283
 
273
284
  /**
@@ -278,12 +289,15 @@ export interface RegenerateTitleParams {
278
289
  export async function regenerateConversationTitle(
279
290
  params: RegenerateTitleParams,
280
291
  ): Promise<{ title: string; updated: boolean }> {
281
- const { conversationId, signal } = params;
292
+ const { conversationId, onlyIfReplaceable, signal } = params;
282
293
 
283
294
  const conversation = getConversation(conversationId);
284
295
  if (!conversation || !conversation.isAutoTitle) {
285
296
  return { title: conversation?.title ?? UNTITLED_FALLBACK, updated: false };
286
297
  }
298
+ if (onlyIfReplaceable && !canReplaceTitle(conversation)) {
299
+ return { title: conversation.title ?? UNTITLED_FALLBACK, updated: false };
300
+ }
287
301
 
288
302
  const provider =
289
303
  params.provider ?? (await getConfiguredProvider("conversationTitle"));
@@ -317,7 +331,11 @@ export async function regenerateConversationTitle(
317
331
  if (title) {
318
332
  // Re-check isAutoTitle before persisting (race guard against manual rename)
319
333
  const current = getConversation(conversationId);
320
- if (!current || !current.isAutoTitle) {
334
+ if (
335
+ !current ||
336
+ !current.isAutoTitle ||
337
+ (onlyIfReplaceable && !canReplaceTitle(current))
338
+ ) {
321
339
  return { title: current?.title ?? UNTITLED_FALLBACK, updated: false };
322
340
  }
323
341
 
@@ -408,6 +426,45 @@ function buildTitlePrompt(
408
426
  return parts.join("\n");
409
427
  }
410
428
 
429
+ function titleGenerationLogFields(params: GenerateTitleParams) {
430
+ return {
431
+ conversationId: params.conversationId,
432
+ contextOrigin: params.context?.origin,
433
+ hasContext: Boolean(params.context),
434
+ userMessageLength: params.userMessage?.length ?? 0,
435
+ assistantResponseLength: params.assistantResponse?.length ?? 0,
436
+ };
437
+ }
438
+
439
+ type TitleGenerationFallbackReason =
440
+ | "no_provider"
441
+ | "empty_output"
442
+ | "generation_error";
443
+
444
+ function retryableFallbackLogFields(
445
+ params: GenerateTitleParams,
446
+ reason: TitleGenerationFallbackReason,
447
+ err?: unknown,
448
+ ): Record<string, unknown> {
449
+ const fields: Record<string, unknown> = {
450
+ ...titleGenerationLogFields(params),
451
+ reason,
452
+ fallbackSource: params.context ? "context" : "untitled",
453
+ };
454
+ if (err) fields.err = err;
455
+ return fields;
456
+ }
457
+
458
+ function logRetryableFallback(
459
+ params: GenerateTitleParams,
460
+ reason: TitleGenerationFallbackReason,
461
+ ): void {
462
+ log.warn(
463
+ retryableFallbackLogFields(params, reason),
464
+ "Conversation title generation used retryable fallback",
465
+ );
466
+ }
467
+
411
468
  const META_FAILURE_TITLES = new Set([
412
469
  "missing context",
413
470
  "no context",
@@ -40,6 +40,7 @@ import {
40
40
  createSkillLoadedEventsTable,
41
41
  createTasksAndWorkItemsTables,
42
42
  createWatchersAndLogsTables,
43
+ dropApprovalPromptTsTrackerTable,
43
44
  migrate230AcpSessionHistory,
44
45
  migrate231RepairMemoryGraphEventDates,
45
46
  migrateA2ATasks,
@@ -102,6 +103,7 @@ import {
102
103
  migrateDropConflicts,
103
104
  migrateDropContactInteractionColumns,
104
105
  migrateDropEntityTables,
106
+ migrateDropExternalUserId,
105
107
  migrateDropLegacyMemberGuardianTables,
106
108
  migrateDropLoopbackPortColumn,
107
109
  migrateDropMemoryItemsTables,
@@ -195,7 +197,9 @@ import {
195
197
  migrateRenameVerificationSessionIdColumn,
196
198
  migrateRenameVerificationTable,
197
199
  migrateRenameVoiceToPhone,
200
+ migrateRewriteBalancedEconomyProfilePins,
198
201
  migrateScheduleCapabilities,
202
+ migrateScheduleDefaultNoReuseConversation,
199
203
  migrateScheduleDescription,
200
204
  migrateScheduleInferenceProfile,
201
205
  migrateScheduleOneShotRouting,
@@ -223,6 +227,7 @@ import {
223
227
  migrateUsageLlmCallCount,
224
228
  migrateVoiceInviteColumns,
225
229
  migrateVoiceInviteDisplayMetadata,
230
+ migrateWorkflowJournalLeafTokens,
226
231
  migrateWorkflowRuns,
227
232
  migrateWorkflowRunTrust,
228
233
  recoverCrashedMigrations,
@@ -521,6 +526,11 @@ export function initializeDb(): void {
521
526
  migrateContactChannelsUniqueExtUser,
522
527
  migrateScheduleCapabilities,
523
528
  migrateContactChannelsRenormalizeAddresses,
529
+ migrateScheduleDefaultNoReuseConversation,
530
+ migrateWorkflowJournalLeafTokens,
531
+ migrateDropExternalUserId,
532
+ dropApprovalPromptTsTrackerTable,
533
+ migrateRewriteBalancedEconomyProfilePins,
524
534
  ];
525
535
 
526
536
  // Run each migration step, catching and logging individual failures so one
@@ -497,6 +497,20 @@ export async function getMemoryBackendStatus(config: AssistantConfig): Promise<{
497
497
  };
498
498
  }
499
499
 
500
+ /**
501
+ * Thrown by {@link embedWithBackend} when no embedding backend is configured or
502
+ * available. This is a PROCESS-WIDE condition (backend selection is effectively
503
+ * cached for the run), so a caller embedding many items should treat the first
504
+ * occurrence as fatal to the whole batch rather than a per-item failure — see
505
+ * `backfillAllSections`, which aborts on it instead of churning through deletes.
506
+ */
507
+ export class EmbeddingBackendUnavailableError extends Error {
508
+ constructor(message = "No memory embedding backend configured") {
509
+ super(message);
510
+ this.name = "EmbeddingBackendUnavailableError";
511
+ }
512
+ }
513
+
500
514
  export async function embedWithBackend(
501
515
  config: AssistantConfig,
502
516
  inputs: EmbeddingInput[],
@@ -514,7 +528,7 @@ export async function embedWithBackend(
514
528
 
515
529
  const selection = await selectEmbeddingBackend(config);
516
530
  if (!selection.backend) {
517
- throw new Error(
531
+ throw new EmbeddingBackendUnavailableError(
518
532
  selection.reason ?? "No memory embedding backend configured",
519
533
  );
520
534
  }
@@ -2,6 +2,7 @@ import { join } from "node:path";
2
2
 
3
3
  import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
4
4
  import { getConfig } from "../config/loader.js";
5
+ import { isMemoryV3Live } from "../config/memory-v3-gate.js";
5
6
  import type { AssistantConfig } from "../config/types.js";
6
7
  import {
7
8
  checkDiskPressureBackgroundGate,
@@ -852,7 +853,7 @@ export function maybeEnqueueGraphMaintenanceJobs(
852
853
  // this guard is belt-and-suspenders that also avoids a wasted enqueue.
853
854
  if (
854
855
  isAssistantFeatureFlagEnabled("memory-v3-shadow", config) ||
855
- isAssistantFeatureFlagEnabled("memory-v3-live", config)
856
+ isMemoryV3Live(config)
856
857
  ) {
857
858
  schedule.push({
858
859
  key: GRAPH_MAINTENANCE_CHECKPOINTS.memoryV3Maintain,
@@ -1,7 +1,7 @@
1
1
  import { and, asc, eq, gt, or } from "drizzle-orm";
2
2
  import { v4 as uuid } from "uuid";
3
3
 
4
- import { getConfig } from "../config/loader.js";
4
+ import { getCachedShareAnalytics } from "../platform/consent-cache.js";
5
5
  import { getDb } from "./db-connection.js";
6
6
  import { lifecycleEvents } from "./schema.js";
7
7
 
@@ -13,7 +13,7 @@ export interface LifecycleEvent {
13
13
 
14
14
  /** Record a lifecycle event (e.g. app_open, hatch). Returns null when usage data collection is disabled. */
15
15
  export function recordLifecycleEvent(eventName: string): LifecycleEvent | null {
16
- if (!getConfig().collectUsageData) return null;
16
+ if (!getCachedShareAnalytics()) return null;
17
17
  const db = getDb();
18
18
  const event: LifecycleEvent = {
19
19
  id: uuid(),
@@ -6,6 +6,8 @@
6
6
  // - Source conversation isn't a memory-retrospective conversation itself
7
7
  // (recursion guard — we never run a retrospective over reflective
8
8
  // musings from the retrospective agent's own writes).
9
+ // - Source isn't a `scheduled` thread or a memory-consolidation background
10
+ // (low yield — see `isLowYieldRetrospectiveSource`).
9
11
  //
10
12
  // All four trigger types funnel through `upsertMemoryRetrospectiveJob` which
11
13
  // coalesces rapid enqueues into a single pending row per conversation.
@@ -13,14 +15,13 @@
13
15
  // after the corresponding signal settles; `interval` and `message_count`
14
16
  // fire immediately.
15
17
 
16
- import {
17
- isUntrustedTrustClass,
18
- type TrustClass,
19
- } from "../runtime/actor-trust-resolver.js";
18
+ import { type TrustClass } from "../runtime/actor-trust-resolver.js";
19
+ import { resolveCapabilities } from "../runtime/capabilities.js";
20
20
  import { getLogger } from "../util/logger.js";
21
- import { getConversationSource } from "./conversation-crud.js";
21
+ import { getConversation, getConversationSource } from "./conversation-crud.js";
22
22
  import { isMemoryEnabled, upsertMemoryRetrospectiveJob } from "./jobs-store.js";
23
23
  import { isMemoryRetrospectiveSource } from "./memory-retrospective-constants.js";
24
+ import { MEMORY_V2_CONSOLIDATION_SOURCE } from "./v2/constants.js";
24
25
 
25
26
  const log = getLogger("memory-retrospective-enqueue");
26
27
 
@@ -50,6 +51,14 @@ export function enqueueMemoryRetrospectiveIfEnabled(args: {
50
51
  return;
51
52
  }
52
53
 
54
+ if (isLowYieldRetrospectiveSource(conversationId)) {
55
+ log.debug(
56
+ { conversationId, trigger },
57
+ "Skipping memory-retrospective enqueue: scheduled or consolidation source",
58
+ );
59
+ return;
60
+ }
61
+
53
62
  const runAfter =
54
63
  trigger === "compaction" ? Date.now() + COMPACTION_DEBOUNCE_MS : Date.now();
55
64
 
@@ -75,6 +84,22 @@ export function isMemoryRetrospectiveConversation(
75
84
  return source !== null && isMemoryRetrospectiveSource(source);
76
85
  }
77
86
 
87
+ /**
88
+ * Scheduled task threads (location/health pulses) rarely carry anything worth
89
+ * remembering, and memory-consolidation conversations already persist their
90
+ * output to the corpus — a retrospective over either burns an inference pass
91
+ * for no unique gain (and, for consolidation, re-stores already-captured
92
+ * content). Heartbeat (`background`) and standard conversations are unaffected.
93
+ */
94
+ function isLowYieldRetrospectiveSource(conversationId: string): boolean {
95
+ const conversation = getConversation(conversationId);
96
+ if (!conversation) return false;
97
+ return (
98
+ conversation.conversationType === "scheduled" ||
99
+ conversation.source === MEMORY_V2_CONSOLIDATION_SOURCE
100
+ );
101
+ }
102
+
78
103
  /**
79
104
  * Fire a memory-retrospective enqueue from the compaction site. Mirrors
80
105
  * `enqueueAutoAnalysisOnCompaction` — same trust-class gate (don't run a
@@ -86,7 +111,7 @@ export function enqueueMemoryRetrospectiveOnCompaction(
86
111
  conversationId: string,
87
112
  trustClass: TrustClass | undefined,
88
113
  ): void {
89
- if (isUntrustedTrustClass(trustClass)) {
114
+ if (!resolveCapabilities(trustClass).canAccessMemory) {
90
115
  return;
91
116
  }
92
117
  try {
@@ -65,6 +65,7 @@ import {
65
65
  getMessagesAfter,
66
66
  resolveOverrideProfile,
67
67
  } from "./conversation-crud.js";
68
+ import { isBackgroundConversationType } from "./conversation-types.js";
68
69
  import {
69
70
  enqueueMemoryJob,
70
71
  type MemoryJob,
@@ -313,6 +314,14 @@ async function runForkBasedRetrospective(
313
314
  // {@link WakeToolContextPin}.
314
315
  toolGateMode: "execution" as const,
315
316
  toolContextPin,
317
+ // Reproduce the source's turn block for message-tier cache-prefix
318
+ // parity: background/scheduled sources ran non-interactive live turns
319
+ // (which carry `<background_turn>`), so the fork must run non-interactive
320
+ // too. Standard/user sources ran interactive (no `<background_turn>`), so
321
+ // the fork stays interactive — preserving their existing byte parity.
322
+ isNonInteractive: isBackgroundConversationType(
323
+ sourceConversation.conversationType,
324
+ ),
316
325
  // Profile forcing (model/thinking/effort parity) is a separate concern
317
326
  // and stays keyed on `matchConversationProfile` via `matchedProfile`.
318
327
  ...(matchedProfile !== undefined
@@ -1,4 +1,4 @@
1
- import type { DrizzleDb } from "../db-connection.js";
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
2
 
3
3
  /**
4
4
  * Add verification and access-control columns to contact_channels for
@@ -9,13 +9,20 @@ import type { DrizzleDb } from "../db-connection.js";
9
9
  * Uses ALTER TABLE ADD COLUMN with try/catch for idempotency.
10
10
  */
11
11
  export function migrateContactChannelsAccessFields(database: DrizzleDb): void {
12
- // Channel-native user ID (e.g., Telegram numeric ID, E.164 phone) machine identifier for trust resolution
13
- try {
12
+ // external_user_id: only add on first run. migration 294 drops this column;
13
+ // if it's absent but other 129-era columns already exist, 294 has run and
14
+ // we must not re-add it (avoids a table-rewrite cycle on every startup).
15
+ const raw = getSqliteFrom(database);
16
+ const cols = raw.prepare("PRAGMA table_info(contact_channels)").all() as {
17
+ name: string;
18
+ }[];
19
+ const colNames = new Set(cols.map((c) => c.name));
20
+ const needsExternalUserId =
21
+ !colNames.has("external_user_id") && !colNames.has("status");
22
+ if (needsExternalUserId) {
14
23
  database.run(
15
24
  /*sql*/ `ALTER TABLE contact_channels ADD COLUMN external_user_id TEXT`,
16
25
  );
17
- } catch {
18
- /* already exists */
19
26
  }
20
27
  // Delivery/notification routing address (e.g., Telegram chat ID for DMs)
21
28
  try {
@@ -98,8 +105,10 @@ export function migrateContactChannelsAccessFields(database: DrizzleDb): void {
98
105
  /* already exists */
99
106
  }
100
107
 
101
- // Composite index for trust resolution lookups by channel type + external user ID
102
- database.run(
103
- /*sql*/ `CREATE INDEX IF NOT EXISTS idx_contact_channels_type_ext_user ON contact_channels(type, external_user_id)`,
104
- );
108
+ // Only create the index when the column exists (migration 294 drops both).
109
+ if (needsExternalUserId || colNames.has("external_user_id")) {
110
+ database.run(
111
+ /*sql*/ `CREATE INDEX IF NOT EXISTS idx_contact_channels_type_ext_user ON contact_channels(type, external_user_id)`,
112
+ );
113
+ }
105
114
  }
@@ -16,6 +16,18 @@ export function migrateDropLegacyMemberGuardianTables(
16
16
  ): void {
17
17
  const raw = getSqliteFrom(database);
18
18
 
19
+ // The safety-sync below reads and writes contact_channels.external_user_id.
20
+ // A later migration drops that column; this step re-runs on every startup,
21
+ // so once the column is gone the sync cannot run (and is moot —
22
+ // identities live in `address`). Skip the sync in that case but still
23
+ // drop the legacy tables, so this step completes instead of failing every
24
+ // boot. Reaching here with the tables still present means the sync never
25
+ // succeeded, so nothing synced is lost.
26
+ const cols = raw.prepare(`PRAGMA table_info(contact_channels)`).all() as {
27
+ name: string;
28
+ }[];
29
+ const hasExternalUserId = cols.some((c) => c.name === "external_user_id");
30
+
19
31
  // ── Safety sync: guardian bindings → contacts ─────────────────────
20
32
  const guardianTableExists = raw
21
33
  .prepare(
@@ -23,7 +35,7 @@ export function migrateDropLegacyMemberGuardianTables(
23
35
  )
24
36
  .get();
25
37
 
26
- if (guardianTableExists) {
38
+ if (guardianTableExists && hasExternalUserId) {
27
39
  // Insert any active guardian bindings not already present in contacts.
28
40
  // We match on (type, external_user_id) to avoid duplicating existing rows.
29
41
  raw.exec(/*sql*/ `
@@ -77,7 +89,7 @@ export function migrateDropLegacyMemberGuardianTables(
77
89
  )
78
90
  .get();
79
91
 
80
- if (membersTableExists) {
92
+ if (membersTableExists && hasExternalUserId) {
81
93
  // Insert non-pending members not already present in contacts.
82
94
  raw.exec(/*sql*/ `
83
95
  INSERT INTO contacts (id, display_name, created_at, updated_at)
@@ -18,6 +18,16 @@ const log = getLogger("migration-289");
18
18
  export function migrateContactChannelsUniqueExtUser(database: DrizzleDb): void {
19
19
  const raw = getSqliteFrom(database);
20
20
 
21
+ // A later migration drops external_user_id; once it has run this historical
22
+ // dedup is obsolete (Steps 2–3 below reference the column). This step re-runs
23
+ // on every startup, so skip when the column is absent rather than failing.
24
+ const cols = raw.prepare("PRAGMA table_info(contact_channels)").all() as {
25
+ name: string;
26
+ }[];
27
+ if (!cols.some((c) => c.name === "external_user_id")) {
28
+ return;
29
+ }
30
+
21
31
  // Step 1: Deduplicate historical case collisions. After this, the existing
22
32
  // case-sensitive UNIQUE(type, address) constraint remains valid because
23
33
  // only one row per case-insensitive group survives.
@@ -16,6 +16,16 @@ export function migrateContactChannelsRenormalizeAddresses(
16
16
  ): void {
17
17
  const raw = getSqliteFrom(database);
18
18
 
19
+ // A later migration drops external_user_id; once it has run there is nothing
20
+ // to renormalize from. This step re-runs on every startup, so skip when the
21
+ // column is absent rather than referencing it and failing.
22
+ const cols = raw.prepare("PRAGMA table_info(contact_channels)").all() as {
23
+ name: string;
24
+ }[];
25
+ if (!cols.some((c) => c.name === "external_user_id")) {
26
+ return;
27
+ }
28
+
19
29
  // Remove rows that would block normalization due to cross-column collisions.
20
30
  raw.run(/*sql*/ `
21
31
  DELETE FROM contact_channels
@@ -0,0 +1,67 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { describe, expect, test } from "bun:test";
3
+
4
+ import { drizzle } from "drizzle-orm/bun-sqlite";
5
+
6
+ import * as schema from "../schema.js";
7
+ import { migrateScheduleDefaultNoReuseConversation } from "./292-schedule-default-no-reuse-conversation.js";
8
+
9
+ function createTestDb() {
10
+ const sqlite = new Database(":memory:");
11
+ // Post-210 shape: reuse_conversation column present (trimmed to the columns
12
+ // the migration and assertions touch).
13
+ sqlite.exec(/*sql*/ `
14
+ CREATE TABLE cron_jobs (
15
+ id TEXT PRIMARY KEY,
16
+ name TEXT NOT NULL,
17
+ message TEXT NOT NULL,
18
+ next_run_at INTEGER NOT NULL,
19
+ created_by TEXT NOT NULL,
20
+ created_at INTEGER NOT NULL,
21
+ updated_at INTEGER NOT NULL,
22
+ reuse_conversation INTEGER NOT NULL DEFAULT 0
23
+ )
24
+ `);
25
+ return { sqlite, db: drizzle(sqlite, { schema }) };
26
+ }
27
+
28
+ function insertJob(sqlite: Database, id: string, reuse: number) {
29
+ sqlite
30
+ .query(
31
+ /*sql*/ `INSERT INTO cron_jobs
32
+ (id, name, message, next_run_at, created_by, created_at, updated_at, reuse_conversation)
33
+ VALUES (?, ?, 'msg', 1000, 'agent', 1000, 1000, ?)`,
34
+ )
35
+ .run(id, id, reuse);
36
+ }
37
+
38
+ function reuseValue(sqlite: Database, id: string): number {
39
+ return (
40
+ sqlite
41
+ .query("SELECT reuse_conversation FROM cron_jobs WHERE id = ?")
42
+ .get(id) as { reuse_conversation: number }
43
+ ).reuse_conversation;
44
+ }
45
+
46
+ describe("migration 292: schedules default to no conversation reuse", () => {
47
+ test("flips existing reuse_conversation=1 rows to 0", () => {
48
+ const { sqlite, db } = createTestDb();
49
+ insertJob(sqlite, "reusing", 1);
50
+ insertJob(sqlite, "already-fresh", 0);
51
+
52
+ migrateScheduleDefaultNoReuseConversation(db);
53
+
54
+ expect(reuseValue(sqlite, "reusing")).toBe(0);
55
+ expect(reuseValue(sqlite, "already-fresh")).toBe(0);
56
+ });
57
+
58
+ test("is idempotent — re-run is a no-op", () => {
59
+ const { sqlite, db } = createTestDb();
60
+ insertJob(sqlite, "reusing", 1);
61
+
62
+ migrateScheduleDefaultNoReuseConversation(db);
63
+ expect(() => migrateScheduleDefaultNoReuseConversation(db)).not.toThrow();
64
+
65
+ expect(reuseValue(sqlite, "reusing")).toBe(0);
66
+ });
67
+ });
@@ -0,0 +1,25 @@
1
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
2
+
3
+ /**
4
+ * Disable conversation reuse on all existing schedules.
5
+ *
6
+ * Recurring schedules previously defaulted to `reuse_conversation = 1`, so
7
+ * every fire appended to one long-lived conversation. That unbounded,
8
+ * self-similar transcript is a drift hazard for weaker models (it primes them
9
+ * to repeat or extend the prior run) and grows per-fire token cost without
10
+ * adding correctness — durable cross-run state already lives in workspace files
11
+ * and memory. Schedules now default to a fresh conversation per fire; this
12
+ * aligns existing rows with that contract.
13
+ *
14
+ * Idempotent: the guarded UPDATE matches nothing once every row is already 0.
15
+ */
16
+ export function migrateScheduleDefaultNoReuseConversation(
17
+ database: DrizzleDb,
18
+ ): void {
19
+ const raw = getSqliteFrom(database);
20
+ raw
21
+ .query(
22
+ /*sql*/ `UPDATE cron_jobs SET reuse_conversation = 0 WHERE reuse_conversation != 0`,
23
+ )
24
+ .run();
25
+ }
@@ -0,0 +1,32 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ /**
5
+ * Add nullable `input_tokens` / `output_tokens` columns to `workflow_journal`.
6
+ *
7
+ * Persists per-leaf token usage so the journal route can attribute usage to each
8
+ * leaf. The client then computes run-level token metrics from the per-leaf sum
9
+ * (a single source of truth), counting each leaf exactly once regardless of
10
+ * whether its usage arrives via a live `leaf_finished` event or a journal
11
+ * backfill — which avoids the undercount that arose when a mid-run journal
12
+ * aggregate counted a leaf the journal could not itself attribute.
13
+ *
14
+ * Nullable — legacy rows and non-completed leaves (failures, nested
15
+ * `workflow`-kind entries) stay NULL and contribute zero to the sum.
16
+ *
17
+ * Idempotent — each ALTER is wrapped so a re-run (column already present) is a
18
+ * no-op.
19
+ */
20
+ export function migrateWorkflowJournalLeafTokens(database: DrizzleDb): void {
21
+ const raw = getSqliteFrom(database);
22
+ try {
23
+ raw.exec(`ALTER TABLE workflow_journal ADD COLUMN input_tokens INTEGER`);
24
+ } catch {
25
+ /* Column already exists */
26
+ }
27
+ try {
28
+ raw.exec(`ALTER TABLE workflow_journal ADD COLUMN output_tokens INTEGER`);
29
+ } catch {
30
+ /* Column already exists */
31
+ }
32
+ }
@@ -0,0 +1,31 @@
1
+ import { getLogger } from "../../util/logger.js";
2
+ import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
3
+
4
+ const log = getLogger("migration-294");
5
+
6
+ /**
7
+ * Drops the `external_user_id` column and its index from `contact_channels`.
8
+ *
9
+ * `address` is the single canonical identity column; `external_user_id` is
10
+ * redundant. The index `idx_contact_channels_type_ext_user` must be dropped
11
+ * first — SQLite refuses to drop a column referenced by an index.
12
+ *
13
+ * Idempotent: skips if the column has already been dropped.
14
+ */
15
+ export function migrateDropExternalUserId(database: DrizzleDb): void {
16
+ const raw = getSqliteFrom(database);
17
+
18
+ const cols = raw.prepare("PRAGMA table_info(contact_channels)").all() as {
19
+ name: string;
20
+ }[];
21
+ const hasColumn = cols.some((c) => c.name === "external_user_id");
22
+
23
+ if (!hasColumn) {
24
+ log.info("external_user_id column already absent — skipping");
25
+ return;
26
+ }
27
+
28
+ raw.run("DROP INDEX IF EXISTS idx_contact_channels_type_ext_user");
29
+ raw.run("ALTER TABLE contact_channels DROP COLUMN external_user_id");
30
+ log.info("Dropped external_user_id column from contact_channels");
31
+ }