@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -88,6 +88,7 @@ function getBroadcaster(): NotificationBroadcaster {
88
88
  targetGuardianPrincipalId: info.targetGuardianPrincipalId,
89
89
  groupId: info.groupId,
90
90
  source: info.source,
91
+ silent: info.silent,
91
92
  });
92
93
  log.info(
93
94
  {
@@ -387,10 +388,18 @@ export async function emitNotificationSignal<TEventName extends string>(
387
388
  // Step 5: Mirror background-origin signals into the home activity feed.
388
389
  // The helper itself decides whether to write (background filter); we
389
390
  // catch and log so a feed-write failure cannot poison the dispatch result.
391
+ // Pass the paired vellum delivery conversation as a fallback so producers
392
+ // whose `sourceContextId` is a sentinel string (e.g. heartbeat startup,
393
+ // credential health, watcher emits, scheduler retries-exhausted) still
394
+ // get a "Go to Convo" button — pointing at the conversation the
395
+ // broadcaster paired the notification with.
396
+ const pairedVellumConversationId = dispatchResult.deliveryResults.find(
397
+ (r) => r.channel === "vellum",
398
+ )?.conversationId;
390
399
  await writeHomeFeedItemForSignal(
391
400
  signal,
392
401
  decision,
393
- dispatchResult.deliveryResults,
402
+ pairedVellumConversationId,
394
403
  ).catch((err) => {
395
404
  log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
396
405
  });
@@ -22,10 +22,7 @@ import { getConversation } from "../memory/conversation-crud.js";
22
22
  import { isBackgroundConversationType } from "../memory/conversation-types.js";
23
23
  import { getLogger } from "../util/logger.js";
24
24
  import type { NotificationSignal } from "./signal.js";
25
- import type {
26
- NotificationDecision,
27
- NotificationDeliveryResult,
28
- } from "./types.js";
25
+ import type { NotificationDecision, RenderedChannelCopy } from "./types.js";
29
26
 
30
27
  const log = getLogger("home-feed-side-effect");
31
28
 
@@ -40,6 +37,16 @@ const FEED_ITEM_URGENCIES: ReadonlySet<string> = new Set<FeedItemUrgency>([
40
37
  * Append a `FeedItem` for the given notification signal when the
41
38
  * filter criteria pass.
42
39
  *
40
+ * `fallbackConversationId` is used as the feed item's "Go to Convo"
41
+ * navigation target when `signal.sourceContextId` doesn't resolve to a
42
+ * real conversation row. The notification broadcaster pairs the vellum
43
+ * delivery with a conversation (newly created or reused) before this
44
+ * function runs, so callers can thread that paired id through here for
45
+ * producers whose `sourceContextId` is a sentinel (heartbeat startup,
46
+ * credential health, watcher emits, scheduler retries-exhausted) — the
47
+ * feed item will then carry the paired delivery conversation and the
48
+ * "Go to Convo" button can render.
49
+ *
43
50
  * Returns the persisted `FeedItem`, or `null` if the signal does not
44
51
  * qualify for home-feed mirroring (non-background origin AND no
45
52
  * `isAsyncBackground` hint) or if schema validation fails.
@@ -47,17 +54,39 @@ const FEED_ITEM_URGENCIES: ReadonlySet<string> = new Set<FeedItemUrgency>([
47
54
  export async function writeHomeFeedItemForSignal(
48
55
  signal: NotificationSignal,
49
56
  decision: NotificationDecision,
50
- deliveryResults: NotificationDeliveryResult[],
57
+ fallbackConversationId?: string,
51
58
  ): Promise<FeedItem | null> {
52
- if (!shouldMirrorToHomeFeed(signal)) return null;
59
+ const { mirror, sourceConversationId } = resolveHomeFeedMirror(
60
+ signal,
61
+ fallbackConversationId,
62
+ );
63
+ if (!mirror) return null;
53
64
 
54
- const renderedCopy = decision.renderedCopy.vellum;
55
- const payloadTitle = readPayloadString(signal.contextPayload, "title");
56
- const payloadBody = readPayloadString(signal.contextPayload, "body");
65
+ const renderedCopy =
66
+ decision.renderedCopy.vellum ??
67
+ firstSelectedRenderedCopy(decision.renderedCopy, decision.selectedChannels);
68
+ const payloadTitle =
69
+ readPayloadString(signal.contextPayload, "title") ??
70
+ readPayloadString(signal.contextPayload, "requestedTitle");
71
+ const payloadBody =
72
+ readPayloadString(signal.contextPayload, "body") ??
73
+ readPayloadString(signal.contextPayload, "requestedMessage");
74
+
75
+ // Source the title from the payload only. The LLM's `renderedCopy.title`
76
+ // often echoes the body when no explicit title was passed, which stutters
77
+ // against `summary` in the row. Leave undefined when absent; renderers
78
+ // fall back to `summary`.
79
+ const resolvedTitle = payloadTitle?.trim() || undefined;
80
+ const resolvedSummary =
81
+ renderedCopy?.body?.trim() || payloadBody?.trim() || "";
82
+ if (!resolvedSummary) {
83
+ log.warn(
84
+ { signalId: signal.signalId, sourceEventName: signal.sourceEventName },
85
+ "Home-feed write skipped: no summary available (would have fallen back to event name)",
86
+ );
87
+ return null;
88
+ }
57
89
 
58
- const conversationId = deliveryResults.find(
59
- (r) => r.channel === "vellum",
60
- )?.conversationId;
61
90
  const urgency = FEED_ITEM_URGENCIES.has(signal.attentionHints.urgency)
62
91
  ? (signal.attentionHints.urgency as FeedItemUrgency)
63
92
  : undefined;
@@ -76,14 +105,16 @@ export async function writeHomeFeedItemForSignal(
76
105
  id: `notif:${signal.signalId}`,
77
106
  type: "notification",
78
107
  priority: 50,
79
- title: renderedCopy?.title ?? payloadTitle ?? signal.sourceEventName,
80
- summary: renderedCopy?.body ?? payloadBody ?? signal.sourceEventName,
108
+ ...(resolvedTitle ? { title: resolvedTitle } : {}),
109
+ summary: resolvedSummary,
81
110
  timestamp: now,
82
111
  createdAt: now,
83
112
  status: "new",
84
113
  category,
114
+ noteworthy: deriveNoteworthy(signal),
115
+ fromAssistant: signal.sourceChannel === "assistant_tool",
85
116
  ...(urgency ? { urgency } : {}),
86
- ...(conversationId ? { conversationId } : {}),
117
+ ...(sourceConversationId ? { conversationId: sourceConversationId } : {}),
87
118
  ...(panelKind ? { detailPanel: { kind: panelKind } } : {}),
88
119
  ...(metadata ? { metadata } : {}),
89
120
  };
@@ -108,7 +139,6 @@ const EVENT_CATEGORY_MAP: Record<string, FeedItemCategory> = {
108
139
  "credential.health_alert": "security",
109
140
  "activity.failed": "background",
110
141
  "activity.complete": "background",
111
- "heartbeat.alert": "system",
112
142
  "watcher.notification": "system",
113
143
  "schedule.notify": "scheduling",
114
144
  "guardian.question": "security",
@@ -143,19 +173,51 @@ function deriveDetailPanelKind(
143
173
  }
144
174
 
145
175
  /**
146
- * `sourceContextId` is best-effort it may not be a conversation id
147
- * (e.g. scheduler job id, watcher event id), so a lookup failure
148
- * falls through to "not a background conversation" rather than throwing.
176
+ * The lookup is best-effort and unified: a single `getConversation` call
177
+ * both gates the "background conversation" mirror branch and populates
178
+ * `sourceConversationId` for the "Go to Thread" navigation target. Misses
179
+ * (scheduler job ids, watcher event ids, CLI tool-call ids) leave
180
+ * `sourceConversationId` undefined so the client hides the affordance.
181
+ *
182
+ * `assistant_tool` mirrors unconditionally because the documented
183
+ * `notifications send` skill (and background-job failure emits) deliberately
184
+ * does not require a background-typed conversation or the
185
+ * `isAsyncBackground` hint.
149
186
  */
150
- function shouldMirrorToHomeFeed(signal: NotificationSignal): boolean {
151
- if (signal.attentionHints.isAsyncBackground) return true;
152
- if (!signal.sourceContextId) return false;
153
- try {
154
- const row = getConversation(signal.sourceContextId);
155
- return isBackgroundConversationType(row?.conversationType);
156
- } catch {
157
- return false;
187
+ function resolveHomeFeedMirror(
188
+ signal: NotificationSignal,
189
+ fallbackConversationId?: string,
190
+ ): {
191
+ mirror: boolean;
192
+ sourceConversationId?: string;
193
+ } {
194
+ let sourceRow: { conversationType?: string } | null = null;
195
+ if (signal.sourceContextId) {
196
+ try {
197
+ sourceRow = getConversation(signal.sourceContextId) ?? null;
198
+ } catch {
199
+ sourceRow = null;
200
+ }
201
+ }
202
+ // Prefer the producer's source context (e.g. the heartbeat / background
203
+ // job conversation that emitted the signal) for the "Go to Convo" target,
204
+ // since that's where the work actually happened. Fall back to the paired
205
+ // delivery conversation only when the source context didn't resolve —
206
+ // covers producers whose `sourceContextId` is a sentinel string.
207
+ const sourceConversationId = sourceRow
208
+ ? signal.sourceContextId
209
+ : fallbackConversationId;
210
+
211
+ if (signal.sourceChannel === "assistant_tool") {
212
+ return { mirror: true, sourceConversationId };
213
+ }
214
+ if (signal.attentionHints.isAsyncBackground) {
215
+ return { mirror: true, sourceConversationId };
158
216
  }
217
+ if (isBackgroundConversationType(sourceRow?.conversationType)) {
218
+ return { mirror: true, sourceConversationId };
219
+ }
220
+ return { mirror: false };
159
221
  }
160
222
 
161
223
  function readPayloadString(payload: unknown, key: string): string | undefined {
@@ -163,3 +225,50 @@ function readPayloadString(payload: unknown, key: string): string | undefined {
163
225
  const value = (payload as Record<string, unknown>)[key];
164
226
  return typeof value === "string" ? value : undefined;
165
227
  }
228
+
229
+ /**
230
+ * Routing-intent enforcement can prune `selectedChannels` without also
231
+ * pruning `renderedCopy`, so iterating `renderedCopy` directly risks
232
+ * surfacing copy for a channel that was never delivered. Walk
233
+ * `selectedChannels` in order instead so the channel that actually shipped
234
+ * wins.
235
+ */
236
+ function firstSelectedRenderedCopy(
237
+ renderedCopy: NotificationDecision["renderedCopy"],
238
+ selectedChannels: NotificationDecision["selectedChannels"],
239
+ ): RenderedChannelCopy | undefined {
240
+ for (const channel of selectedChannels) {
241
+ const copy = renderedCopy[channel];
242
+ if (copy && (copy.title?.trim() || copy.body?.trim())) return copy;
243
+ }
244
+ return undefined;
245
+ }
246
+
247
+ // ── Noteworthy derivation ─────────────────────────────────────────────
248
+ //
249
+ // Clients split the feed into inbox-style (noteworthy) and activity-style
250
+ // (routine) surfaces. Assistant-initiated shares and a small allow-list of
251
+ // high-importance system events land in the inbox; routine background
252
+ // signals stay in activity.
253
+
254
+ const NOTEWORTHY_EVENT_NAMES: ReadonlySet<string> = new Set([
255
+ "guardian.question",
256
+ "guardian.channel_activation",
257
+ "ingress.access_request",
258
+ "ingress.escalation",
259
+ "credential.health_alert",
260
+ ]);
261
+
262
+ function deriveNoteworthy(signal: NotificationSignal): boolean {
263
+ // Background-job failures emit with `sourceChannel: "assistant_tool"`
264
+ // (see `runtime/background-job-runner.ts`), so the activity.failed rule
265
+ // must run BEFORE the assistant_tool short-circuit — otherwise every
266
+ // routine watcher/heartbeat failure would land in the Inbox instead of
267
+ // staying in the activity feed.
268
+ if (signal.sourceEventName === "activity.failed") {
269
+ return signal.attentionHints.urgency === "critical";
270
+ }
271
+ if (signal.sourceChannel === "assistant_tool") return true;
272
+ if (NOTEWORTHY_EVENT_NAMES.has(signal.sourceEventName)) return true;
273
+ return false;
274
+ }
@@ -107,10 +107,6 @@ export const NOTIFICATION_SOURCE_EVENT_NAMES = [
107
107
  description:
108
108
  "OAuth credential health issue detected (expired, revoked, missing scopes)",
109
109
  },
110
- {
111
- id: "heartbeat.alert",
112
- description: "Heartbeat found something worth surfacing to the guardian",
113
- },
114
110
  ] as const;
115
111
 
116
112
  export type NotificationSourceEventName =
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ChannelPolicies } from "../channels/config.js";
9
9
  import type { ChannelId } from "../channels/types.js";
10
+ import type { AttentionHints } from "./signal.js";
10
11
 
11
12
  /**
12
13
  * Derived from the channel policy registry: only channels whose
@@ -81,6 +82,13 @@ export interface ChannelDeliveryPayload {
81
82
  deepLinkTarget?: Record<string, unknown>;
82
83
  /** Original signal context payload — available for channel-specific structured rendering. */
83
84
  contextPayload?: Record<string, unknown>;
85
+ /**
86
+ * Forwarded from the originating signal so adapters can make
87
+ * urgency-aware decisions (e.g. the vellum adapter suppresses the OS
88
+ * banner for non-urgent intents while still emitting the conversation
89
+ * pairing side effects).
90
+ */
91
+ urgency: AttentionHints["urgency"];
84
92
  }
85
93
 
86
94
  /** Interface that each channel adapter must implement. */
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { emitPostConnectNudge } from "../home/post-connect-feed.js";
23
+ import { invalidateAssistantSuggestedPromptsCache } from "../home/suggested-prompts.js";
23
24
  import type { TokenEndpointAuthMethod } from "../security/oauth2.js";
24
25
  import { prepareOAuth2Flow, startOAuth2Flow } from "../security/oauth2.js";
25
26
  import { getLogger } from "../util/logger.js";
@@ -252,6 +253,7 @@ export async function orchestrateOAuthConnect(
252
253
  },
253
254
  "Deferred OAuth2 flow completed — tokens stored",
254
255
  );
256
+ invalidateAssistantSuggestedPromptsCache();
255
257
  void emitPostConnectNudge(options.service);
256
258
  options.onDeferredComplete?.({
257
259
  success: true,
@@ -375,6 +377,7 @@ export async function orchestrateOAuthConnect(
375
377
  "orchestrateOAuthConnect: tokens stored, connect complete",
376
378
  );
377
379
 
380
+ invalidateAssistantSuggestedPromptsCache();
378
381
  void emitPostConnectNudge(options.service);
379
382
 
380
383
  return {
@@ -49,6 +49,8 @@ function manualTokenAccessCredentialKey(provider: string): string | null {
49
49
  return credentialKey("slack_channel", "bot_token");
50
50
  case "telegram":
51
51
  return credentialKey("telegram", "bot_token");
52
+ case "sanity":
53
+ return credentialKey("sanity", "api_token");
52
54
  default:
53
55
  return null;
54
56
  }
@@ -122,6 +122,24 @@ export async function syncManualTokenConnection(
122
122
  return;
123
123
  }
124
124
 
125
+ case "sanity": {
126
+ const tokenResult = await getSecureKeyResultAsync(
127
+ credentialKey("sanity", "api_token"),
128
+ );
129
+ if (tokenResult.unreachable) {
130
+ log.warn(
131
+ "Skipping sanity manual-token reconciliation — credential backend unreachable",
132
+ );
133
+ return;
134
+ }
135
+ if (tokenResult.value) {
136
+ await ensureManualTokenConnection(provider, accountInfo);
137
+ } else {
138
+ removeManualTokenConnection(provider);
139
+ }
140
+ return;
141
+ }
142
+
125
143
  default:
126
144
  return;
127
145
  }
@@ -142,4 +160,5 @@ export async function syncManualTokenConnection(
142
160
  export async function backfillManualTokenConnections(): Promise<void> {
143
161
  await syncManualTokenConnection("telegram");
144
162
  await syncManualTokenConnection("slack_channel");
163
+ await syncManualTokenConnection("sanity");
145
164
  }
@@ -1100,5 +1100,17 @@ export async function disconnectOAuthProvider(
1100
1100
 
1101
1101
  deleteConnection(conn.id);
1102
1102
 
1103
+ // Dynamic import: `suggested-prompts.ts` imports from this module, so a
1104
+ // static import here would create a cycle. The cache invalidation is
1105
+ // best-effort — failures must not block disconnect.
1106
+ void import("../home/suggested-prompts.js")
1107
+ .then((m) => m.invalidateAssistantSuggestedPromptsCache())
1108
+ .catch((err) => {
1109
+ log.warn(
1110
+ { err: err instanceof Error ? err.message : String(err) },
1111
+ "Failed to invalidate suggested-prompts cache after disconnect",
1112
+ );
1113
+ });
1114
+
1103
1115
  return "disconnected";
1104
1116
  }
@@ -261,17 +261,57 @@ describe("PlatformOAuthConnection", () => {
261
261
  ).rejects.toThrow(CredentialRequiredError);
262
262
  });
263
263
 
264
- test("502 response throws ProviderUnreachableError", async () => {
264
+ test("502 response retries then throws ProviderUnreachableError", async () => {
265
+ let callCount = 0;
266
+ const client = makeMockClient(
267
+ mock(async () => {
268
+ callCount++;
269
+ return new Response("", { status: 502 });
270
+ }) as unknown as typeof globalThis.fetch,
271
+ );
272
+
273
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
274
+ await expect(
275
+ conn.request({ method: "GET", path: "/test" }),
276
+ ).rejects.toThrow(ProviderUnreachableError);
277
+ // 1 initial + 3 retries = 4 total attempts
278
+ expect(callCount).toBe(4);
279
+ });
280
+
281
+ test("502 response includes detail from response body", async () => {
265
282
  const client = makeMockClient(
266
283
  mock(
267
- async () => new Response("", { status: 502 }),
284
+ async () => new Response("upstream timeout after 30s", { status: 502 }),
268
285
  ) as unknown as typeof globalThis.fetch,
269
286
  );
270
287
 
271
288
  const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
272
289
  await expect(
273
290
  conn.request({ method: "GET", path: "/test" }),
274
- ).rejects.toThrow(ProviderUnreachableError);
291
+ ).rejects.toThrow(/upstream timeout after 30s/);
292
+ });
293
+
294
+ test("502 recovers on retry", async () => {
295
+ let callCount = 0;
296
+ const client = makeMockClient(
297
+ mock(async () => {
298
+ callCount++;
299
+ if (callCount <= 2) {
300
+ return new Response("", { status: 502 });
301
+ }
302
+ return new Response(
303
+ JSON.stringify({ status: 200, headers: {}, body: { ok: true } }),
304
+ { status: 200 },
305
+ );
306
+ }) as unknown as typeof globalThis.fetch,
307
+ );
308
+
309
+ const conn = new PlatformOAuthConnection({ ...DEFAULT_OPTIONS, client });
310
+ const result = await conn.request({ method: "GET", path: "/test" });
311
+
312
+ expect(result.status).toBe(200);
313
+ expect(result.body).toEqual({ ok: true });
314
+ expect(callCount).toBe(3);
275
315
  });
276
316
 
277
317
  test("withToken throws clear error", async () => {
@@ -1,5 +1,6 @@
1
1
  import type { VellumPlatformClient } from "../platform/client.js";
2
2
  import { BackendError } from "../util/errors.js";
3
+ import { getLogger } from "../util/logger.js";
3
4
  import { getHttpRetryDelay, isRetryableStatus, sleep } from "../util/retry.js";
4
5
  import type {
5
6
  OAuthConnection,
@@ -7,6 +8,7 @@ import type {
7
8
  OAuthConnectionResponse,
8
9
  } from "./connection.js";
9
10
 
11
+ const log = getLogger("platform-oauth-connection");
10
12
  const MAX_RETRIES = 3;
11
13
 
12
14
  export class CredentialRequiredError extends BackendError {
@@ -111,19 +113,26 @@ export class PlatformOAuthConnection implements OAuthConnection {
111
113
  throw new CredentialRequiredError();
112
114
  }
113
115
 
114
- if (response.status === 502) {
115
- throw new ProviderUnreachableError();
116
- }
117
-
118
116
  if (
119
117
  !response.ok &&
120
118
  isRetryableStatus(response.status) &&
121
119
  attempt < MAX_RETRIES
122
120
  ) {
121
+ log.warn(
122
+ { status: response.status, attempt, provider: "platform-proxy" },
123
+ `Retryable status ${response.status} from platform proxy (attempt ${attempt + 1}/${MAX_RETRIES + 1})`,
124
+ );
123
125
  await sleep(getHttpRetryDelay(response, attempt));
124
126
  continue;
125
127
  }
126
128
 
129
+ if (response.status === 502) {
130
+ const detail = await response.text().catch(() => "");
131
+ throw new ProviderUnreachableError(
132
+ `The external service provider is temporarily unreachable (HTTP 502).${detail ? ` Detail: ${detail}` : ""} This may be a transient issue — retry after a brief pause.`,
133
+ );
134
+ }
135
+
127
136
  if (!response.ok) {
128
137
  throw new BackendError(
129
138
  `Platform proxy returned unexpected status ${response.status}`,
@@ -746,6 +746,28 @@ export const PROVIDER_SEED_DATA: Record<
746
746
  logoUrl: "https://cdn.simpleicons.org/telegram",
747
747
  defaultScopes: [],
748
748
  },
749
+
750
+ sanity: {
751
+ provider: "sanity",
752
+ authorizeUrl: "urn:manual-token",
753
+ tokenExchangeUrl: "urn:manual-token",
754
+ baseUrl: "https://api.sanity.io",
755
+ displayLabel: "Sanity",
756
+ description: "Content management platform",
757
+ dashboardUrl: "https://www.sanity.io/manage",
758
+ clientIdPlaceholder: null,
759
+ requiresClientSecret: false,
760
+ logoUrl: "https://cdn.simpleicons.org/sanity",
761
+ defaultScopes: [],
762
+ injectionTemplates: [
763
+ {
764
+ hostPattern: "*.sanity.io",
765
+ injectionType: "header",
766
+ headerName: "Authorization",
767
+ valuePrefix: "Bearer ",
768
+ },
769
+ ],
770
+ },
749
771
  };
750
772
 
751
773
  export const SEEDED_PROVIDER_KEYS = new Set(Object.keys(PROVIDER_SEED_DATA));
@@ -196,7 +196,10 @@ export class PermissionPrompter {
196
196
  }
197
197
  // The prompter owns deregistration; all callers use get() to peek before
198
198
  // routing to resolveConfirmation, which fires the rpcResolve callback.
199
- const interaction = pendingInteractions.resolve(requestId);
199
+ const interaction = pendingInteractions.resolve(
200
+ requestId,
201
+ decision === "allow" ? "approved" : "rejected",
202
+ );
200
203
  this.ownedIds.delete(requestId);
201
204
  (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
202
205
  { decision, selectedPattern, selectedScope, decisionContext },
@@ -210,7 +213,7 @@ export class PermissionPrompter {
210
213
  */
211
214
  denyAllPending(): void {
212
215
  for (const requestId of [...this.ownedIds]) {
213
- const interaction = pendingInteractions.resolve(requestId);
216
+ const interaction = pendingInteractions.resolve(requestId, "superseded");
214
217
  this.ownedIds.delete(requestId);
215
218
  (interaction?.rpcResolve as ((v: ConfirmResult) => void) | undefined)?.(
216
219
  {
@@ -130,7 +130,10 @@ export class SecretPrompter {
130
130
  }
131
131
  // approval-routes calls pendingInteractions.get() before routing here;
132
132
  // the prompter owns deregistration so it fires the Promise callback cleanly.
133
- const interaction = pendingInteractions.resolve(requestId);
133
+ const interaction = pendingInteractions.resolve(
134
+ requestId,
135
+ value === undefined ? "cancelled" : "answered",
136
+ );
134
137
  this.ownedIds.delete(requestId);
135
138
  (interaction?.rpcResolve as ((v: SecretPromptResult) => void) | undefined)?.(
136
139
  { value: value ?? null, delivery: delivery ?? "store" },