@vellumai/assistant 0.8.5 → 0.8.6

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 (544) hide show
  1. package/AGENTS.md +33 -1
  2. package/ARCHITECTURE.md +1 -1
  3. package/bunfig.toml +6 -1
  4. package/docs/credential-execution-service.md +6 -6
  5. package/docs/plugins.md +4 -3
  6. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +12 -13
  7. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +4 -1
  8. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +16 -14
  9. package/openapi.yaml +1900 -166
  10. package/package.json +1 -1
  11. package/src/__tests__/actor-token-service.test.ts +3 -2
  12. package/src/__tests__/agent-loop-exit-reason.test.ts +102 -9
  13. package/src/__tests__/agent-loop-override-profile.test.ts +2 -1
  14. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
  15. package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
  16. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -2
  17. package/src/__tests__/annotate-risk-options.test.ts +1 -0
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/approval-routes-http.test.ts +9 -13
  20. package/src/__tests__/assert-not-live-db.ts +79 -0
  21. package/src/__tests__/assistant-feature-flags-integration.test.ts +9 -25
  22. package/src/__tests__/audit-log-rotation.test.ts +2 -2
  23. package/src/__tests__/auto-analysis-end-to-end.test.ts +6 -6
  24. package/src/__tests__/background-workers-disk-pressure.test.ts +5 -8
  25. package/src/__tests__/browser-skill-endstate.test.ts +3 -3
  26. package/src/__tests__/btw-routes.test.ts +3 -2
  27. package/src/__tests__/call-controller.test.ts +3 -2
  28. package/src/__tests__/channel-approval-routes.test.ts +3 -2
  29. package/src/__tests__/channel-guardian.test.ts +3 -2
  30. package/src/__tests__/channel-readiness-slack-remote.test.ts +175 -0
  31. package/src/__tests__/channel-reply-delivery.test.ts +35 -0
  32. package/src/__tests__/channel-retry-sweep.test.ts +320 -3
  33. package/src/__tests__/checker.test.ts +12 -12
  34. package/src/__tests__/compaction-events.test.ts +1 -0
  35. package/src/__tests__/compaction-trail-store.test.ts +264 -0
  36. package/src/__tests__/compactor-call-site-logging.test.ts +1 -0
  37. package/src/__tests__/compactor-preserved-tail-count.test.ts +1 -0
  38. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +7 -5
  39. package/src/__tests__/computer-use-tools.test.ts +12 -14
  40. package/src/__tests__/config-loader-backfill.test.ts +13 -28
  41. package/src/__tests__/config-loader-corrupt.test.ts +5 -5
  42. package/src/__tests__/config-loader-platform-defaults.test.ts +93 -26
  43. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +3 -3
  44. package/src/__tests__/config-managed-gemini-defaults.test.ts +3 -4
  45. package/src/__tests__/config-schema.test.ts +10 -10
  46. package/src/__tests__/connection-model-compat.test.ts +83 -0
  47. package/src/__tests__/contacts-tools.test.ts +3 -2
  48. package/src/__tests__/context-token-estimator.test.ts +22 -0
  49. package/src/__tests__/conversation-abort-tool-results.test.ts +5 -0
  50. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop-handlers-max-tokens.test.ts +55 -0
  52. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +34 -0
  54. package/src/__tests__/conversation-agent-loop.test.ts +488 -2
  55. package/src/__tests__/conversation-analysis-routes.test.ts +1 -0
  56. package/src/__tests__/conversation-app-control-instantiation.test.ts +29 -19
  57. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -0
  58. package/src/__tests__/conversation-attention-store.test.ts +101 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +3 -2
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  61. package/src/__tests__/conversation-error.test.ts +30 -0
  62. package/src/__tests__/conversation-fork-crud.test.ts +69 -8
  63. package/src/__tests__/conversation-fork-route.test.ts +3 -2
  64. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  65. package/src/__tests__/conversation-inference-profile-list.test.ts +3 -2
  66. package/src/__tests__/conversation-inference-profile-route.test.ts +3 -2
  67. package/src/__tests__/conversation-lifecycle.test.ts +1 -0
  68. package/src/__tests__/conversation-list-source.test.ts +3 -2
  69. package/src/__tests__/conversation-load-history-repair.test.ts +2 -1
  70. package/src/__tests__/conversation-load-history-stripped.test.ts +1 -0
  71. package/src/__tests__/conversation-pairing.test.ts +53 -0
  72. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +26 -7
  73. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  74. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -0
  75. package/src/__tests__/conversation-queue.test.ts +333 -291
  76. package/src/__tests__/conversation-routes-disk-view.test.ts +3 -18
  77. package/src/__tests__/conversation-routes-guardian-reply.test.ts +33 -8
  78. package/src/__tests__/conversation-routes-slash-commands.test.ts +33 -2
  79. package/src/__tests__/conversation-runtime-assembly.test.ts +78 -0
  80. package/src/__tests__/conversation-skill-tools.test.ts +38 -142
  81. package/src/__tests__/conversation-slash-queue.test.ts +84 -32
  82. package/src/__tests__/conversation-slash-unknown.test.ts +5 -0
  83. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  84. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +46 -0
  85. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -0
  86. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +6 -3
  87. package/src/__tests__/conversation-surfaces-standalone.test.ts +6 -3
  88. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -3
  89. package/src/__tests__/conversation-surfaces-table-action.test.ts +7 -17
  90. package/src/__tests__/conversation-sync-tags.test.ts +128 -12
  91. package/src/__tests__/conversation-title-service.test.ts +1 -0
  92. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +30 -0
  93. package/src/__tests__/conversation-usage.test.ts +1 -0
  94. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  95. package/src/__tests__/conversation-workspace-injection.test.ts +5 -0
  96. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -0
  97. package/src/__tests__/credential-broker-browser-fill.test.ts +3 -3
  98. package/src/__tests__/credential-broker-server-use.test.ts +5 -5
  99. package/src/__tests__/credential-execution-client.test.ts +72 -1
  100. package/src/__tests__/credential-execution-feature-gates.test.ts +10 -12
  101. package/src/__tests__/credential-health-service.test.ts +252 -3
  102. package/src/__tests__/credential-security-invariants.test.ts +5 -5
  103. package/src/__tests__/credential-vault-unit.test.ts +19 -19
  104. package/src/__tests__/credential-vault.test.ts +5 -5
  105. package/src/__tests__/cross-provider-web-search.test.ts +56 -2
  106. package/src/__tests__/db-connection-isolation.test.ts +7 -6
  107. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +8 -10
  108. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +7 -10
  109. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +9 -15
  110. package/src/__tests__/db-test-helpers.ts +58 -0
  111. package/src/__tests__/disk-pressure-guard.test.ts +58 -41
  112. package/src/__tests__/disk-pressure-lifecycle.test.ts +13 -10
  113. package/src/__tests__/disk-pressure-routes.test.ts +0 -33
  114. package/src/__tests__/disk-pressure-tools.test.ts +0 -4
  115. package/src/__tests__/dm-persistence.test.ts +26 -40
  116. package/src/__tests__/document-create-dedupe.test.ts +189 -0
  117. package/src/__tests__/document-find-replace.test.ts +3 -2
  118. package/src/__tests__/document-tool-security.test.ts +81 -2
  119. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +5 -4
  120. package/src/__tests__/encrypted-store-test-helpers.ts +56 -0
  121. package/src/__tests__/encrypted-store.test.ts +11 -9
  122. package/src/__tests__/feature-flag-test-helpers.ts +53 -0
  123. package/src/__tests__/filing-service.test.ts +1 -0
  124. package/src/__tests__/first-greeting.test.ts +62 -12
  125. package/src/__tests__/gateway-flag-listener.test.ts +0 -1
  126. package/src/__tests__/gemini-provider.test.ts +26 -0
  127. package/src/__tests__/guardian-action-sweep.test.ts +3 -2
  128. package/src/__tests__/guardian-outbound-http.test.ts +3 -2
  129. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +48 -3
  130. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -0
  131. package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
  132. package/src/__tests__/heartbeat-service.test.ts +1 -0
  133. package/src/__tests__/helpers/mock-logger.ts +26 -0
  134. package/src/__tests__/host-bash-routes.test.ts +1 -0
  135. package/src/__tests__/host-cu-routes-targeted.test.ts +1 -0
  136. package/src/__tests__/host-file-routes-targeted.test.ts +1 -0
  137. package/src/__tests__/host-shell-tool.test.ts +5 -4
  138. package/src/__tests__/host-transfer-routes-targeted.test.ts +1 -0
  139. package/src/__tests__/http-conversation-lineage.test.ts +3 -2
  140. package/src/__tests__/http-user-message-parity.test.ts +29 -7
  141. package/src/__tests__/identity-intro-cache.test.ts +133 -22
  142. package/src/__tests__/inbound-slack-persistence.test.ts +44 -72
  143. package/src/__tests__/inference-profile-reaper.test.ts +3 -2
  144. package/src/__tests__/inference-profile-session-ipc.test.ts +3 -2
  145. package/src/__tests__/injector-disk-pressure.test.ts +3 -17
  146. package/src/__tests__/inline-skill-load-permissions.test.ts +4 -4
  147. package/src/__tests__/list-messages-hidden-metadata.test.ts +80 -0
  148. package/src/__tests__/llm-context-normalization.test.ts +42 -0
  149. package/src/__tests__/llm-resolver.test.ts +331 -0
  150. package/src/__tests__/llm-schema.test.ts +1 -1
  151. package/src/__tests__/manual-token-reconciliation.test.ts +76 -1
  152. package/src/__tests__/mcp-abort-signal.test.ts +14 -0
  153. package/src/__tests__/mcp-client-auth.test.ts +14 -0
  154. package/src/__tests__/messaging-send-tool.test.ts +1 -0
  155. package/src/__tests__/migration-import-from-url.test.ts +3 -3
  156. package/src/__tests__/mock-gateway-ipc.ts +18 -2
  157. package/src/__tests__/model-intents.test.ts +3 -3
  158. package/src/__tests__/native-web-search.test.ts +30 -2
  159. package/src/__tests__/notification-deep-link.test.ts +62 -0
  160. package/src/__tests__/oauth-commands-routes.test.ts +37 -0
  161. package/src/__tests__/oauth-provider-visibility.test.ts +8 -8
  162. package/src/__tests__/oauth-store.test.ts +3 -2
  163. package/src/__tests__/onboarding-template-contract.test.ts +3 -2
  164. package/src/__tests__/openai-provider.test.ts +8 -9
  165. package/src/__tests__/openai-responses-provider.test.ts +70 -10
  166. package/src/__tests__/openrouter-provider-only.test.ts +27 -5
  167. package/src/__tests__/outbound-slack-persistence.test.ts +46 -1
  168. package/src/__tests__/persistence-pipeline.test.ts +139 -1
  169. package/src/__tests__/persistence-secret-redaction.test.ts +83 -12
  170. package/src/__tests__/plugin-bootstrap.test.ts +9 -11
  171. package/src/__tests__/plugin-tool-contribution.test.ts +41 -38
  172. package/src/__tests__/process-message-background-slack.test.ts +21 -16
  173. package/src/__tests__/process-message-display-content.test.ts +19 -22
  174. package/src/__tests__/provider-catalog-visibility.test.ts +9 -9
  175. package/src/__tests__/provider-platform-proxy-integration.test.ts +216 -4
  176. package/src/__tests__/provider-registry-ollama.test.ts +45 -22
  177. package/src/__tests__/recording-handler.test.ts +1 -0
  178. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  179. package/src/__tests__/registry.test.ts +82 -76
  180. package/src/__tests__/relay-server.test.ts +10 -10
  181. package/src/__tests__/runtime-attachment-metadata.test.ts +3 -2
  182. package/src/__tests__/schedule-store.test.ts +16 -1
  183. package/src/__tests__/scheduler-reuse-conversation.test.ts +48 -3
  184. package/src/__tests__/secret-ingress-http.test.ts +5 -1
  185. package/src/__tests__/secure-keys.test.ts +3 -3
  186. package/src/__tests__/send-endpoint-busy.test.ts +81 -42
  187. package/src/__tests__/server-history-render.test.ts +4 -1
  188. package/src/__tests__/skill-feature-flags-integration.test.ts +8 -10
  189. package/src/__tests__/skill-feature-flags.test.ts +14 -16
  190. package/src/__tests__/skill-load-feature-flag.test.ts +5 -5
  191. package/src/__tests__/skill-projection-feature-flag.test.ts +44 -30
  192. package/src/__tests__/skill-projection.benchmark.test.ts +5 -7
  193. package/src/__tests__/skill-tool-factory.test.ts +96 -95
  194. package/src/__tests__/slack-channel-config.test.ts +3 -3
  195. package/src/__tests__/subagent-call-site-routing.test.ts +11 -3
  196. package/src/__tests__/subagent-disposal.test.ts +27 -8
  197. package/src/__tests__/subagent-fork-notifications.test.ts +24 -9
  198. package/src/__tests__/subagent-fork-spawn.test.ts +13 -4
  199. package/src/__tests__/subagent-manager-notify.test.ts +20 -8
  200. package/src/__tests__/subagent-notify-parent.test.ts +5 -4
  201. package/src/__tests__/subagent-spawn-tool-fork.test.ts +58 -0
  202. package/src/__tests__/subagent-tools.test.ts +2 -1
  203. package/src/__tests__/suggestion-routes.test.ts +1 -0
  204. package/src/__tests__/system-prompt.test.ts +38 -0
  205. package/src/__tests__/test-preload-verifier.ts +68 -0
  206. package/src/__tests__/test-preload.ts +32 -39
  207. package/src/__tests__/tool-executor-lifecycle-events.test.ts +20 -7
  208. package/src/__tests__/tool-executor.test.ts +55 -10
  209. package/src/__tests__/tool-preview-lifecycle.test.ts +1 -0
  210. package/src/__tests__/tool-result-metadata-plumbing.test.ts +1 -0
  211. package/src/__tests__/twilio-routes.test.ts +3 -2
  212. package/src/__tests__/validate-input.test.ts +381 -0
  213. package/src/__tests__/verification-control-plane-policy.test.ts +1 -0
  214. package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -1
  215. package/src/__tests__/voice-session-bridge.test.ts +37 -28
  216. package/src/__tests__/workspace-migration-090-memory-router-cost-optimized-profile.test.ts +326 -0
  217. package/src/__tests__/workspace-migration-091-retighten-migration-onboarding-thread.test.ts +166 -0
  218. package/src/acp/session-manager.ts +5 -6
  219. package/src/agent/loop.ts +80 -0
  220. package/src/api/README.md +124 -2
  221. package/src/api/constants/call-sites.ts +27 -0
  222. package/src/api/events/assistant-outbound-attachment.ts +51 -0
  223. package/src/api/events/assistant-text-delta.ts +32 -0
  224. package/src/api/events/assistant-turn-start.ts +33 -0
  225. package/src/api/events/document-comment-created.ts +48 -0
  226. package/src/api/events/document-comment-deleted.ts +24 -0
  227. package/src/api/events/document-comment-reopened.ts +25 -0
  228. package/src/api/events/document-comment-resolved.ts +27 -0
  229. package/src/api/events/generation-cancelled.ts +24 -0
  230. package/src/api/events/generation-handoff.ts +41 -0
  231. package/src/api/events/message-complete.ts +42 -0
  232. package/src/api/events/open-url.ts +30 -0
  233. package/src/{events → api/events}/relationship-state-updated.ts +3 -3
  234. package/src/api/events/tool-use-start.ts +32 -0
  235. package/src/api/index.ts +128 -3
  236. package/src/api/responses/llm-context-response.ts +39 -0
  237. package/src/api/responses/llm-request-log-entry.ts +93 -0
  238. package/src/api/responses/memory-recall-log.ts +65 -0
  239. package/src/api/responses/memory-v2-activation-log.ts +78 -0
  240. package/src/background-wake/background-wake-routes.test.ts +687 -52
  241. package/src/background-wake/platform-client.test.ts +308 -0
  242. package/src/background-wake/platform-client.ts +167 -0
  243. package/src/background-wake/publisher.ts +91 -0
  244. package/src/background-wake/runtime-registry.ts +2 -2
  245. package/src/background-wake/wake-intent-hooks.test.ts +282 -0
  246. package/src/calls/guardian-dispatch.ts +1 -0
  247. package/src/calls/voice-session-bridge.ts +4 -4
  248. package/src/cli/commands/__tests__/conversations-slack.test.ts +16 -0
  249. package/src/cli/commands/__tests__/notifications.test.ts +184 -40
  250. package/src/cli/commands/channels/__tests__/channels.test.ts +143 -0
  251. package/src/cli/commands/channels/index.ts +229 -0
  252. package/src/cli/commands/memory-v3-render.ts +147 -0
  253. package/src/cli/commands/memory-v3.ts +255 -4
  254. package/src/cli/commands/notifications.ts +365 -55
  255. package/src/cli/lib/open-browser.ts +7 -2
  256. package/src/cli/program.ts +2 -0
  257. package/src/config/assistant-feature-flags.ts +23 -42
  258. package/src/config/bundled-skills/document-editor/SKILL.md +5 -1
  259. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  260. package/src/config/bundled-skills/schedule/TOOLS.json +2 -2
  261. package/src/config/bundled-skills/settings/tools/open-system-settings.ts +1 -0
  262. package/src/config/call-site-defaults.ts +1 -1
  263. package/src/config/feature-flag-cache.ts +86 -0
  264. package/src/config/feature-flag-registry.json +17 -17
  265. package/src/config/llm-context-resolution.ts +10 -1
  266. package/src/config/llm-resolver.ts +121 -15
  267. package/src/config/loader.ts +4 -5
  268. package/src/config/schemas/__tests__/memory-v2.test.ts +15 -0
  269. package/src/config/schemas/heartbeat.ts +1 -1
  270. package/src/config/schemas/llm.ts +90 -1
  271. package/src/config/schemas/memory-v2.ts +26 -0
  272. package/src/config/schemas/services.ts +6 -2
  273. package/src/config/seed-inference-profiles.ts +36 -16
  274. package/src/context/token-estimator.ts +10 -5
  275. package/src/credential-execution/executable-discovery.ts +40 -0
  276. package/src/credential-execution/process-manager.ts +6 -2
  277. package/src/credential-health/credential-health-service.ts +125 -40
  278. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -6
  279. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +13 -15
  280. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -2
  281. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -0
  282. package/src/daemon/__tests__/meet-manifest-loader.test.ts +25 -12
  283. package/src/daemon/__tests__/native-web-search-metadata.test.ts +1 -0
  284. package/src/daemon/__tests__/switch-inference-profile-tool.test.ts +107 -0
  285. package/src/daemon/__tests__/web-search-status-text.test.ts +1 -0
  286. package/src/daemon/conversation-agent-loop-handlers.ts +389 -68
  287. package/src/daemon/conversation-agent-loop.ts +132 -28
  288. package/src/daemon/conversation-error.ts +33 -5
  289. package/src/daemon/conversation-messaging.ts +84 -43
  290. package/src/daemon/conversation-process.ts +74 -37
  291. package/src/daemon/conversation-runtime-assembly.ts +29 -9
  292. package/src/daemon/conversation-skill-tools.ts +14 -30
  293. package/src/daemon/conversation-surfaces.ts +69 -34
  294. package/src/daemon/conversation-tool-setup.ts +33 -48
  295. package/src/daemon/conversation.ts +26 -46
  296. package/src/daemon/daemon-control.ts +1 -1
  297. package/src/daemon/daemon-skill-host.ts +9 -2
  298. package/src/daemon/disk-pressure-guard.ts +27 -29
  299. package/src/daemon/first-greeting.ts +31 -13
  300. package/src/daemon/handlers/shared.ts +6 -1
  301. package/src/daemon/lifecycle.ts +12 -12
  302. package/src/daemon/mcp-reload-service.ts +1 -1
  303. package/src/daemon/meet-manifest-loader.ts +10 -17
  304. package/src/daemon/message-types/conversations.ts +20 -22
  305. package/src/daemon/message-types/document-comments.ts +8 -44
  306. package/src/daemon/message-types/home.ts +2 -2
  307. package/src/daemon/message-types/integrations.ts +2 -7
  308. package/src/daemon/message-types/messages.ts +23 -38
  309. package/src/daemon/message-types/subagents.ts +6 -0
  310. package/src/daemon/process-message.ts +9 -9
  311. package/src/daemon/providers-setup.ts +1 -1
  312. package/src/daemon/server.ts +16 -0
  313. package/src/daemon/switch-inference-profile-tool.ts +13 -3
  314. package/src/daemon/tool-setup-types.ts +0 -6
  315. package/src/daemon/wake-target-adapter.ts +10 -0
  316. package/src/documents/document-store.ts +38 -0
  317. package/src/export/__tests__/transcript-formatter.test.ts +1 -0
  318. package/src/heartbeat/__tests__/heartbeat-service.test.ts +29 -0
  319. package/src/heartbeat/heartbeat-service.ts +63 -0
  320. package/src/home/__tests__/feed-writer.test.ts +161 -0
  321. package/src/home/__tests__/post-connect-feed.test.ts +1 -0
  322. package/src/home/__tests__/suggested-prompts.test.ts +55 -59
  323. package/src/home/feed-writer.ts +146 -7
  324. package/src/home/suggested-prompts.ts +27 -145
  325. package/src/ipc/__tests__/cli-ipc.test.ts +1 -0
  326. package/src/ipc/gateway-client.test.ts +4 -1
  327. package/src/ipc/skill-routes/__tests__/memory.test.ts +1 -0
  328. package/src/ipc/skill-routes/__tests__/registries.test.ts +36 -7
  329. package/src/ipc/skill-routes/memory.ts +4 -3
  330. package/src/ipc/skill-routes/registries.ts +28 -29
  331. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +1 -0
  332. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +26 -5
  333. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +1 -0
  334. package/src/memory/__tests__/memory-retrospective-job.test.ts +1 -0
  335. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +1 -0
  336. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +31 -0
  337. package/src/memory/conversation-attention-store.ts +17 -3
  338. package/src/memory/conversation-crud.ts +352 -112
  339. package/src/memory/db-connection.ts +29 -19
  340. package/src/memory/db-init.ts +4 -0
  341. package/src/memory/db-singleton.ts +77 -0
  342. package/src/memory/delivery-channels.ts +82 -0
  343. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +2 -4
  344. package/src/memory/graph/retriever.test.ts +3 -3
  345. package/src/memory/job-handlers/embedding.test.ts +3 -2
  346. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -2
  347. package/src/memory/jobs-worker.ts +12 -1
  348. package/src/memory/llm-request-log-source-clickhouse.ts +80 -0
  349. package/src/memory/llm-request-log-source-local.ts +24 -0
  350. package/src/memory/llm-request-log-source.ts +31 -0
  351. package/src/memory/llm-request-log-store.ts +188 -3
  352. package/src/memory/memory-v2-activation-log-store.ts +95 -1
  353. package/src/memory/migrations/265-drop-provider-connection-status.ts +26 -0
  354. package/src/memory/migrations/266-messages-client-message-id.ts +43 -0
  355. package/src/memory/migrations/index.ts +2 -0
  356. package/src/memory/schema/conversations.ts +9 -1
  357. package/src/memory/schema/inference.ts +0 -1
  358. package/src/memory/v2/__tests__/backfill-jobs.test.ts +5 -2
  359. package/src/memory/v2/__tests__/harness-metrics.test.ts +9 -0
  360. package/src/memory/v2/__tests__/harness-replay-input.test.ts +9 -4
  361. package/src/memory/v2/__tests__/harness-runner.test.ts +26 -0
  362. package/src/memory/v2/__tests__/sweep-job.test.ts +6 -3
  363. package/src/memory/v2/harness/metrics.ts +5 -1
  364. package/src/memory/v2/harness/replay-input.ts +19 -3
  365. package/src/memory/v2/harness/runner.ts +6 -0
  366. package/src/memory/v2/harness/trace.ts +6 -0
  367. package/src/memory/v3/__tests__/consolidation-job.test.ts +2 -4
  368. package/src/memory/v3/__tests__/coretrieval-seed.test.ts +270 -0
  369. package/src/memory/v3/__tests__/edges.test.ts +144 -1
  370. package/src/memory/v3/__tests__/filter.test.ts +48 -0
  371. package/src/memory/v3/__tests__/gate.test.ts +96 -33
  372. package/src/memory/v3/__tests__/index-composition.test.ts +58 -0
  373. package/src/memory/v3/__tests__/loop.test.ts +250 -5
  374. package/src/memory/v3/__tests__/scouts.test.ts +49 -0
  375. package/src/memory/v3/__tests__/shadow-diff.test.ts +225 -0
  376. package/src/memory/v3/__tests__/shadow-middleware.test.ts +88 -2
  377. package/src/memory/v3/__tests__/traversal.test.ts +39 -0
  378. package/src/memory/v3/__tests__/tree-walk.test.ts +77 -0
  379. package/src/memory/v3/__tests__/validate.test.ts +32 -0
  380. package/src/memory/v3/coretrieval-seed.ts +240 -0
  381. package/src/memory/v3/edges.ts +58 -21
  382. package/src/memory/v3/filter.ts +27 -22
  383. package/src/memory/v3/gate.ts +51 -36
  384. package/src/memory/v3/index-composition.ts +18 -5
  385. package/src/memory/v3/loop.ts +65 -17
  386. package/src/memory/v3/scouts.ts +15 -4
  387. package/src/memory/v3/shadow-diff.ts +287 -0
  388. package/src/memory/v3/shadow-middleware.ts +44 -2
  389. package/src/memory/v3/traversal.ts +6 -1
  390. package/src/memory/v3/tree-walk.ts +6 -1
  391. package/src/memory/v3/validate.ts +56 -33
  392. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  393. package/src/notifications/__tests__/home-feed-side-effect.test.ts +1 -0
  394. package/src/notifications/adapters/slack.ts +45 -11
  395. package/src/notifications/broadcaster.ts +114 -63
  396. package/src/notifications/conversation-pairing.ts +23 -3
  397. package/src/notifications/decisions-store.ts +32 -1
  398. package/src/notifications/deliveries-store.ts +45 -0
  399. package/src/notifications/edit-notification.ts +201 -0
  400. package/src/notifications/emit-signal.ts +11 -1
  401. package/src/notifications/signal.ts +10 -0
  402. package/src/notifications/types.ts +37 -0
  403. package/src/oauth/byo-connection.test.ts +67 -3
  404. package/src/oauth/byo-connection.ts +32 -5
  405. package/src/oauth/connect-orchestrator.ts +9 -0
  406. package/src/oauth/connection-resolver.test.ts +76 -0
  407. package/src/oauth/connection-resolver.ts +49 -10
  408. package/src/oauth/manual-token-connection.ts +51 -3
  409. package/src/oauth/seed-providers.ts +3 -0
  410. package/src/permissions/approval-policy.test.ts +19 -5
  411. package/src/permissions/approval-policy.ts +14 -3
  412. package/src/permissions/checker.ts +21 -8
  413. package/src/platform/client.test.ts +24 -1
  414. package/src/platform/client.ts +8 -0
  415. package/src/platform/feature-gate.ts +15 -0
  416. package/src/plugins/defaults/injectors.ts +2 -8
  417. package/src/plugins/defaults/persistence.ts +25 -6
  418. package/src/plugins/types.ts +57 -13
  419. package/src/proactive-artifact/job.test.ts +1 -0
  420. package/src/prompts/__tests__/system-prompt.test.ts +4 -4
  421. package/src/prompts/system-prompt.ts +38 -40
  422. package/src/prompts/template-detection.ts +10 -4
  423. package/src/prompts/templates/BOOTSTRAP.md +7 -11
  424. package/src/prompts/templates/IDENTITY.md +0 -2
  425. package/src/providers/__tests__/connection-model-compat.test.ts +3 -4
  426. package/src/providers/__tests__/registry-native-web-search.test.ts +122 -0
  427. package/src/providers/call-site-routing.ts +33 -9
  428. package/src/providers/connection-model-compat.ts +23 -0
  429. package/src/providers/connection-resolution.ts +39 -20
  430. package/src/providers/fireworks/client.ts +1 -0
  431. package/src/providers/gemini/client.ts +24 -3
  432. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +0 -2
  433. package/src/providers/inference/__tests__/base-url-security.test.ts +2 -3
  434. package/src/providers/inference/__tests__/{connections-status-label.test.ts → connections-label.test.ts} +12 -111
  435. package/src/providers/inference/auth.ts +0 -8
  436. package/src/providers/inference/connections.ts +3 -66
  437. package/src/providers/inference/resolve-auth.ts +2 -3
  438. package/src/providers/model-catalog.ts +35 -1
  439. package/src/providers/model-intents.ts +3 -3
  440. package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
  441. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +157 -5
  442. package/src/providers/openai/chat-completions-provider.ts +110 -12
  443. package/src/providers/openai/codex-models.ts +2 -0
  444. package/src/providers/openai/responses-provider.ts +53 -53
  445. package/src/providers/openrouter/client.ts +13 -8
  446. package/src/providers/provider-send-message.ts +18 -9
  447. package/src/providers/registry.ts +48 -8
  448. package/src/providers/retry.ts +16 -4
  449. package/src/providers/search-provider-catalog.ts +17 -9
  450. package/src/providers/types.ts +9 -0
  451. package/src/runtime/__tests__/agent-wake.test.ts +1 -0
  452. package/src/runtime/__tests__/background-job-runner.test.ts +1 -0
  453. package/src/runtime/access-request-helper.ts +1 -0
  454. package/src/runtime/auth/route-policy.ts +10 -0
  455. package/src/runtime/channel-readiness-service.ts +68 -0
  456. package/src/runtime/channel-reply-delivery.ts +23 -0
  457. package/src/runtime/channel-retry-sweep.ts +47 -14
  458. package/src/runtime/confirmation-request-guardian-bridge.ts +1 -1
  459. package/src/runtime/migrations/vbundle-builder.ts +3 -2
  460. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +1 -0
  461. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +406 -0
  462. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  463. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
  464. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +209 -1
  465. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +13 -50
  466. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +51 -3
  467. package/src/runtime/routes/__tests__/memory-v3-simulate-params.test.ts +35 -0
  468. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +3 -2
  469. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +294 -0
  470. package/src/runtime/routes/__tests__/task-routes.test.ts +48 -3
  471. package/src/runtime/routes/acp-routes-list.test.ts +3 -0
  472. package/src/runtime/routes/app-management-routes.ts +111 -4
  473. package/src/runtime/routes/background-wake-routes.ts +188 -20
  474. package/src/runtime/routes/btw-routes.ts +4 -4
  475. package/src/runtime/routes/conversation-analysis-routes.ts +6 -0
  476. package/src/runtime/routes/conversation-compaction-routes.ts +263 -0
  477. package/src/runtime/routes/conversation-list-routes.ts +147 -0
  478. package/src/runtime/routes/conversation-management-routes.ts +39 -14
  479. package/src/runtime/routes/conversation-query-routes.ts +60 -10
  480. package/src/runtime/routes/conversation-routes.ts +186 -140
  481. package/src/runtime/routes/conversations-import-routes.ts +19 -6
  482. package/src/runtime/routes/documents-routes.ts +10 -1
  483. package/src/runtime/routes/group-routes.ts +11 -0
  484. package/src/runtime/routes/home-feed-routes.ts +129 -0
  485. package/src/runtime/routes/identity-intro-cache.ts +61 -16
  486. package/src/runtime/routes/identity-routes.ts +30 -9
  487. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +530 -6
  488. package/src/runtime/routes/inbound-stages/background-dispatch.ts +57 -8
  489. package/src/runtime/routes/index.ts +2 -0
  490. package/src/runtime/routes/inference-provider-connection-routes.ts +5 -26
  491. package/src/runtime/routes/integrations/vercel.ts +15 -0
  492. package/src/runtime/routes/llm-context-normalization.ts +7 -2
  493. package/src/runtime/routes/memory-v3-routes.ts +160 -2
  494. package/src/runtime/routes/migration-routes.ts +20 -13
  495. package/src/runtime/routes/notification-routes.ts +63 -1
  496. package/src/runtime/routes/oauth-commands-routes.ts +6 -1
  497. package/src/runtime/routes/surface-action-routes.ts +1 -38
  498. package/src/runtime/routes/surface-content-routes.ts +12 -5
  499. package/src/runtime/routes/surface-conversation-resolver.ts +65 -0
  500. package/src/runtime/routes/wipe-conversation-routes.ts +3 -0
  501. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -0
  502. package/src/runtime/slack-dm-text-delivery.ts +177 -0
  503. package/src/runtime/sync/resource-sync-events.ts +1 -1
  504. package/src/runtime/tool-grant-request-helper.ts +1 -0
  505. package/src/schedule/schedule-store.ts +8 -1
  506. package/src/schedule/scheduler.ts +111 -15
  507. package/src/security/__tests__/provider-key-env-fallback.test.ts +3 -3
  508. package/src/security/encrypted-store.ts +7 -16
  509. package/src/security/store-path-override.ts +61 -0
  510. package/src/signals/user-message.ts +5 -8
  511. package/src/skills/validate-input.ts +177 -0
  512. package/src/subagent/manager.ts +13 -13
  513. package/src/subagent/types.ts +6 -0
  514. package/src/tasks/tool-sanitizer.ts +2 -2
  515. package/src/tools/apps/definitions.ts +35 -21
  516. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +2 -8
  517. package/src/tools/computer-use/definitions.ts +268 -266
  518. package/src/tools/document/document-tool.ts +131 -8
  519. package/src/tools/execution-target.ts +2 -5
  520. package/src/tools/executor.ts +18 -55
  521. package/src/tools/host-filesystem/edit.test.ts +1 -0
  522. package/src/tools/host-filesystem/read.test.ts +1 -0
  523. package/src/tools/host-filesystem/transfer.test.ts +31 -6
  524. package/src/tools/host-filesystem/write.test.ts +1 -0
  525. package/src/tools/mcp/mcp-tool-factory.ts +0 -2
  526. package/src/tools/network/__tests__/managed-search-proxy.test.ts +282 -0
  527. package/src/tools/network/__tests__/web-search.test.ts +211 -3
  528. package/src/tools/network/managed-search-proxy.ts +183 -0
  529. package/src/tools/network/web-search.ts +199 -44
  530. package/src/tools/policy-context.ts +3 -1
  531. package/src/tools/registry.ts +146 -103
  532. package/src/tools/schedule/create.ts +1 -1
  533. package/src/tools/skills/skill-tool-factory.ts +17 -36
  534. package/src/tools/subagent/spawn.ts +3 -0
  535. package/src/tools/tool-approval-handler.ts +10 -4
  536. package/src/tools/tool-name-aliases.ts +72 -14
  537. package/src/tools/types.ts +17 -15
  538. package/src/tools/ui-surface/definitions.ts +98 -86
  539. package/src/types/onboarding-context.ts +6 -0
  540. package/src/usage/attribution.ts +32 -1
  541. package/src/util/browser.ts +7 -2
  542. package/src/workspace/migrations/090-memory-router-cost-optimized-profile.ts +109 -0
  543. package/src/workspace/migrations/091-retighten-migration-onboarding-thread.ts +41 -0
  544. package/src/workspace/migrations/registry.ts +4 -0
