@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
@@ -0,0 +1,307 @@
1
+ /**
2
+ * OAuth2 Device Authorization Grant (RFC 8628).
3
+ *
4
+ * Implements the device-code flow for environments where a browser redirect
5
+ * is impractical (CLI, headless). The user visits a verification URI and
6
+ * enters a short code; meanwhile, the client polls the token endpoint until
7
+ * authorization completes.
8
+ *
9
+ * This is intentionally separate from the PKCE authorization code flow in
10
+ * oauth2.ts — different grant type, different UX (no localhost server), and
11
+ * different polling lifecycle.
12
+ */
13
+
14
+ import { getLogger } from "../util/logger.js";
15
+
16
+ const log = getLogger("oauth2-device-code");
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export interface DeviceCodeConfig {
23
+ deviceCodeUrl: string;
24
+ tokenUrl: string;
25
+ clientId: string;
26
+ scopes: string[];
27
+ audience?: string;
28
+ }
29
+
30
+ export interface DeviceCodeInitResult {
31
+ deviceCode: string;
32
+ userCode: string;
33
+ verificationUri: string;
34
+ verificationUriComplete?: string;
35
+ expiresIn: number;
36
+ interval: number;
37
+ }
38
+
39
+ export interface DeviceCodeTokenResult {
40
+ accessToken: string;
41
+ refreshToken?: string;
42
+ expiresIn?: number;
43
+ tokenType?: string;
44
+ scope?: string;
45
+ }
46
+
47
+ export class DeviceCodeError extends Error {
48
+ constructor(
49
+ message: string,
50
+ public readonly code:
51
+ | "expired_token"
52
+ | "access_denied"
53
+ | "request_failed"
54
+ | "aborted",
55
+ ) {
56
+ super(message);
57
+ this.name = "DeviceCodeError";
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Well-known provider configs
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export const OPENAI_DEVICE_CODE_CONFIG: DeviceCodeConfig = {
66
+ deviceCodeUrl: "https://auth.openai.com/oauth/device/code",
67
+ tokenUrl: "https://auth.openai.com/oauth/token",
68
+ clientId: "app_EMoamEEZ73f0CkXaXp7hrann",
69
+ scopes: ["openid", "profile", "email", "offline_access"],
70
+ audience: "https://chatgpt.com",
71
+ };
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Device code request
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export async function requestDeviceCode(
78
+ config: DeviceCodeConfig,
79
+ ): Promise<DeviceCodeInitResult> {
80
+ const body: Record<string, string> = {
81
+ client_id: config.clientId,
82
+ scope: config.scopes.join(" "),
83
+ };
84
+ if (config.audience) {
85
+ body.audience = config.audience;
86
+ }
87
+
88
+ const resp = await fetch(config.deviceCodeUrl, {
89
+ method: "POST",
90
+ headers: {
91
+ "Content-Type": "application/x-www-form-urlencoded",
92
+ Accept: "application/json",
93
+ },
94
+ body: new URLSearchParams(body),
95
+ });
96
+
97
+ if (!resp.ok) {
98
+ const rawBody = await resp.text().catch(() => "");
99
+ log.error(
100
+ { status: resp.status, body: rawBody },
101
+ "Device code request failed",
102
+ );
103
+ throw new DeviceCodeError(
104
+ `Device code request failed (HTTP ${resp.status})`,
105
+ "request_failed",
106
+ );
107
+ }
108
+
109
+ const data = (await resp.json()) as Record<string, unknown>;
110
+
111
+ return {
112
+ deviceCode: data.device_code as string,
113
+ userCode: data.user_code as string,
114
+ verificationUri: data.verification_uri as string,
115
+ verificationUriComplete: data.verification_uri_complete as
116
+ | string
117
+ | undefined,
118
+ expiresIn: data.expires_in as number,
119
+ interval: (data.interval as number | undefined) ?? 5,
120
+ };
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Token polling
125
+ // ---------------------------------------------------------------------------
126
+
127
+ /**
128
+ * Poll the token endpoint until the user completes authorization or the
129
+ * device code expires.
130
+ *
131
+ * Handles RFC 8628 error codes:
132
+ * - `authorization_pending` — keep polling
133
+ * - `slow_down` — increase interval by 5 seconds (per spec)
134
+ * - `expired_token` — abort with error
135
+ * - `access_denied` — abort with error
136
+ */
137
+ export async function pollForToken(
138
+ config: DeviceCodeConfig,
139
+ deviceCode: string,
140
+ intervalSeconds: number,
141
+ expiresIn: number,
142
+ signal?: AbortSignal,
143
+ /** @internal Test-only: override the sleep function to avoid real delays. */
144
+ _sleepFn?: (ms: number, signal?: AbortSignal) => Promise<void>,
145
+ ): Promise<DeviceCodeTokenResult> {
146
+ const doSleep = _sleepFn ?? sleep;
147
+ let interval = intervalSeconds;
148
+ const deadline = Date.now() + expiresIn * 1000;
149
+
150
+ while (Date.now() < deadline) {
151
+ if (signal?.aborted) {
152
+ throw new DeviceCodeError("Device code flow aborted", "aborted");
153
+ }
154
+
155
+ await doSleep(interval * 1000, signal);
156
+
157
+ if (signal?.aborted) {
158
+ throw new DeviceCodeError("Device code flow aborted", "aborted");
159
+ }
160
+
161
+ const body = new URLSearchParams({
162
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
163
+ device_code: deviceCode,
164
+ client_id: config.clientId,
165
+ });
166
+
167
+ let resp: Response;
168
+ try {
169
+ resp = await fetch(config.tokenUrl, {
170
+ method: "POST",
171
+ headers: {
172
+ "Content-Type": "application/x-www-form-urlencoded",
173
+ Accept: "application/json",
174
+ },
175
+ body,
176
+ signal,
177
+ });
178
+ } catch (err) {
179
+ if (signal?.aborted) {
180
+ throw new DeviceCodeError("Device code flow aborted", "aborted");
181
+ }
182
+ log.warn({ err }, "Token poll request failed, will retry");
183
+ continue;
184
+ }
185
+
186
+ const data = (await resp.json()) as Record<string, unknown>;
187
+
188
+ if (resp.ok) {
189
+ log.info("Device code authorization completed");
190
+ return {
191
+ accessToken: data.access_token as string,
192
+ refreshToken: data.refresh_token as string | undefined,
193
+ expiresIn: data.expires_in as number | undefined,
194
+ tokenType: data.token_type as string | undefined,
195
+ scope: data.scope as string | undefined,
196
+ };
197
+ }
198
+
199
+ const errorCode = data.error as string | undefined;
200
+
201
+ if (errorCode === "authorization_pending") {
202
+ log.debug("Authorization pending, continuing to poll");
203
+ continue;
204
+ }
205
+
206
+ if (errorCode === "slow_down") {
207
+ interval += 5;
208
+ log.info({ newInterval: interval }, "Received slow_down, increasing poll interval");
209
+ continue;
210
+ }
211
+
212
+ if (errorCode === "expired_token") {
213
+ throw new DeviceCodeError(
214
+ "Device code expired before user completed authorization",
215
+ "expired_token",
216
+ );
217
+ }
218
+
219
+ if (errorCode === "access_denied") {
220
+ throw new DeviceCodeError(
221
+ "User denied the authorization request",
222
+ "access_denied",
223
+ );
224
+ }
225
+
226
+ log.error(
227
+ { status: resp.status, error: errorCode },
228
+ "Unexpected token poll error",
229
+ );
230
+ throw new DeviceCodeError(
231
+ `Token poll failed: ${errorCode ?? `HTTP ${resp.status}`}`,
232
+ "request_failed",
233
+ );
234
+ }
235
+
236
+ throw new DeviceCodeError(
237
+ "Device code expired before user completed authorization",
238
+ "expired_token",
239
+ );
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Combined flow
244
+ // ---------------------------------------------------------------------------
245
+
246
+ export interface DeviceCodeFlowResult {
247
+ tokens: DeviceCodeTokenResult;
248
+ init: DeviceCodeInitResult;
249
+ }
250
+
251
+ /**
252
+ * Run the full device-code flow:
253
+ * 1. Request a device code + user code
254
+ * 2. Return the user code and verification URI (caller shows these to the user)
255
+ * 3. Poll for the token
256
+ *
257
+ * The returned `init` contains the user code and verification URI that the
258
+ * caller should present to the user before awaiting `tokens`.
259
+ */
260
+ export async function startDeviceCodeFlow(
261
+ config: DeviceCodeConfig,
262
+ signal?: AbortSignal,
263
+ ): Promise<DeviceCodeFlowResult> {
264
+ const init = await requestDeviceCode(config);
265
+
266
+ log.info(
267
+ {
268
+ verificationUri: init.verificationUri,
269
+ expiresIn: init.expiresIn,
270
+ interval: init.interval,
271
+ },
272
+ "Device code flow started",
273
+ );
274
+
275
+ const tokens = await pollForToken(
276
+ config,
277
+ init.deviceCode,
278
+ init.interval,
279
+ init.expiresIn,
280
+ signal,
281
+ );
282
+
283
+ return { tokens, init };
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Helpers
288
+ // ---------------------------------------------------------------------------
289
+
290
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
291
+ return new Promise((resolve, reject) => {
292
+ if (signal?.aborted) {
293
+ reject(new DeviceCodeError("Device code flow aborted", "aborted"));
294
+ return;
295
+ }
296
+ const timer = setTimeout(resolve, ms);
297
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
298
+ signal?.addEventListener(
299
+ "abort",
300
+ () => {
301
+ clearTimeout(timer);
302
+ reject(new DeviceCodeError("Device code flow aborted", "aborted"));
303
+ },
304
+ { once: true },
305
+ );
306
+ });
307
+ }
@@ -86,6 +86,10 @@ export interface OAuth2FlowOptions {
86
86
  * instead of an OS-assigned random port. Required for providers like Slack that
87
87
  * need pre-registered redirect URIs. */
88
88
  loopbackPort?: number;
89
+ /** Override the loopback callback path. Defaults to `/oauth/callback`.
90
+ * Required for providers with pre-registered redirect URIs that use a
91
+ * different path (e.g. OpenAI Codex uses `/auth/callback`). */
92
+ loopbackCallbackPath?: string;
89
93
  }
90
94
 
91
95
  export interface OAuth2FlowResult {
@@ -98,15 +102,15 @@ export interface OAuth2FlowResult {
98
102
  // PKCE helpers
99
103
  // ---------------------------------------------------------------------------
100
104
 
101
- function generateCodeVerifier(): string {
105
+ export function generateCodeVerifier(): string {
102
106
  return randomBytes(32).toString("base64url");
103
107
  }
104
108
 
105
- function generateCodeChallenge(verifier: string): string {
109
+ export function generateCodeChallenge(verifier: string): string {
106
110
  return createHash("sha256").update(verifier).digest("base64url");
107
111
  }
108
112
 
109
- function generateState(): string {
113
+ export function generateState(): string {
110
114
  return randomBytes(16).toString("hex");
111
115
  }
112
116
 
@@ -114,7 +118,7 @@ function generateState(): string {
114
118
  // Token exchange (shared between transports)
115
119
  // ---------------------------------------------------------------------------
116
120
 
117
- async function exchangeCodeForTokens(
121
+ export async function exchangeCodeForTokens(
118
122
  config: OAuth2Config,
119
123
  code: string,
120
124
  redirectUri: string,
@@ -293,6 +297,7 @@ async function runLoopbackFlow(
293
297
  codeChallenge: string,
294
298
  state: string,
295
299
  loopbackPort?: number,
300
+ callbackPath?: string,
296
301
  ): Promise<OAuth2FlowResult> {
297
302
  const { code, redirectUri } = await startLoopbackServerAndWaitForCode(
298
303
  config,
@@ -300,6 +305,7 @@ async function runLoopbackFlow(
300
305
  codeChallenge,
301
306
  state,
302
307
  loopbackPort,
308
+ callbackPath,
303
309
  );
304
310
 
305
311
  return await exchangeCodeForTokens(config, code, redirectUri, codeVerifier);
@@ -317,7 +323,9 @@ function startLoopbackServerAndWaitForCode(
317
323
  codeChallenge: string,
318
324
  state: string,
319
325
  loopbackPort?: number,
326
+ callbackPath?: string,
320
327
  ): Promise<{ code: string; redirectUri: string }> {
328
+ const effectiveCallbackPath = callbackPath ?? LOOPBACK_CALLBACK_PATH;
321
329
  return new Promise((resolve, reject) => {
322
330
  let settled = false;
323
331
  let boundRedirectUri = "";
@@ -340,7 +348,7 @@ function startLoopbackServerAndWaitForCode(
340
348
 
341
349
  const url = new URL(req.url ?? "/", `http://127.0.0.1`);
342
350
 
343
- if (url.pathname !== LOOPBACK_CALLBACK_PATH) {
351
+ if (url.pathname !== effectiveCallbackPath) {
344
352
  log.info(
345
353
  { pathname: url.pathname },
346
354
  "oauth2 loopback: non-callback path, returning 404",
@@ -426,7 +434,7 @@ function startLoopbackServerAndWaitForCode(
426
434
 
427
435
  server.listen(loopbackPort ?? 0, "localhost", () => {
428
436
  const addr = server.address() as { port: number };
429
- boundRedirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
437
+ boundRedirectUri = `http://localhost:${addr.port}${effectiveCallbackPath}`;
430
438
 
431
439
  log.info(
432
440
  { port: addr.port, redirectUri: boundRedirectUri },
@@ -497,7 +505,11 @@ export async function prepareOAuth2Flow(
497
505
  const transport = options?.callbackTransport ?? "loopback";
498
506
 
499
507
  if (transport === "loopback") {
500
- return prepareLoopbackFlow(config, options?.loopbackPort);
508
+ return prepareLoopbackFlow(
509
+ config,
510
+ options?.loopbackPort,
511
+ options?.loopbackCallbackPath,
512
+ );
501
513
  }
502
514
 
503
515
  // Dynamic imports required here to avoid circular dependencies with
@@ -555,6 +567,7 @@ export async function prepareOAuth2Flow(
555
567
  async function prepareLoopbackFlow(
556
568
  config: OAuth2Config,
557
569
  loopbackPort?: number,
570
+ callbackPath?: string,
558
571
  ): Promise<OAuth2PreparedFlow> {
559
572
  const codeVerifier = generateCodeVerifier();
560
573
  const codeChallenge = generateCodeChallenge(codeVerifier);
@@ -563,6 +576,7 @@ async function prepareLoopbackFlow(
563
576
  const { redirectUri, codePromise } = await startLoopbackServerForPreparedFlow(
564
577
  state,
565
578
  loopbackPort,
579
+ callbackPath,
566
580
  );
567
581
 
568
582
  const authParams = new URLSearchParams({
@@ -599,7 +613,9 @@ async function prepareLoopbackFlow(
599
613
  function startLoopbackServerForPreparedFlow(
600
614
  state: string,
601
615
  loopbackPort?: number,
616
+ callbackPath?: string,
602
617
  ): Promise<{ redirectUri: string; codePromise: Promise<string> }> {
618
+ const effectiveCallbackPath = callbackPath ?? LOOPBACK_CALLBACK_PATH;
603
619
  return new Promise((resolveSetup, rejectSetup) => {
604
620
  let settled = false;
605
621
  let listening = false;
@@ -619,7 +635,7 @@ function startLoopbackServerForPreparedFlow(
619
635
 
620
636
  const url = new URL(req.url ?? "/", `http://127.0.0.1`);
621
637
 
622
- if (url.pathname !== LOOPBACK_CALLBACK_PATH) {
638
+ if (url.pathname !== effectiveCallbackPath) {
623
639
  res.writeHead(404, { "Content-Type": "text/plain" });
624
640
  res.end("Not found");
625
641
  return;
@@ -683,7 +699,7 @@ function startLoopbackServerForPreparedFlow(
683
699
 
684
700
  server.listen(loopbackPort ?? 0, "localhost", () => {
685
701
  const addr = server.address() as { port: number };
686
- const redirectUri = `http://localhost:${addr.port}${LOOPBACK_CALLBACK_PATH}`;
702
+ const redirectUri = `http://localhost:${addr.port}${effectiveCallbackPath}`;
687
703
  listening = true;
688
704
  resolveSetup({ redirectUri, codePromise });
689
705
  });
@@ -784,6 +800,7 @@ export async function startOAuth2Flow(
784
800
  codeChallenge,
785
801
  state,
786
802
  options?.loopbackPort,
803
+ options?.loopbackCallbackPath,
787
804
  );
788
805
  }
789
806
 
@@ -113,6 +113,11 @@ const RECONNECT_COOLDOWN_MS = 3_000;
113
113
  */
114
114
  const CREDENTIAL_OP_TIMEOUT_MS = 45_000;
115
115
 
116
+ /** Returns the current CES RPC client if one has been injected. */
117
+ export function getCesClient(): CesClient | undefined {
118
+ return _cesClient;
119
+ }
120
+
116
121
  /** Inject a CES RPC client for credential routing. Resets the resolved backend. */
117
122
  export function setCesClient(client: CesClient | undefined): void {
118
123
  _cesClient = client;
@@ -111,7 +111,9 @@ export async function fetchCatalog(): Promise<CatalogSkill[]> {
111
111
  if (!Array.isArray(manifest.skills)) {
112
112
  throw new Error("Platform catalog has invalid skills array");
113
113
  }
114
- return manifest.skills;
114
+ return manifest.skills.filter(
115
+ (s): s is CatalogSkill => !!s && typeof s.id === "string",
116
+ );
115
117
  }
116
118
 
117
119
  export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
@@ -119,7 +121,9 @@ export function readLocalCatalog(repoSkillsDir: string): CatalogSkill[] {
119
121
  const raw = readFileSync(join(repoSkillsDir, "catalog.json"), "utf-8");
120
122
  const manifest = JSON.parse(raw) as CatalogManifest;
121
123
  if (!Array.isArray(manifest.skills)) return [];
122
- return manifest.skills;
124
+ return manifest.skills.filter(
125
+ (s): s is CatalogSkill => !!s && typeof s.id === "string",
126
+ );
123
127
  } catch {
124
128
  return [];
125
129
  }
@@ -903,6 +903,8 @@ export class SubagentManager {
903
903
  subagentId: info.subagentId,
904
904
  label: info.label,
905
905
  status: "running" as const,
906
+ conversationId: managed.state.conversationId,
907
+ objective: managed.state.config.objective,
906
908
  },
907
909
  },
908
910
  );
@@ -0,0 +1,80 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ __resetPinnedTabsForTests,
5
+ clearPinnedTab,
6
+ clearPinnedTabByTabId,
7
+ getPinnedTab,
8
+ setPinnedTab,
9
+ } from "../pinned-tabs.js";
10
+
11
+ describe("pinned-tabs", () => {
12
+ afterEach(() => {
13
+ __resetPinnedTabsForTests();
14
+ });
15
+
16
+ test("setPinnedTab / getPinnedTab round-trips", () => {
17
+ setPinnedTab("conv-a", "42");
18
+ expect(getPinnedTab("conv-a")).toBe("42");
19
+ });
20
+
21
+ test("getPinnedTab returns undefined for unset conversation", () => {
22
+ expect(getPinnedTab("unset-conv")).toBeUndefined();
23
+ });
24
+
25
+ test("conversations have independent pins", () => {
26
+ setPinnedTab("conv-a", "42");
27
+ setPinnedTab("conv-b", "99");
28
+ expect(getPinnedTab("conv-a")).toBe("42");
29
+ expect(getPinnedTab("conv-b")).toBe("99");
30
+ });
31
+
32
+ test("setPinnedTab overwrites a prior pin for the same conversation", () => {
33
+ setPinnedTab("conv-a", "42");
34
+ setPinnedTab("conv-a", "100");
35
+ expect(getPinnedTab("conv-a")).toBe("100");
36
+ });
37
+
38
+ test("clearPinnedTab removes the pin", () => {
39
+ setPinnedTab("conv-a", "42");
40
+ clearPinnedTab("conv-a");
41
+ expect(getPinnedTab("conv-a")).toBeUndefined();
42
+ });
43
+
44
+ test("clearPinnedTab on a non-existent conversation is a no-op", () => {
45
+ // Should not throw or affect other conversations.
46
+ setPinnedTab("conv-a", "42");
47
+ clearPinnedTab("nonexistent");
48
+ expect(getPinnedTab("conv-a")).toBe("42");
49
+ });
50
+
51
+ test("clearPinnedTabByTabId clears every matching conversation", () => {
52
+ setPinnedTab("conv-a", "42");
53
+ setPinnedTab("conv-b", "42");
54
+ setPinnedTab("conv-c", "99");
55
+
56
+ const cleared = clearPinnedTabByTabId("42");
57
+
58
+ expect(cleared).toBe(2);
59
+ expect(getPinnedTab("conv-a")).toBeUndefined();
60
+ expect(getPinnedTab("conv-b")).toBeUndefined();
61
+ expect(getPinnedTab("conv-c")).toBe("99"); // unchanged
62
+ });
63
+
64
+ test("clearPinnedTabByTabId on a non-matching tab is a no-op returning 0", () => {
65
+ setPinnedTab("conv-a", "42");
66
+ const cleared = clearPinnedTabByTabId("999");
67
+ expect(cleared).toBe(0);
68
+ expect(getPinnedTab("conv-a")).toBe("42");
69
+ });
70
+
71
+ test("empty conversationId is rejected on set", () => {
72
+ setPinnedTab("", "42");
73
+ expect(getPinnedTab("")).toBeUndefined();
74
+ });
75
+
76
+ test("empty tabId is rejected on set", () => {
77
+ setPinnedTab("conv-a", "");
78
+ expect(getPinnedTab("conv-a")).toBeUndefined();
79
+ });
80
+ });
@@ -70,6 +70,7 @@ import type {
70
70
  CdpClient,
71
71
  CdpClientKind,
72
72
  } from "./cdp-client/types.js";
73
+ import { clearPinnedTab, setPinnedTab } from "./pinned-tabs.js";
73
74
  import { checkBrowserRuntime } from "./runtime-check.js";
74
75
 
75
76
  const log = getLogger("headless-browser");
@@ -471,6 +472,15 @@ function wrapWithKindMemo(
471
472
  dispose(): void {
472
473
  inner.dispose();
473
474
  },
475
+ // Proxy the optional setCdpSessionId through to the underlying
476
+ // client so the navigate executor's --new-tab path can pin the
477
+ // freshly-created tab onto the extension CDP client. We define
478
+ // the method unconditionally here (rather than only when the
479
+ // inner client implements it) so callers can use a simple optional
480
+ // chain on this wrapper without re-walking the inner reference.
481
+ setCdpSessionId(cdpSessionId: string): void {
482
+ inner.setCdpSessionId?.(cdpSessionId);
483
+ },
474
484
  };
475
485
  }
476
486
 
@@ -624,6 +634,89 @@ export async function executeBrowserNavigate(
624
634
  if (acquired.errorResult) return acquired.errorResult;
625
635
  const { cdp, browserMode } = acquired;
626
636
 
637
+ // --new-tab: open a fresh tab via the extension's Vellum.createTab
638
+ // pseudo-CDP method, then pin this client (and the conversation) to
639
+ // the returned tabId so this Page.navigate and every subsequent
640
+ // command on the same conversation routes to the new tab instead of
641
+ // the user's currently-active tab. Extension backend only; the local
642
+ // (Playwright) backend manages its own isolated browser and the
643
+ // cdp-inspect backend connects to a single tab by URL pattern.
644
+ const newTab = input.new_tab === true;
645
+ if (newTab && cdp.kind === "extension") {
646
+ try {
647
+ const result = await cdp.send<{ tabId?: number | string }>(
648
+ "Vellum.createTab",
649
+ {},
650
+ context.signal,
651
+ );
652
+ const tabId =
653
+ typeof result?.tabId === "number"
654
+ ? String(result.tabId)
655
+ : typeof result?.tabId === "string"
656
+ ? result.tabId
657
+ : undefined;
658
+ if (!tabId) {
659
+ // Malformed createTab response (no tabId). We're nominally falling
660
+ // back to active-tab routing — but the live `cdp` instance was
661
+ // already constructed with whatever pin was in scope for this
662
+ // conversation, AND the pin store still holds it for future
663
+ // client construction. Clear both: the pin store (so the next
664
+ // executeBrowserNavigate builds a clean client) AND the current
665
+ // cdp instance's session (so the Page.navigate that runs in a
666
+ // few lines targets the active tab rather than the stale pin).
667
+ // Without the setCdpSessionId(undefined) call, the warn message
668
+ // is a lie: navigation would still route to the dead tab via the
669
+ // already-injected cdpSessionId and likely fail with
670
+ // cdp_session_not_found.
671
+ clearPinnedTab(context.conversationId);
672
+ cdp.setCdpSessionId?.(undefined);
673
+ log.warn(
674
+ { conversationId: context.conversationId, result },
675
+ "Vellum.createTab returned no tabId; cleared stale pin and live session, falling back to active-tab routing",
676
+ );
677
+ } else {
678
+ cdp.setCdpSessionId?.(tabId);
679
+ setPinnedTab(context.conversationId, tabId);
680
+ log.debug(
681
+ { conversationId: context.conversationId, tabId },
682
+ "Opened new tab via --new-tab; pinned subsequent ops to it",
683
+ );
684
+ }
685
+ } catch (err) {
686
+ // Surface the failure rather than silently clobbering the active
687
+ // tab — that's exactly the behavior --new-tab is supposed to
688
+ // avoid. Clear any stale pin so subsequent ops don't route to a
689
+ // dead tab. Note: an old extension build without Vellum.createTab
690
+ // support will land here (CDP returns an "unknown method" error).
691
+ // We're early-returning before the main try/finally block below,
692
+ // so we must dispose the cdp client manually to avoid leaking it.
693
+ clearPinnedTab(context.conversationId);
694
+ const message =
695
+ err instanceof Error ? err.message : String(err);
696
+ log.warn(
697
+ { conversationId: context.conversationId, err },
698
+ "Vellum.createTab failed; aborting --new-tab navigate",
699
+ );
700
+ try {
701
+ cdp.dispose();
702
+ } catch (disposeErr) {
703
+ log.warn(
704
+ { conversationId: context.conversationId, err: disposeErr },
705
+ "Failed to dispose CDP client after Vellum.createTab failure",
706
+ );
707
+ }
708
+ return {
709
+ content: `Error: Failed to open a new tab for navigation: ${message}. The Chrome extension may need an update to support --new-tab.`,
710
+ isError: true,
711
+ };
712
+ }
713
+ } else if (newTab && cdp.kind !== "extension") {
714
+ log.debug(
715
+ { conversationId: context.conversationId, backendKind: cdp.kind },
716
+ "--new-tab requested but backend does not support it; ignoring",
717
+ );
718
+ }
719
+
627
720
  // Screencast + handoff are Playwright-backed and only meaningful
628
721
  // for the local sacrificial-profile path. On the extension path the
629
722
  // user already has their own Chrome window, so both are no-ops.