@@ -134,58 +134,81 @@ function nodeChildrenOf(tree: TreeIndex, nodeId: string): string[] {
134
134
  }
135
135
 
136
136
  /**
137
- * Full DFS over `node:` adjacency from `tree.root`. Returns the set of
138
- * reachable node ids (for orphan-page reachability) and the back-edges that
139
- * close a cycle. A back-edge is an edge into a node still on the active
140
- * recursion stack (classic gray-node cycle detection); `visited` (black)
141
- * prevents re-walking shared DAG sub-nodes.
137
+ * Full DFS over `node:` adjacency. Returns the set of nodes reachable from
138
+ * `tree.root` (for orphan-page reachability) and the back-edges that close a
139
+ * cycle. A back-edge is an edge into a node still on the active recursion stack
140
+ * (classic gray-node cycle detection); `visited` (black) prevents re-walking
141
+ * shared DAG sub-nodes.
142
+ *
143
+ * The walk runs in two phases. The first seeds from the root and records every
144
+ * node it reaches in `reachableNodes`. The second sweeps any node still
145
+ * unvisited — a disconnected component that the root cannot reach — so cycles
146
+ * living entirely outside the root's reach are still reported. Sweep-only nodes
147
+ * are deliberately kept out of `reachableNodes`: they are *not* reachable from
148
+ * the root, and pages hanging off them must still surface as orphans.
142
149
  */
143
150
  function descend(tree: TreeIndex): {
144
151
  reachableNodes: Set<string>;
145
152
  cycles: Array<{ from: string; to: string }>;
146
153
  } {
147
154
  const reachableNodes = new Set<string>();
148
- const onStack = new Set<string>();
155
+ const visited = new Set<string>();
149
156
  const cycles: Array<{ from: string; to: string }> = [];
150
157
 
151
158
  // Iterative DFS with an explicit stack so deep trees don't blow the call
152
159
  // stack. Each frame tracks its child cursor; we push a child frame, and on
153
160
  // exhaustion pop the parent off the recursion stack (`onStack`).
154
161
  type Frame = { node: string; children: string[]; cursor: number };
155
- const stack: Frame[] = [];
156
162
 
157
- function enter(nodeId: string): void {
158
- reachableNodes.add(nodeId);
159
- onStack.add(nodeId);
160
- stack.push({
161
- node: nodeId,
162
- children: nodeChildrenOf(tree, nodeId),
163
- cursor: 0,
164
- });
163
+ function walkFrom(start: string, trackReachable: boolean): void {
164
+ const onStack = new Set<string>();
165
+ const stack: Frame[] = [];
166
+
167
+ function enter(nodeId: string): void {
168
+ visited.add(nodeId);
169
+ if (trackReachable) reachableNodes.add(nodeId);
170
+ onStack.add(nodeId);
171
+ stack.push({
172
+ node: nodeId,
173
+ children: nodeChildrenOf(tree, nodeId),
174
+ cursor: 0,
175
+ });
176
+ }
177
+
178
+ enter(start);
179
+
180
+ while (stack.length > 0) {
181
+ const frame = stack[stack.length - 1];
182
+ if (frame.cursor >= frame.children.length) {
183
+ onStack.delete(frame.node);
184
+ stack.pop();
185
+ continue;
186
+ }
187
+ const child = frame.children[frame.cursor++];
188
+ if (onStack.has(child)) {
189
+ // Edge into an ancestor still on the stack → cycle-closing back-edge.
190
+ cycles.push({ from: frame.node, to: child });
191
+ continue;
192
+ }
193
+ if (visited.has(child)) {
194
+ // Already fully explored (shared DAG sub-node or an earlier sweep).
195
+ continue;
196
+ }
197
+ enter(child);
198
+ }
165
199
  }
166
200
 
167
201
  if (tree.nodes.has(tree.root)) {
168
- enter(tree.root);
202
+ walkFrom(tree.root, true);
169
203
  }
170
204
 
171
- while (stack.length > 0) {
172
- const frame = stack[stack.length - 1];
173
- if (frame.cursor >= frame.children.length) {
174
- onStack.delete(frame.node);
175
- stack.pop();
176
- continue;
177
- }
178
- const child = frame.children[frame.cursor++];
179
- if (onStack.has(child)) {
180
- // Edge into an ancestor still on the stack → cycle-closing back-edge.
181
- cycles.push({ from: frame.node, to: child });
182
- continue;
183
- }
184
- if (reachableNodes.has(child)) {
185
- // Already fully explored via another parent (shared DAG sub-node).
186
- continue;
205
+ // Cover nodes the root never reached (disconnected components) so a cycle
206
+ // among them is not silently missed. These are not root-reachable, so their
207
+ // pages stay eligible for the orphan-page report.
208
+ for (const nodeId of tree.nodes.keys()) {
209
+ if (!visited.has(nodeId)) {
210
+ walkFrom(nodeId, false);
187
211
  }
188
- enter(child);
189
212
  }
190
213
 
191
214
  cycles.sort(
@@ -34,6 +34,7 @@ mock.module("../../home/feed-writer.js", () => ({
34
34
  // home-feed-side-effect.ts only consumes `getConversation`.
35
35
  mock.module("../../memory/conversation-crud.js", () => ({
36
36
  getConversation: () => conversationRow,
37
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
37
38
  }));
38
39
 
39
40
  // Stub the broadcaster so emit-signal's `getBroadcaster()` does not need
@@ -30,6 +30,7 @@ mock.module("../../memory/conversation-crud.js", () => ({
30
30
  }
31
31
  return conversationRow;
32
32
  },
33
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
33
34
  }));
34
35
 
35
36
  const { writeHomeFeedItemForSignal } =
@@ -16,6 +16,8 @@ import type {
16
16
  ChannelAdapter,
17
17
  ChannelDeliveryPayload,
18
18
  ChannelDestination,
19
+ ChannelUpdateContext,
20
+ ChannelUpdatePayload,
19
21
  DeliveryResult,
20
22
  NotificationChannel,
21
23
  } from "../types.js";
@@ -52,9 +54,7 @@ function resolveSlackMessageText(payload: ChannelDeliveryPayload): string {
52
54
  * - Optional context: message preview
53
55
  * - Context: approval code instructions + invite directive
54
56
  */
55
- function buildAccessRequestBlocks(
56
- payload: Record<string, unknown>,
57
- ): unknown[] {
57
+ function buildAccessRequestBlocks(payload: Record<string, unknown>): unknown[] {
58
58
  const blocks: unknown[] = [];
59
59
 
60
60
  // Header
@@ -209,19 +209,18 @@ export class SlackAdapter implements ChannelAdapter {
209
209
  payload.contextPayload != null;
210
210
 
211
211
  try {
212
- if (isAccessRequest) {
213
- const blocks = buildAccessRequestBlocks(payload.contextPayload!);
214
- await sendSlackReply(chatId, messageText, { blocks });
215
- } else {
216
- await sendSlackReply(chatId, messageText, { useBlocks: true });
217
- }
212
+ const result = isAccessRequest
213
+ ? await sendSlackReply(chatId, messageText, {
214
+ blocks: buildAccessRequestBlocks(payload.contextPayload!),
215
+ })
216
+ : await sendSlackReply(chatId, messageText, { useBlocks: true });
218
217
 
219
218
  log.info(
220
- { sourceEventName: payload.sourceEventName, chatId },
219
+ { sourceEventName: payload.sourceEventName, chatId, ts: result.ts },
221
220
  "Slack notification delivered",
222
221
  );
223
222
 
224
- return { success: true };
223
+ return { success: true, messageId: result.ts };
225
224
  } catch (err) {
226
225
  const message = err instanceof Error ? err.message : String(err);
227
226
  log.error(
@@ -231,4 +230,39 @@ export class SlackAdapter implements ChannelAdapter {
231
230
  return { success: false, error: message };
232
231
  }
233
232
  }
233
+
234
+ async update(
235
+ delivery: ChannelUpdateContext,
236
+ patch: ChannelUpdatePayload,
237
+ ): Promise<DeliveryResult> {
238
+ if (!delivery.messageId) {
239
+ return {
240
+ success: false,
241
+ error:
242
+ "missing_message_id: this delivery has no captured Slack ts (sent before edit support landed)",
243
+ };
244
+ }
245
+ const text = patch.body?.trim() || patch.title?.trim();
246
+ if (!text) {
247
+ return { success: false, error: "no body or title supplied for update" };
248
+ }
249
+ try {
250
+ const result = await sendSlackReply(delivery.destination, text, {
251
+ messageTs: delivery.messageId,
252
+ useBlocks: true,
253
+ });
254
+ log.info(
255
+ { chatId: delivery.destination, messageTs: delivery.messageId },
256
+ "Slack notification updated",
257
+ );
258
+ return { success: true, messageId: result.ts ?? delivery.messageId };
259
+ } catch (err) {
260
+ const message = err instanceof Error ? err.message : String(err);
261
+ log.error(
262
+ { err, chatId: delivery.destination, messageTs: delivery.messageId },
263
+ "Failed to update Slack notification",
264
+ );
265
+ return { success: false, error: message };
266
+ }
267
+ }
234
268
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { v4 as uuid } from "uuid";
13
13
 
14
+ import { getConversation } from "../memory/conversation-crud.js";
14
15
  import { getLogger } from "../util/logger.js";
15
16
  import { isGuardianSensitiveEvent } from "./adapters/macos.js";
16
17
  import { pairDeliveryWithConversation } from "./conversation-pairing.js";
@@ -72,6 +73,11 @@ export class NotificationBroadcaster {
72
73
  this.onConversationCreated = fn;
73
74
  }
74
75
 
76
+ /** Return the registered adapter for a channel, if any. */
77
+ getAdapter(channel: NotificationChannel): ChannelAdapter | undefined {
78
+ return this.adapters.get(channel);
79
+ }
80
+
75
81
  /**
76
82
  * Broadcast a notification decision to all selected channels.
77
83
  *
@@ -231,78 +237,93 @@ export class NotificationBroadcaster {
231
237
  );
232
238
 
233
239
  // For the vellum channel, merge the conversationId into deep-link metadata
234
- // so the macOS client can navigate directly to the notification conversation.
240
+ // so the macOS client can navigate directly to the conversation. Prefer
241
+ // the paired conversation (interactive opt-in flows); otherwise fall back
242
+ // to the originating conversation referenced by `sourceContextId` when it
243
+ // resolves to a real row. Sentinel context ids (job IDs, call session IDs,
244
+ // access-req-* strings) leave the deep link without a conversation, and
245
+ // the macOS handler opens the app to its default landing.
235
246
  let deepLinkTarget = decision.deepLinkTarget;
236
- if (channel === "vellum" && pairing.conversationId) {
237
- deepLinkTarget = {
238
- ...deepLinkTarget,
239
- conversationId: pairing.conversationId,
240
- };
241
- if (pairing.messageId) {
242
- deepLinkTarget = { ...deepLinkTarget, messageId: pairing.messageId };
243
- }
244
-
245
- // Resolve guardian scoping for conversation-created events so clients
246
- // can filter guardian-sensitive conversations the same way they filter
247
- // guardian-sensitive notification intents.
248
- const guardianPrincipalId =
249
- typeof destination.metadata?.guardianPrincipalId === "string"
250
- ? destination.metadata.guardianPrincipalId
251
- : undefined;
252
- const targetGuardianPrincipalId =
253
- guardianPrincipalId &&
254
- isGuardianSensitiveEvent(signal.sourceEventName)
255
- ? guardianPrincipalId
256
- : undefined;
257
-
258
- const conversationTitle =
259
- copy.conversationTitle ?? copy.title ?? signal.sourceEventName;
260
- const conversationSilent =
261
- signal.attentionHints.urgency !== "high" &&
262
- signal.attentionHints.urgency !== "critical";
263
- const info: ConversationCreatedInfo = {
264
- conversationId: pairing.conversationId,
265
- title: conversationTitle,
266
- sourceEventName: signal.sourceEventName,
267
- targetGuardianPrincipalId,
268
- groupId: signal.conversationMetadata?.groupId,
269
- source: signal.conversationMetadata?.source,
270
- silent: conversationSilent,
271
- };
272
-
273
- // The per-dispatch onConversationCreated callback fires whenever a vellum
274
- // conversation is paired (new or reused) because callers like
275
- // dispatchGuardianQuestion rely on it to create delivery bookkeeping
276
- // rows before emitNotificationSignal() returns.
277
- if (options?.onConversationCreated) {
278
- try {
279
- options.onConversationCreated(info);
280
- } catch (err) {
281
- log.error(
282
- { err, signalId: signal.signalId },
283
- "per-dispatch onConversationCreated callback failed — continuing broadcast",
284
- );
247
+ if (channel === "vellum") {
248
+ const deepLinkConversationId =
249
+ pairing.conversationId ??
250
+ resolveSourceConversationId(signal.sourceContextId);
251
+ if (deepLinkConversationId) {
252
+ deepLinkTarget = {
253
+ ...deepLinkTarget,
254
+ conversationId: deepLinkConversationId,
255
+ };
256
+ if (pairing.messageId) {
257
+ deepLinkTarget = {
258
+ ...deepLinkTarget,
259
+ messageId: pairing.messageId,
260
+ };
285
261
  }
286
262
  }
287
263
 
288
- // Emit notification_conversation_created event only when a NEW
289
- // conversation was actually created. Reusing an existing conversation
290
- // should not fire the event — the client already knows about the
291
- // conversation.
292
- if (
293
- pairing.createdNewConversation &&
294
- pairing.strategy === "start_new_conversation"
295
- ) {
296
- if (this.onConversationCreated) {
264
+ if (pairing.conversationId) {
265
+ // Resolve guardian scoping for conversation-created events so clients
266
+ // can filter guardian-sensitive conversations the same way they filter
267
+ // guardian-sensitive notification intents.
268
+ const guardianPrincipalId =
269
+ typeof destination.metadata?.guardianPrincipalId === "string"
270
+ ? destination.metadata.guardianPrincipalId
271
+ : undefined;
272
+ const targetGuardianPrincipalId =
273
+ guardianPrincipalId &&
274
+ isGuardianSensitiveEvent(signal.sourceEventName)
275
+ ? guardianPrincipalId
276
+ : undefined;
277
+
278
+ const conversationTitle =
279
+ copy.conversationTitle ?? copy.title ?? signal.sourceEventName;
280
+ const conversationSilent =
281
+ signal.attentionHints.urgency !== "high" &&
282
+ signal.attentionHints.urgency !== "critical";
283
+ const info: ConversationCreatedInfo = {
284
+ conversationId: pairing.conversationId,
285
+ title: conversationTitle,
286
+ sourceEventName: signal.sourceEventName,
287
+ targetGuardianPrincipalId,
288
+ groupId: signal.conversationMetadata?.groupId,
289
+ source: signal.conversationMetadata?.source,
290
+ silent: conversationSilent,
291
+ };
292
+
293
+ // The per-dispatch onConversationCreated callback fires whenever a vellum
294
+ // conversation is paired (new or reused) because callers like
295
+ // dispatchGuardianQuestion rely on it to create delivery bookkeeping
296
+ // rows before emitNotificationSignal() returns.
297
+ if (options?.onConversationCreated) {
297
298
  try {
298
- this.onConversationCreated(info);
299
+ options.onConversationCreated(info);
299
300
  } catch (err) {
300
301
  log.error(
301
302
  { err, signalId: signal.signalId },
302
- "onConversationCreated callback failed — continuing broadcast",
303
+ "per-dispatch onConversationCreated callback failed — continuing broadcast",
303
304
  );
304
305
  }
305
306
  }
307
+
308
+ // Emit notification_conversation_created event only when a NEW
309
+ // conversation was actually created. Reusing an existing conversation
310
+ // should not fire the event — the client already knows about the
311
+ // conversation.
312
+ if (
313
+ pairing.createdNewConversation &&
314
+ pairing.strategy === "start_new_conversation"
315
+ ) {
316
+ if (this.onConversationCreated) {
317
+ try {
318
+ this.onConversationCreated(info);
319
+ } catch (err) {
320
+ log.error(
321
+ { err, signalId: signal.signalId },
322
+ "onConversationCreated callback failed — continuing broadcast",
323
+ );
324
+ }
325
+ }
326
+ }
306
327
  }
307
328
  }
308
329
 
@@ -354,8 +375,21 @@ export class NotificationBroadcaster {
354
375
  const adapterResult = await adapter.send(payload, destination);
355
376
 
356
377
  if (adapterResult.success) {
378
+ // Prefer the channel-native id the adapter just captured (e.g.
379
+ // Slack `ts`) so later edits can target the same message; fall
380
+ // back to the pairing-supplied id for channels that surface it
381
+ // through conversation pairing instead.
382
+ const resolvedMessageId =
383
+ adapterResult.messageId ?? pairing.messageId ?? undefined;
357
384
  if (hasPersistedDecision) {
358
- updateDeliveryStatus(deliveryId, "sent");
385
+ updateDeliveryStatus(
386
+ deliveryId,
387
+ "sent",
388
+ undefined,
389
+ adapterResult.messageId
390
+ ? { messageId: adapterResult.messageId }
391
+ : undefined,
392
+ );
359
393
  }
360
394
  results.push({
361
395
  channel,
@@ -363,7 +397,7 @@ export class NotificationBroadcaster {
363
397
  status: "sent",
364
398
  sentAt: Date.now(),
365
399
  conversationId: pairing.conversationId ?? undefined,
366
- messageId: pairing.messageId ?? undefined,
400
+ messageId: resolvedMessageId,
367
401
  conversationStrategy: pairing.strategy,
368
402
  });
369
403
  } else {
@@ -414,3 +448,20 @@ export class NotificationBroadcaster {
414
448
  return results;
415
449
  }
416
450
  }
451
+
452
+ /**
453
+ * Resolve a signal's `sourceContextId` to a conversation id if it points at a
454
+ * real row. Producers may pass sentinels (job IDs, call session IDs,
455
+ * `access-req-*` strings) here; those simply return undefined so the deep
456
+ * link omits the conversation target.
457
+ */
458
+ function resolveSourceConversationId(
459
+ sourceContextId: string | undefined,
460
+ ): string | undefined {
461
+ if (!sourceContextId) return undefined;
462
+ try {
463
+ return getConversation(sourceContextId) ? sourceContextId : undefined;
464
+ } catch {
465
+ return undefined;
466
+ }
467
+ }
@@ -113,6 +113,29 @@ export async function pairDeliveryWithConversation(
113
113
  };
114
114
  }
115
115
 
116
+ const conversationAction = options?.conversationAction;
117
+ const bindingContext = options?.bindingContext;
118
+
119
+ // Passive vellum notifications surface via the home feed alone and link
120
+ // back to the originating conversation via `signal.sourceContextId`.
121
+ // Materializing a fresh per-notification conversation just to host the
122
+ // seed message leaves a graveyard entry in the sidebar; skip it unless
123
+ // the producer opted in via `requiresConversation` or the decision engine
124
+ // requested explicit reuse of a target conversation.
125
+ if (
126
+ strategy === "start_new_conversation" &&
127
+ !signal.requiresConversation &&
128
+ conversationAction?.action !== "reuse_existing"
129
+ ) {
130
+ return {
131
+ conversationId: null,
132
+ messageId: null,
133
+ strategy,
134
+ createdNewConversation: false,
135
+ conversationFallbackUsed: false,
136
+ };
137
+ }
138
+
116
139
  const title =
117
140
  copy.conversationTitle ?? copy.title ?? signal.sourceEventName;
118
141
 
@@ -130,9 +153,6 @@ export async function pairDeliveryWithConversation(
130
153
  ? copy.conversationSeedMessage
131
154
  : composeConversationSeed(signal, channel, copy);
132
155
 
133
- const conversationAction = options?.conversationAction;
134
- const bindingContext = options?.bindingContext;
135
-
136
156
  // Attempt to reuse an existing conversation when the model requests it
137
157
  if (conversationAction?.action === "reuse_existing") {
138
158
  const targetId = conversationAction.conversationId;
@@ -7,7 +7,7 @@
7
7
  * were routed.
8
8
  */
9
9
 
10
- import { eq } from "drizzle-orm";
10
+ import { desc, eq } from "drizzle-orm";
11
11
 
12
12
  import { getDb } from "../memory/db-connection.js";
13
13
  import { notificationDecisions } from "../memory/schema.js";
@@ -94,3 +94,34 @@ export function updateDecision(id: string, params: UpdateDecisionParams): void {
94
94
  .where(eq(notificationDecisions.id, id))
95
95
  .run();
96
96
  }
97
+
98
+ /**
99
+ * Return the most recent decision for a given notification event, or
100
+ * `undefined` if none exists. The decision engine writes exactly one
101
+ * decision per event today, but ordering by createdAt DESC keeps this
102
+ * stable if that ever changes (e.g. re-decisions on retry).
103
+ */
104
+ export function findLatestDecisionByEventId(
105
+ eventId: string,
106
+ ): NotificationDecisionRow | undefined {
107
+ const db = getDb();
108
+ const row = db
109
+ .select()
110
+ .from(notificationDecisions)
111
+ .where(eq(notificationDecisions.notificationEventId, eventId))
112
+ .orderBy(desc(notificationDecisions.createdAt))
113
+ .get();
114
+ if (!row) return undefined;
115
+ return {
116
+ id: row.id,
117
+ notificationEventId: row.notificationEventId,
118
+ shouldNotify: row.shouldNotify === 1,
119
+ selectedChannels: row.selectedChannels,
120
+ reasoningSummary: row.reasoningSummary,
121
+ confidence: row.confidence,
122
+ fallbackUsed: row.fallbackUsed === 1,
123
+ promptVersion: row.promptVersion,
124
+ validationResults: row.validationResults,
125
+ createdAt: row.createdAt,
126
+ };
127
+ }
@@ -137,6 +137,7 @@ export function updateDeliveryStatus(
137
137
  id: string,
138
138
  status: NotificationDeliveryStatus,
139
139
  error?: { code?: string; message?: string },
140
+ patch?: { messageId?: string },
140
141
  ): boolean {
141
142
  const db = getDb();
142
143
  const now = Date.now();
@@ -151,6 +152,9 @@ export function updateDeliveryStatus(
151
152
  if (error?.message) {
152
153
  updates.errorMessage = error.message;
153
154
  }
155
+ if (patch?.messageId !== undefined) {
156
+ updates.messageId = patch.messageId;
157
+ }
154
158
 
155
159
  db.update(notificationDeliveries)
156
160
  .set(updates)
@@ -160,6 +164,34 @@ export function updateDeliveryStatus(
160
164
  return rawChanges() > 0;
161
165
  }
162
166
 
167
+ /**
168
+ * Update the rendered copy on an existing delivery row.
169
+ *
170
+ * Used by the edit pipeline so the delivery audit reflects the latest
171
+ * title/body shown to the user, not just the original send.
172
+ */
173
+ export function updateDeliveryRenderedCopy(
174
+ id: string,
175
+ patch: { renderedTitle?: string; renderedBody?: string },
176
+ ): boolean {
177
+ const updates: Record<string, unknown> = { updatedAt: Date.now() };
178
+ if (patch.renderedTitle !== undefined) {
179
+ updates.renderedTitle = patch.renderedTitle;
180
+ }
181
+ if (patch.renderedBody !== undefined) {
182
+ updates.renderedBody = patch.renderedBody;
183
+ }
184
+ if (Object.keys(updates).length === 1) return false;
185
+
186
+ getDb()
187
+ .update(notificationDeliveries)
188
+ .set(updates)
189
+ .where(eq(notificationDeliveries.id, id))
190
+ .run();
191
+
192
+ return rawChanges() > 0;
193
+ }
194
+
163
195
  /** Check whether a delivery already exists for a given decision+channel pair. */
164
196
  export function findDeliveryByDecisionAndChannel(
165
197
  decisionId: string,
@@ -178,3 +210,16 @@ export function findDeliveryByDecisionAndChannel(
178
210
  .get();
179
211
  return row ? rowToDelivery(row) : undefined;
180
212
  }
213
+
214
+ /** Return every delivery row for a given decision. */
215
+ export function findDeliveriesByDecisionId(
216
+ decisionId: string,
217
+ ): NotificationDeliveryRow[] {
218
+ const db = getDb();
219
+ const rows = db
220
+ .select()
221
+ .from(notificationDeliveries)
222
+ .where(eq(notificationDeliveries.notificationDecisionId, decisionId))
223
+ .all();
224
+ return rows.map(rowToDelivery);
225
+ }