@vellumai/assistant 0.6.2 → 0.6.3

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 (396) hide show
  1. package/bun.lock +40 -40
  2. package/bunfig.toml +3 -0
  3. package/docs/architecture/memory.md +1 -1
  4. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  5. package/openapi.yaml +184 -69
  6. package/package.json +41 -41
  7. package/scripts/generate-openapi.ts +1 -2
  8. package/src/__tests__/acp-session.test.ts +43 -0
  9. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  10. package/src/__tests__/app-executors.test.ts +1 -0
  11. package/src/__tests__/app-source-watcher.test.ts +37 -11
  12. package/src/__tests__/approval-routes-http.test.ts +178 -1
  13. package/src/__tests__/browser-fill-credential.test.ts +229 -94
  14. package/src/__tests__/browser-manager.test.ts +40 -27
  15. package/src/__tests__/catalog-files.test.ts +862 -0
  16. package/src/__tests__/channel-approvals.test.ts +53 -0
  17. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  18. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  19. package/src/__tests__/config-schema.test.ts +125 -48
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  21. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  22. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop.test.ts +1 -1
  24. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  25. package/src/__tests__/conversation-attachments.test.ts +80 -4
  26. package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
  27. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  28. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  29. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  30. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  31. package/src/__tests__/conversation-queue.test.ts +45 -2
  32. package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
  33. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  34. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  35. package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
  36. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  37. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  38. package/src/__tests__/conversation-store.test.ts +195 -0
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  40. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  41. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  42. package/src/__tests__/credential-vault-unit.test.ts +4 -4
  43. package/src/__tests__/credential-vault.test.ts +152 -13
  44. package/src/__tests__/credentials-cli.test.ts +2 -2
  45. package/src/__tests__/date-context.test.ts +4 -4
  46. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  47. package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
  48. package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
  49. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  50. package/src/__tests__/gemini-provider.test.ts +2 -2
  51. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  52. package/src/__tests__/headless-browser-interactions.test.ts +707 -371
  53. package/src/__tests__/headless-browser-navigate.test.ts +389 -47
  54. package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
  55. package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
  56. package/src/__tests__/host-bash-proxy.test.ts +150 -1
  57. package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
  58. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  59. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  60. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  61. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  62. package/src/__tests__/host-browser-routes.test.ts +198 -0
  63. package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
  64. package/src/__tests__/host-cu-proxy.test.ts +171 -1
  65. package/src/__tests__/host-file-proxy.test.ts +185 -1
  66. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  67. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  68. package/src/__tests__/host-shell-tool.test.ts +1 -11
  69. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  70. package/src/__tests__/integration-status.test.ts +6 -7
  71. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  72. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  73. package/src/__tests__/mcp-health-check.test.ts +10 -3
  74. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  75. package/src/__tests__/migration-export-http.test.ts +61 -2
  76. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  77. package/src/__tests__/migration-import-commit-http.test.ts +101 -1
  78. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  79. package/src/__tests__/oauth-apps-routes.test.ts +17 -12
  80. package/src/__tests__/oauth-cli.test.ts +707 -60
  81. package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
  82. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  83. package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
  84. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  85. package/src/__tests__/oauth-providers-routes.test.ts +50 -14
  86. package/src/__tests__/oauth-store.test.ts +1386 -182
  87. package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
  88. package/src/__tests__/onboarding-template-contract.test.ts +75 -57
  89. package/src/__tests__/openai-provider.test.ts +2 -2
  90. package/src/__tests__/outlook-categories.test.ts +1 -1
  91. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  92. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  93. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  94. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  95. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  96. package/src/__tests__/outlook-trash.test.ts +1 -1
  97. package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
  98. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  99. package/src/__tests__/permission-mode.test.ts +28 -56
  100. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  101. package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
  102. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  103. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  104. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  105. package/src/__tests__/schedule-routes.test.ts +162 -0
  106. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  107. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  108. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  109. package/src/__tests__/set-permission-mode.test.ts +13 -250
  110. package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
  111. package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
  112. package/src/__tests__/slack-channel-config.test.ts +12 -15
  113. package/src/__tests__/subagent-detail.test.ts +44 -2
  114. package/src/__tests__/subagent-disposal.test.ts +1 -0
  115. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  116. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  117. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  118. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  119. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  120. package/src/__tests__/subagent-tools.test.ts +1 -0
  121. package/src/__tests__/subagent-types.test.ts +1 -0
  122. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  123. package/src/__tests__/system-prompt.test.ts +72 -1
  124. package/src/__tests__/task-scheduler.test.ts +32 -6
  125. package/src/__tests__/telegram-config.test.ts +10 -13
  126. package/src/__tests__/terminal-tools.test.ts +9 -0
  127. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  128. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  129. package/src/__tests__/top-level-renderer.test.ts +73 -1
  130. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  131. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  132. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  133. package/src/acp/client-handler.ts +30 -4
  134. package/src/agent/loop.ts +12 -6
  135. package/src/approvals/guardian-request-resolvers.ts +21 -15
  136. package/src/browser-session/__tests__/manager.test.ts +297 -0
  137. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  138. package/src/browser-session/backends/extension.ts +26 -0
  139. package/src/browser-session/backends/local.ts +24 -0
  140. package/src/browser-session/events.ts +164 -0
  141. package/src/browser-session/index.ts +27 -0
  142. package/src/browser-session/manager.ts +159 -0
  143. package/src/browser-session/types.ts +28 -0
  144. package/src/channels/__tests__/types.test.ts +134 -0
  145. package/src/channels/types.ts +53 -3
  146. package/src/cli/commands/browser-relay.ts +339 -409
  147. package/src/cli/commands/credentials.ts +3 -3
  148. package/src/cli/commands/email.ts +18 -13
  149. package/src/cli/commands/mcp.ts +16 -4
  150. package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
  151. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  152. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  153. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  154. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
  155. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
  156. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
  157. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  158. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  159. package/src/cli/commands/oauth/apps.ts +7 -4
  160. package/src/cli/commands/oauth/connect.ts +6 -3
  161. package/src/cli/commands/oauth/disconnect.ts +1 -1
  162. package/src/cli/commands/oauth/providers.ts +200 -36
  163. package/src/cli/commands/oauth/shared.ts +5 -5
  164. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
  165. package/src/cli/commands/platform/index.ts +107 -10
  166. package/src/cli/commands/usage.ts +10 -9
  167. package/src/cli/lib/daemon-credential-client.ts +4 -0
  168. package/src/cli/program.ts +1 -1
  169. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  170. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
  171. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  172. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  173. package/src/config/bundled-skills/contacts/SKILL.md +3 -0
  174. package/src/config/bundled-skills/document/SKILL.md +4 -0
  175. package/src/config/bundled-skills/gmail/SKILL.md +1 -1
  176. package/src/config/bundled-skills/outlook/SKILL.md +7 -0
  177. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  178. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  179. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  180. package/src/config/env-registry.ts +14 -0
  181. package/src/config/env.ts +21 -0
  182. package/src/config/feature-flag-registry.json +44 -5
  183. package/src/config/loader.ts +56 -1
  184. package/src/config/sanitize-for-transfer.ts +47 -0
  185. package/src/config/schema.ts +46 -5
  186. package/src/config/schemas/host-browser.ts +66 -0
  187. package/src/config/schemas/memory-lifecycle.ts +1 -1
  188. package/src/config/schemas/memory-retrieval.ts +103 -0
  189. package/src/config/schemas/security.ts +0 -6
  190. package/src/config/schemas/services.ts +8 -0
  191. package/src/config/types.ts +0 -1
  192. package/src/context/post-turn-tool-result-truncation.ts +176 -0
  193. package/src/context/window-manager.ts +19 -1
  194. package/src/credential-execution/approval-bridge.ts +49 -15
  195. package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
  196. package/src/daemon/app-source-watcher.ts +35 -0
  197. package/src/daemon/context-overflow-approval.ts +5 -0
  198. package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
  199. package/src/daemon/conversation-agent-loop.ts +58 -24
  200. package/src/daemon/conversation-attachments.ts +40 -0
  201. package/src/daemon/conversation-process.ts +48 -1
  202. package/src/daemon/conversation-runtime-assembly.ts +118 -36
  203. package/src/daemon/conversation-surfaces.ts +37 -36
  204. package/src/daemon/conversation-tool-setup.ts +74 -8
  205. package/src/daemon/conversation-workspace.ts +12 -0
  206. package/src/daemon/conversation.ts +226 -8
  207. package/src/daemon/date-context.ts +10 -10
  208. package/src/daemon/first-greeting.ts +3 -2
  209. package/src/daemon/handlers/conversations.ts +9 -140
  210. package/src/daemon/handlers/shared.ts +58 -0
  211. package/src/daemon/handlers/skills.ts +232 -37
  212. package/src/daemon/host-bash-proxy.ts +48 -13
  213. package/src/daemon/host-browser-proxy.ts +191 -0
  214. package/src/daemon/host-cu-proxy.ts +36 -11
  215. package/src/daemon/host-file-proxy.ts +57 -9
  216. package/src/daemon/lifecycle.ts +65 -11
  217. package/src/daemon/message-protocol.ts +7 -0
  218. package/src/daemon/message-types/conversations.ts +55 -13
  219. package/src/daemon/message-types/host-browser.ts +100 -0
  220. package/src/daemon/message-types/messages.ts +5 -5
  221. package/src/daemon/message-types/skills.ts +10 -0
  222. package/src/daemon/message-types/subagents.ts +2 -0
  223. package/src/daemon/server.ts +92 -12
  224. package/src/daemon/tool-side-effects.ts +6 -0
  225. package/src/daemon/transport-hints.ts +5 -24
  226. package/src/inbound/platform-callback-registration.ts +18 -17
  227. package/src/mcp/client.ts +59 -24
  228. package/src/memory/app-store.ts +31 -1
  229. package/src/memory/conversation-crud.ts +23 -0
  230. package/src/memory/conversation-starters-cadence.ts +76 -0
  231. package/src/memory/conversation-title-service.ts +5 -2
  232. package/src/memory/db-init.ts +12 -0
  233. package/src/memory/embedding-backend.test.ts +75 -0
  234. package/src/memory/embedding-backend.ts +131 -5
  235. package/src/memory/embedding-gemini.test.ts +54 -0
  236. package/src/memory/embedding-gemini.ts +20 -9
  237. package/src/memory/embedding-local.ts +176 -17
  238. package/src/memory/graph/consolidation.ts +10 -23
  239. package/src/memory/graph/extraction-job.ts +15 -0
  240. package/src/memory/graph/retriever.ts +40 -22
  241. package/src/memory/graph/store.test.ts +7 -3
  242. package/src/memory/graph/store.ts +47 -12
  243. package/src/memory/llm-usage-store.ts +45 -4
  244. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  245. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  246. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  247. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  248. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  249. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  250. package/src/memory/migrations/index.ts +6 -0
  251. package/src/memory/migrations/registry.ts +8 -0
  252. package/src/memory/schema/conversations.ts +1 -0
  253. package/src/memory/schema/oauth.ts +18 -13
  254. package/src/oauth/AGENTS.md +76 -0
  255. package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
  256. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  257. package/src/oauth/byo-connection.test.ts +8 -8
  258. package/src/oauth/byo-connection.ts +7 -7
  259. package/src/oauth/connect-orchestrator.ts +23 -21
  260. package/src/oauth/connect-types.ts +3 -3
  261. package/src/oauth/connection-resolver.test.ts +17 -4
  262. package/src/oauth/connection-resolver.ts +16 -16
  263. package/src/oauth/connection.ts +1 -1
  264. package/src/oauth/manual-token-connection.ts +13 -13
  265. package/src/oauth/oauth-store.ts +214 -100
  266. package/src/oauth/platform-connection.test.ts +3 -3
  267. package/src/oauth/platform-connection.ts +4 -4
  268. package/src/oauth/provider-serializer.ts +31 -5
  269. package/src/oauth/revoke.ts +76 -0
  270. package/src/oauth/seed-providers.ts +126 -87
  271. package/src/oauth/token-persistence.ts +1 -1
  272. package/src/permissions/permission-mode.ts +4 -11
  273. package/src/permissions/prompter.ts +13 -1
  274. package/src/permissions/v2-consent-policy.ts +87 -0
  275. package/src/prompts/system-prompt.ts +18 -21
  276. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  277. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  278. package/src/providers/anthropic/client.ts +1 -0
  279. package/src/providers/types.ts +1 -1
  280. package/src/runtime/AGENTS.md +23 -0
  281. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  282. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  283. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  284. package/src/runtime/assistant-event-hub.ts +2 -2
  285. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  286. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  287. package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
  288. package/src/runtime/auth/middleware.ts +98 -0
  289. package/src/runtime/auth/route-policy.ts +6 -7
  290. package/src/runtime/capability-tokens.ts +414 -0
  291. package/src/runtime/channel-approvals.ts +18 -5
  292. package/src/runtime/chrome-extension-registry.ts +332 -0
  293. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  294. package/src/runtime/guardian-decision-types.ts +7 -0
  295. package/src/runtime/http-server.ts +425 -70
  296. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  297. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  298. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
  299. package/src/runtime/migrations/migration-transport.ts +6 -0
  300. package/src/runtime/migrations/migration-wizard.ts +22 -2
  301. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  302. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  303. package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
  304. package/src/runtime/migrations/vbundle-importer.ts +55 -5
  305. package/src/runtime/pending-interactions.ts +29 -13
  306. package/src/runtime/routes/approval-routes.ts +90 -16
  307. package/src/runtime/routes/browser-cdp-routes.ts +229 -0
  308. package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
  309. package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
  310. package/src/runtime/routes/conversation-management-routes.ts +108 -0
  311. package/src/runtime/routes/conversation-routes.ts +301 -27
  312. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  313. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  314. package/src/runtime/routes/host-browser-routes.ts +279 -0
  315. package/src/runtime/routes/host-file-routes.ts +9 -1
  316. package/src/runtime/routes/identity-routes.ts +259 -16
  317. package/src/runtime/routes/log-export-routes.ts +42 -22
  318. package/src/runtime/routes/memory-item-routes.ts +1 -7
  319. package/src/runtime/routes/migration-routes.ts +87 -2
  320. package/src/runtime/routes/oauth-apps.ts +15 -17
  321. package/src/runtime/routes/oauth-providers.ts +4 -0
  322. package/src/runtime/routes/schedule-routes.ts +24 -11
  323. package/src/runtime/routes/settings-routes.ts +9 -97
  324. package/src/runtime/routes/skills-routes.ts +52 -2
  325. package/src/runtime/routes/subagents-routes.ts +14 -10
  326. package/src/runtime/routes/usage-routes.ts +8 -7
  327. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  328. package/src/runtime/routes/workspace-routes.ts +8 -1
  329. package/src/runtime/routes/workspace-utils.ts +2 -0
  330. package/src/schedule/scheduler.ts +7 -5
  331. package/src/security/ces-credential-client.ts +20 -0
  332. package/src/security/ces-rpc-credential-backend.ts +17 -0
  333. package/src/security/credential-backend.ts +5 -0
  334. package/src/security/oauth2.ts +42 -25
  335. package/src/security/secure-keys.ts +118 -25
  336. package/src/security/token-manager.ts +23 -10
  337. package/src/skills/catalog-files.ts +492 -0
  338. package/src/subagent/manager.ts +131 -26
  339. package/src/subagent/types.ts +19 -0
  340. package/src/tools/apps/executors.ts +11 -2
  341. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  342. package/src/tools/browser/auth-detector.ts +43 -12
  343. package/src/tools/browser/browser-execution.ts +645 -340
  344. package/src/tools/browser/browser-manager.ts +36 -12
  345. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  346. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  347. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
  348. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
  349. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
  350. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  351. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  352. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  353. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  354. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  355. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  356. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
  357. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  358. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
  359. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  360. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
  361. package/src/tools/browser/cdp-client/errors.ts +34 -0
  362. package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
  363. package/src/tools/browser/cdp-client/factory.ts +204 -0
  364. package/src/tools/browser/cdp-client/index.ts +14 -0
  365. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  366. package/src/tools/browser/cdp-client/types.ts +52 -0
  367. package/src/tools/filesystem/edit.ts +1 -1
  368. package/src/tools/filesystem/list.ts +1 -1
  369. package/src/tools/filesystem/read.ts +1 -1
  370. package/src/tools/filesystem/write.ts +2 -1
  371. package/src/tools/host-filesystem/edit.ts +1 -1
  372. package/src/tools/host-filesystem/read.ts +12 -15
  373. package/src/tools/host-filesystem/write.ts +1 -1
  374. package/src/tools/host-terminal/host-shell.ts +21 -16
  375. package/src/tools/permission-checker.ts +77 -82
  376. package/src/tools/registry.ts +0 -2
  377. package/src/tools/secret-detection-handler.ts +34 -0
  378. package/src/tools/shared/filesystem/image-read.ts +61 -40
  379. package/src/tools/subagent/spawn.ts +47 -3
  380. package/src/tools/subagent/status.ts +2 -0
  381. package/src/tools/system/register.ts +2 -16
  382. package/src/tools/terminal/safe-env.ts +7 -0
  383. package/src/tools/terminal/shell.ts +21 -16
  384. package/src/tools/tool-approval-handler.ts +48 -2
  385. package/src/tools/types.ts +2 -0
  386. package/src/util/platform.ts +14 -19
  387. package/src/workspace/top-level-renderer.ts +19 -1
  388. package/src/__tests__/chrome-cdp.test.ts +0 -419
  389. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  390. package/src/__tests__/permission-mode-store.test.ts +0 -277
  391. package/src/browser-extension-relay/protocol.ts +0 -63
  392. package/src/browser-extension-relay/server.ts +0 -203
  393. package/src/config/schemas/sandbox.ts +0 -14
  394. package/src/permissions/permission-mode-store.ts +0 -180
  395. package/src/tools/browser/chrome-cdp.ts +0 -239
  396. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -36,12 +36,13 @@ import {
36
36
  } from "../../daemon/first-greeting.js";
37
37
  import { renderHistoryContent } from "../../daemon/handlers/shared.js";
38
38
  import { HostBashProxy } from "../../daemon/host-bash-proxy.js";
39
+ import { HostBrowserProxy } from "../../daemon/host-browser-proxy.js";
39
40
  import { HostCuProxy } from "../../daemon/host-cu-proxy.js";
40
41
  import { HostFileProxy } from "../../daemon/host-file-proxy.js";
41
42
  import type { ServerMessage } from "../../daemon/message-protocol.js";
42
43
  import type {
43
- MacosTransportMetadata,
44
- NonMacosTransportMetadata,
44
+ HostProxyTransportMetadata,
45
+ NonHostProxyTransportMetadata,
45
46
  } from "../../daemon/message-types/conversations.js";
46
47
  import type { HeartbeatService } from "../../heartbeat/heartbeat-service.js";
47
48
  import * as attachmentsStore from "../../memory/attachments-store.js";
@@ -77,6 +78,7 @@ import { silentlyWithLog } from "../../util/silently.js";
77
78
  import { buildAssistantEvent } from "../assistant-event.js";
78
79
  import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
79
80
  import type { AuthContext } from "../auth/types.js";
81
+ import { getChromeExtensionRegistry } from "../chrome-extension-registry.js";
80
82
  import { bridgeConfirmationRequestToGuardian } from "../confirmation-request-guardian-bridge.js";
81
83
  import { routeGuardianReply } from "../guardian-reply-router.js";
82
84
  import { healGuardianBindingDrift } from "../guardian-vellum-migration.js";
@@ -422,8 +424,18 @@ export function handleListMessages(
422
424
  // pendingToolUses map — otherwise they render as "Unknown" tool calls.
423
425
  const mergedMessages = mergeToolResultsIntoAssistantMessages(rawMessages);
424
426
 
427
+ // During streaming, all assistant turns within one agent loop accumulate
428
+ // on a single client-side ChatMessage (via currentAssistantMessageId).
429
+ // In the DB, each API turn is a separate assistant row because
430
+ // consolidation is deferred to compaction for prefix-cache stability.
431
+ // Merge consecutive assistant messages here at query time so
432
+ // renderHistoryContent produces the same contentOrder shape as streaming
433
+ // (consecutive tool refs grouped together).
434
+ const { messages: consolidatedMessages, mergedIdMap } =
435
+ mergeConsecutiveAssistantMessages(mergedMessages);
436
+
425
437
  // Parse content blocks and extract text + tool calls
426
- const parsed = mergedMessages.map((msg) => {
438
+ const parsed = consolidatedMessages.map((msg) => {
427
439
  let content: unknown;
428
440
  try {
429
441
  content = JSON.parse(msg.content);
@@ -548,7 +560,13 @@ export function handleListMessages(
548
560
  // blobs for non-image attachments (documents, audio). Then
549
561
  // selectively fetch full data only for images so the client can
550
562
  // generate thumbnails for inline display on history restore.
551
- const linked = attachmentsStore.getAttachmentMetadataForMessage(m.id);
563
+ // Also query attachments for any messages that were merged into
564
+ // this one (consecutive assistant merge), so their attachments
565
+ // aren't lost before DB compaction relinks them.
566
+ const idsToQuery = [m.id, ...(mergedIdMap.get(m.id) ?? [])];
567
+ const linked = idsToQuery.flatMap((id) =>
568
+ attachmentsStore.getAttachmentMetadataForMessage(id),
569
+ );
552
570
  if (linked.length > 0) {
553
571
  msgAttachments = linked.map((a) => {
554
572
  if (a.mimeType.startsWith("image/")) {
@@ -793,14 +811,158 @@ function mergeToolResultsIntoAssistantMessages(
793
811
  return result;
794
812
  }
795
813
 
814
+ // ── Consecutive assistant message merging ────────────────────────────
815
+
816
+ /** Parse a message's JSON content into an array of content blocks. */
817
+ function parseContentBlocks(content: string): unknown[] {
818
+ try {
819
+ const parsed = JSON.parse(content);
820
+ return Array.isArray(parsed) ? parsed : [parsed];
821
+ } catch (err) {
822
+ log.warn(
823
+ { err },
824
+ "Failed to parse content blocks during assistant message merge",
825
+ );
826
+ return [];
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Append content blocks from a donor message onto a target block array.
832
+ * Parses the donor's JSON content and pushes each block into `target`.
833
+ */
834
+ function appendContentBlocks(target: unknown[], donorContent: string): void {
835
+ try {
836
+ const parsed = JSON.parse(donorContent);
837
+ if (Array.isArray(parsed)) {
838
+ target.push(...parsed);
839
+ } else {
840
+ target.push(parsed);
841
+ }
842
+ } catch (err) {
843
+ log.warn(
844
+ { err },
845
+ "Failed to parse donor content blocks during assistant message merge",
846
+ );
847
+ }
848
+ }
849
+
850
+ /**
851
+ * Promote metadata fields from a donor message to the surviving message
852
+ * when the survivor lacks them. Currently promotes `subagentNotification`.
853
+ * Returns a new MessageRow if promotion occurred, otherwise the original.
854
+ */
855
+ function promoteMetadata(survivor: MessageRow, donor: MessageRow): MessageRow {
856
+ if (donor.metadata && survivor.metadata) {
857
+ try {
858
+ const survivorMeta = JSON.parse(survivor.metadata);
859
+ const donorMeta = JSON.parse(donor.metadata);
860
+ if (
861
+ !survivorMeta.subagentNotification &&
862
+ donorMeta.subagentNotification
863
+ ) {
864
+ survivorMeta.subagentNotification = donorMeta.subagentNotification;
865
+ return { ...survivor, metadata: JSON.stringify(survivorMeta) };
866
+ }
867
+ } catch (err) {
868
+ log.warn(
869
+ { err },
870
+ "Failed to parse metadata during assistant message merge",
871
+ );
872
+ }
873
+ } else if (donor.metadata && !survivor.metadata) {
874
+ return { ...survivor, metadata: donor.metadata };
875
+ }
876
+ return survivor;
877
+ }
878
+
879
+ /**
880
+ * Merge consecutive assistant messages into a single message at query time.
881
+ *
882
+ * During streaming, all assistant turns within one agent loop accumulate on
883
+ * a single client-side ChatMessage. In the DB, each API turn is stored as a
884
+ * separate assistant row (consolidation is deferred to compaction for
885
+ * prefix-cache stability). This produces N separate assistant messages that
886
+ * the client renders as N individual bubbles — each showing "Completed 1
887
+ * step" instead of one grouped "Completed N steps" accordion.
888
+ *
889
+ * This function concatenates the content block arrays of consecutive
890
+ * assistant messages (no intervening user messages after tool-result
891
+ * merging) into the first message of each run. The merged messages are
892
+ * removed from the output. This is query-time only — the DB is not
893
+ * modified.
894
+ *
895
+ * The first message in each run keeps its id, createdAt, and metadata so
896
+ * that attachment lookups, display timestamps, and subagent notifications
897
+ * continue to work. Metadata from later messages in the run (e.g.
898
+ * subagentNotification) is preserved by promoting it to the surviving
899
+ * message when the surviving message has no metadata of its own for that
900
+ * field.
901
+ */
902
+ function mergeConsecutiveAssistantMessages(messages: MessageRow[]): {
903
+ messages: MessageRow[];
904
+ /** Maps each surviving message ID → all original message IDs merged into it. */
905
+ mergedIdMap: Map<string, string[]>;
906
+ } {
907
+ const result: MessageRow[] = [];
908
+ // Key = index in `result`, value = accumulated content blocks.
909
+ const pendingMerges = new Map<number, unknown[]>();
910
+ // Key = index in `result`, value = IDs of messages merged into the target.
911
+ const mergedIds = new Map<number, string[]>();
912
+
913
+ for (const msg of messages) {
914
+ const lastIdx = result.length - 1;
915
+ const isConsecutiveAssistant =
916
+ msg.role === "assistant" &&
917
+ lastIdx >= 0 &&
918
+ result[lastIdx].role === "assistant";
919
+
920
+ if (!isConsecutiveAssistant) {
921
+ result.push(msg);
922
+ continue;
923
+ }
924
+
925
+ // Track the donor message ID.
926
+ let ids = mergedIds.get(lastIdx);
927
+ if (!ids) {
928
+ ids = [];
929
+ mergedIds.set(lastIdx, ids);
930
+ }
931
+ ids.push(msg.id);
932
+
933
+ // Lazily parse the target's content on first merge.
934
+ let targetContent = pendingMerges.get(lastIdx);
935
+ if (!targetContent) {
936
+ targetContent = parseContentBlocks(result[lastIdx].content);
937
+ pendingMerges.set(lastIdx, targetContent);
938
+ }
939
+
940
+ appendContentBlocks(targetContent, msg.content);
941
+ result[lastIdx] = promoteMetadata(result[lastIdx], msg);
942
+ }
943
+
944
+ // Write back merged content for any messages that were targets.
945
+ for (const [idx, content] of pendingMerges) {
946
+ result[idx] = { ...result[idx], content: JSON.stringify(content) };
947
+ }
948
+
949
+ // Build the merged ID map keyed by surviving message ID.
950
+ const mergedIdMap = new Map<string, string[]>();
951
+ for (const [idx, ids] of mergedIds) {
952
+ mergedIdMap.set(result[idx].id, ids);
953
+ }
954
+
955
+ return { messages: result, mergedIdMap };
956
+ }
957
+
796
958
  /**
797
959
  * Build an `onEvent` callback that publishes every outbound event to the
798
960
  * assistant event hub, maintaining ordered delivery through a serial chain.
799
961
  *
800
962
  * Also registers pending interactions when confirmation_request,
801
- * secret_request, host_bash_request, or host_file_request events flow
802
- * through, so standalone approval/result endpoints can look up the conversation
803
- * by requestId.
963
+ * secret_request, host_bash_request, host_browser_request, host_file_request,
964
+ * or host_cu_request events flow through, so standalone approval/result
965
+ * endpoints can look up the conversation by requestId.
804
966
  */
805
967
  function makeHubPublisher(
806
968
  deps: SendMessageDeps,
@@ -892,6 +1054,12 @@ function makeHubPublisher(
892
1054
  conversationId,
893
1055
  kind: "host_bash",
894
1056
  });
1057
+ } else if (msg.type === "host_browser_request") {
1058
+ pendingInteractions.register(msg.requestId, {
1059
+ conversation,
1060
+ conversationId,
1061
+ kind: "host_browser",
1062
+ });
895
1063
  } else if (msg.type === "host_file_request") {
896
1064
  pendingInteractions.register(msg.requestId, {
897
1065
  conversation,
@@ -1058,18 +1226,21 @@ export async function handleSendMessage(
1058
1226
 
1059
1227
  // Build transport metadata from the request so the daemon can inject
1060
1228
  // host environment hints (home directory, username) into the LLM context.
1061
- const transport =
1062
- sourceInterface === "macos"
1063
- ? ({
1064
- channelId: sourceChannel,
1065
- interfaceId: "macos" as const,
1066
- hostHomeDir: body.hostHomeDir,
1067
- hostUsername: body.hostUsername,
1068
- } satisfies MacosTransportMetadata)
1069
- : ({
1070
- channelId: sourceChannel,
1071
- interfaceId: sourceInterface,
1072
- } satisfies NonMacosTransportMetadata);
1229
+ // The `supportsHostProxy` type predicate narrows `sourceInterface` to
1230
+ // `HostProxyInterfaceId` in the truthy branch, which is exactly the
1231
+ // discriminant the `HostProxyTransportMetadata` variant expects — so the
1232
+ // construction site stays in lock-step with the runtime capability gate.
1233
+ const transport = supportsHostProxy(sourceInterface)
1234
+ ? ({
1235
+ channelId: sourceChannel,
1236
+ interfaceId: sourceInterface,
1237
+ hostHomeDir: body.hostHomeDir,
1238
+ hostUsername: body.hostUsername,
1239
+ } satisfies HostProxyTransportMetadata)
1240
+ : ({
1241
+ channelId: sourceChannel,
1242
+ interfaceId: sourceInterface,
1243
+ } satisfies NonHostProxyTransportMetadata);
1073
1244
 
1074
1245
  const conversation = await smDeps.getOrCreateConversation(
1075
1246
  mapping.conversationId,
@@ -1139,12 +1310,13 @@ export async function handleSendMessage(
1139
1310
  conversation,
1140
1311
  );
1141
1312
  const isInteractive = isInteractiveInterface(sourceInterface);
1142
- // Only create the host bash proxy for desktop client interfaces that can
1143
- // execute commands on the user's machine. Non-desktop conversations (CLI,
1144
- // channels, headless) fall back to local execution.
1313
+ // Only create each host proxy for interfaces that support the matching
1314
+ // capability. macOS supports all four; the chrome-extension interface only
1315
+ // supports host_browser. Non-desktop conversations (CLI, channels, headless)
1316
+ // fall back to local execution.
1145
1317
  // Set the proxy BEFORE updateClient so updateClient's call to
1146
1318
  // hostBashProxy.updateSender targets the correct (new) proxy.
1147
- if (supportsHostProxy(sourceInterface)) {
1319
+ if (supportsHostProxy(sourceInterface, "host_bash")) {
1148
1320
  // Reuse the existing proxy if the conversation is actively processing a
1149
1321
  // host bash request to avoid orphaning in-flight requests.
1150
1322
  if (!conversation.isProcessing() || !conversation.hostBashProxy) {
@@ -1153,12 +1325,92 @@ export async function handleSendMessage(
1153
1325
  });
1154
1326
  conversation.setHostBashProxy(proxy);
1155
1327
  }
1328
+ } else if (!conversation.isProcessing()) {
1329
+ conversation.setHostBashProxy(undefined);
1330
+ }
1331
+ // For the chrome-extension interface we route host_browser_request /
1332
+ // host_browser_cancel frames through the in-process ChromeExtensionRegistry
1333
+ // to the WebSocket opened against /v1/browser-relay by the connected
1334
+ // extension, instead of the SSE/onEvent hub used by macOS. The registry
1335
+ // lookup is keyed by the JWT-derived actor principal id, which the
1336
+ // runtime captured at WebSocket upgrade time.
1337
+ //
1338
+ // A single guardian may have multiple parallel extension installs
1339
+ // connected at once (two Chrome profiles, two desktops). The registry
1340
+ // tracks them under (guardianId, clientInstanceId) pairs and the
1341
+ // default `send(guardianId, msg)` path routes to whichever instance
1342
+ // has the most recent activity — typically the one the user is
1343
+ // currently driving. Pinning to a specific instance can be done via
1344
+ // `sendToInstance` if a caller ever needs it.
1345
+ //
1346
+ // macOS (and any other interface that supports host_browser in the
1347
+ // future via the SSE hub) keeps using `onEvent` — see the else branch.
1348
+ const browserProxySendToClient: (msg: ServerMessage) => void =
1349
+ sourceInterface === "chrome-extension"
1350
+ ? (msg) => {
1351
+ // Resolve the guardian principal id at send time rather than
1352
+ // capturing it from the POST-time authContext. This closure can be
1353
+ // re-fired on queue drain — if a different actor's POST lands while
1354
+ // the queue is still draining an earlier turn, a captured
1355
+ // authContext.actorPrincipalId would mis-route the earlier turn's
1356
+ // host_browser frames to the *new* actor. Preferring
1357
+ // conversation.trustContext?.guardianPrincipalId makes the routing
1358
+ // follow the conversation's bound guardian, which is stable across
1359
+ // subsequent POSTs. Falls back to the per-POST authContext for
1360
+ // turns that haven't been bound to a trust context yet.
1361
+ const gid =
1362
+ conversation.trustContext?.guardianPrincipalId ??
1363
+ authContext.actorPrincipalId;
1364
+ if (!gid) {
1365
+ // No guardian identity on this turn — nothing to route to.
1366
+ // The proxy will observe this via its try/catch and surface a
1367
+ // transport error back to the caller.
1368
+ throw new Error(
1369
+ "chrome-extension host_browser send skipped: no guardianId on AuthContext",
1370
+ );
1371
+ }
1372
+ const ok = getChromeExtensionRegistry().send(gid, msg);
1373
+ if (!ok) {
1374
+ throw new Error(
1375
+ `chrome-extension host_browser send failed: no active connection for guardian ${gid}`,
1376
+ );
1377
+ }
1378
+ }
1379
+ : onEvent;
1380
+ // Stash the registry-routed sender on the conversation so queue-drain
1381
+ // restores (which run outside of conversation-routes.ts and only have
1382
+ // access to `sendToClient`) can preserve it when calling
1383
+ // `restoreBrowserProxyAvailability()`. For non-chrome-extension
1384
+ // interfaces the override is cleared so the SSE hub sender is used.
1385
+ if (sourceInterface === "chrome-extension") {
1386
+ conversation.hostBrowserSenderOverride = browserProxySendToClient;
1387
+ } else {
1388
+ conversation.hostBrowserSenderOverride = undefined;
1389
+ }
1390
+ if (supportsHostProxy(sourceInterface, "host_browser")) {
1391
+ if (!conversation.isProcessing() || !conversation.hostBrowserProxy) {
1392
+ const browserProxy = new HostBrowserProxy(
1393
+ browserProxySendToClient,
1394
+ (requestId) => {
1395
+ pendingInteractions.resolve(requestId);
1396
+ },
1397
+ );
1398
+ conversation.setHostBrowserProxy(browserProxy);
1399
+ }
1400
+ } else if (!conversation.isProcessing()) {
1401
+ conversation.setHostBrowserProxy(undefined);
1402
+ }
1403
+ if (supportsHostProxy(sourceInterface, "host_file")) {
1156
1404
  if (!conversation.isProcessing() || !conversation.hostFileProxy) {
1157
1405
  const fileProxy = new HostFileProxy(onEvent, (requestId) => {
1158
1406
  pendingInteractions.resolve(requestId);
1159
1407
  });
1160
1408
  conversation.setHostFileProxy(fileProxy);
1161
1409
  }
1410
+ } else if (!conversation.isProcessing()) {
1411
+ conversation.setHostFileProxy(undefined);
1412
+ }
1413
+ if (supportsHostProxy(sourceInterface, "host_cu")) {
1162
1414
  if (!conversation.isProcessing() || !conversation.hostCuProxy) {
1163
1415
  const cuProxy = new HostCuProxy(onEvent, (requestId) => {
1164
1416
  pendingInteractions.resolve(requestId);
@@ -1172,19 +1424,41 @@ export async function handleSendMessage(
1172
1424
  conversation.addPreactivatedSkillId("computer-use");
1173
1425
  }
1174
1426
  } else if (!conversation.isProcessing()) {
1175
- conversation.setHostBashProxy(undefined);
1176
- conversation.setHostFileProxy(undefined);
1177
1427
  conversation.setHostCuProxy(undefined);
1178
1428
  }
1179
1429
  // Wire sendToClient to the SSE hub so all subsystems can reach the HTTP client.
1180
1430
  // Called after setHostBashProxy so updateSender targets the current proxy.
1181
1431
  // When proxies are preserved during an active turn (non-desktop request while
1182
- // processing), skip updating proxy senders to avoid degrading them.
1432
+ // processing), skip updating proxy senders to avoid degrading them. The gate
1433
+ // matches the host_bash capability because the legacy "reject send during
1434
+ // host bash" flow is what this is really protecting.
1183
1435
  const preservingProxies =
1184
- conversation.isProcessing() && !supportsHostProxy(sourceInterface);
1436
+ conversation.isProcessing() &&
1437
+ !supportsHostProxy(sourceInterface, "host_bash");
1438
+ // hasNoClient must remain `!isInteractive` so downstream tool gating
1439
+ // (`isToolActiveForContext` for HOST_TOOL_NAMES, `createToolExecutor`'s
1440
+ // `isInteractive: !ctx.hasNoClient`) keeps host_bash/host_file/host_cu
1441
+ // tools gated for non-desktop interfaces. The chrome-extension interface
1442
+ // is non-interactive (no SSE prompter UI) but still has a connected client
1443
+ // that can service host_browser_request events; we restore that single
1444
+ // proxy explicitly below without relaxing `hasNoClient`.
1185
1445
  conversation.updateClient(onEvent, !isInteractive, {
1186
1446
  skipProxySenderUpdate: preservingProxies,
1187
1447
  });
1448
+ // For non-interactive interfaces that DO support host_browser
1449
+ // (chrome-extension), explicitly re-enable just the browser proxy. The
1450
+ // helper bypasses the `hasNoClient` gate so the single-capability
1451
+ // chrome-extension turn can drive the browser via CDP without leaking
1452
+ // host_bash/host_file tool availability into tool gating.
1453
+ //
1454
+ // `restoreBrowserProxyAvailability()` reads `hostBrowserSenderOverride`
1455
+ // (set above for chrome-extension) and applies the registry-routed
1456
+ // sender, so the chrome-extension path gets the correct sender here
1457
+ // — including after queue-drain restores run from conversation-process.ts,
1458
+ // which only have access to the conversation instance.
1459
+ if (supportsHostProxy(sourceInterface, "host_browser")) {
1460
+ conversation.restoreBrowserProxyAvailability?.();
1461
+ }
1188
1462
 
1189
1463
  // ── Canned first-greeting fast path ──
1190
1464
  // On a completely fresh workspace, skip LLM inference for the macOS
@@ -4,13 +4,17 @@
4
4
  * GET /v1/conversation-starters — list conversation starters (chips)
5
5
  */
6
6
 
7
- import { and, desc, eq, inArray, like } from "drizzle-orm";
7
+ import { and, desc, eq, inArray, sql } from "drizzle-orm";
8
8
  import { z } from "zod";
9
9
 
10
10
  import { getDb } from "../../memory/db.js";
11
11
  import { enqueueMemoryJob } from "../../memory/jobs-store.js";
12
12
  import { rawGet } from "../../memory/raw-query.js";
13
- import { conversationStarters, memoryJobs } from "../../memory/schema.js";
13
+ import {
14
+ conversationStarters,
15
+ memoryCheckpoints,
16
+ memoryJobs,
17
+ } from "../../memory/schema.js";
14
18
  import type { RouteDefinition } from "../http-router.js";
15
19
 
16
20
  // ---------------------------------------------------------------------------
@@ -26,6 +30,39 @@ interface StarterItem {
26
30
  batch: number;
27
31
  }
28
32
 
33
+ const CK_ITEM_COUNT = "conversation_starters:item_count_at_last_gen";
34
+ const CK_LAST_GEN_AT = "conversation_starters:last_gen_at";
35
+ export const CONVERSATION_STARTERS_STALE_TTL_MS = 24 * 60 * 60 * 1000;
36
+
37
+ function checkpointKey(base: string, scopeId: string): string {
38
+ return `${base}:${scopeId}`;
39
+ }
40
+
41
+ function parseCheckpointInt(value: string | undefined): number | null {
42
+ if (!value) return null;
43
+ const parsed = Number.parseInt(value, 10);
44
+ return Number.isFinite(parsed) ? parsed : null;
45
+ }
46
+
47
+ function hasActiveConversationStarterJob(
48
+ db: ReturnType<typeof getDb>,
49
+ scopeId: string,
50
+ ): boolean {
51
+ return (
52
+ db
53
+ .select({ id: memoryJobs.id })
54
+ .from(memoryJobs)
55
+ .where(
56
+ and(
57
+ eq(memoryJobs.type, "generate_conversation_starters"),
58
+ inArray(memoryJobs.status, ["pending", "running"]),
59
+ sql`json_extract(${memoryJobs.payload}, '$.scopeId') = ${scopeId}`,
60
+ ),
61
+ )
62
+ .get() != null
63
+ );
64
+ }
65
+
29
66
  /**
30
67
  * Re-order starters so adjacent items have distinct categories wherever
31
68
  * possible. Within each category, preserve the original (batch-desc) order.
@@ -150,14 +187,47 @@ function handleListConversationStarters(url: URL): Response {
150
187
 
151
188
  const total = allItems.length;
152
189
 
153
- // If starters exist, reorder for category diversity then paginate.
190
+ // If starters exist, return them immediately. If the batch is stale or
191
+ // the generation checkpoint is ahead of the current active memory count,
192
+ // kick off a background refresh but keep the existing chips visible.
154
193
  if (total > 0) {
194
+ const totalActive =
195
+ rawGet<{ c: number }>(
196
+ `SELECT COUNT(*) AS c FROM memory_graph_nodes WHERE fidelity != 'gone' AND scope_id = ?`,
197
+ scopeId,
198
+ )?.c ?? 0;
199
+ const lastCount = parseCheckpointInt(
200
+ db
201
+ .select({ value: memoryCheckpoints.value })
202
+ .from(memoryCheckpoints)
203
+ .where(eq(memoryCheckpoints.key, checkpointKey(CK_ITEM_COUNT, scopeId)))
204
+ .get()?.value,
205
+ );
206
+ const lastGenAt = parseCheckpointInt(
207
+ db
208
+ .select({ value: memoryCheckpoints.value })
209
+ .from(memoryCheckpoints)
210
+ .where(eq(memoryCheckpoints.key, checkpointKey(CK_LAST_GEN_AT, scopeId)))
211
+ .get()?.value,
212
+ );
213
+ const staleByAge =
214
+ lastGenAt == null ||
215
+ Date.now() - lastGenAt >= CONVERSATION_STARTERS_STALE_TTL_MS;
216
+ const checkpointAhead = lastCount != null && totalActive < lastCount;
217
+ let hasActiveJob = hasActiveConversationStarterJob(db, scopeId);
218
+ const shouldRefresh = staleByAge || checkpointAhead;
219
+
220
+ if (shouldRefresh && !hasActiveJob) {
221
+ enqueueMemoryJob("generate_conversation_starters", { scopeId });
222
+ hasActiveJob = true;
223
+ }
224
+
155
225
  const ordered = orderStrongestFirst(allItems);
156
226
  const page = ordered.slice(offsetParam, offsetParam + limitParam);
157
227
  return Response.json({
158
228
  starters: page,
159
229
  total,
160
- status: "ready",
230
+ status: hasActiveJob ? "refreshing" : "ready",
161
231
  });
162
232
  }
163
233
 
@@ -172,17 +242,7 @@ function handleListConversationStarters(url: URL): Response {
172
242
  }
173
243
 
174
244
  // Memory items exist but no starters yet — ensure a generation job is queued.
175
- const existing = db
176
- .select({ id: memoryJobs.id })
177
- .from(memoryJobs)
178
- .where(
179
- and(
180
- eq(memoryJobs.type, "generate_conversation_starters"),
181
- inArray(memoryJobs.status, ["pending", "running"]),
182
- like(memoryJobs.payload, `%"scopeId":"${scopeId}"%`),
183
- ),
184
- )
185
- .get();
245
+ const existing = hasActiveConversationStarterJob(db, scopeId);
186
246
 
187
247
  if (!existing) {
188
248
  enqueueMemoryJob("generate_conversation_starters", { scopeId });
@@ -227,7 +287,9 @@ export function conversationStarterRouteDefinitions(): RouteDefinition[] {
227
287
  .array(z.unknown())
228
288
  .describe("Ordered list of starter chips"),
229
289
  total: z.number().int().describe("Total number of available starters"),
230
- status: z.string().describe("One of: ready, empty, generating"),
290
+ status: z
291
+ .enum(["ready", "refreshing", "empty", "generating"])
292
+ .describe("One of: ready, refreshing, empty, generating"),
231
293
  }),
232
294
  },
233
295
  ];
@@ -19,13 +19,14 @@ import {
19
19
  type CanonicalGuardianRequest,
20
20
  listPendingRequestsByConversationScope,
21
21
  } from "../../memory/canonical-guardian-store.js";
22
+ import { isPermissionControlsV2Enabled } from "../../permissions/v2-consent-policy.js";
22
23
  import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
23
24
  import type { AuthContext } from "../auth/types.js";
24
25
  import { processGuardianDecision } from "../guardian-action-service.js";
25
26
  import type { GuardianDecisionPrompt } from "../guardian-decision-types.js";
26
27
  import {
27
28
  buildDecisionActions,
28
- GUARDIAN_DECISION_ACTIONS,
29
+ buildOneTimeDecisionActions,
29
30
  } from "../guardian-decision-types.js";
30
31
  import { httpError } from "../http-errors.js";
31
32
  import type { RouteDefinition } from "../http-router.js";
@@ -98,6 +99,18 @@ export async function handleGuardianActionDecision(
98
99
  return httpError("BAD_REQUEST", "action is required", 400);
99
100
  }
100
101
 
102
+ if (
103
+ isPermissionControlsV2Enabled() &&
104
+ action !== "approve_once" &&
105
+ action !== "reject"
106
+ ) {
107
+ return httpError(
108
+ "FORBIDDEN",
109
+ "permission-controls-v2 only accepts approve_once or reject for guardian actions",
110
+ 403,
111
+ );
112
+ }
113
+
101
114
  // Resolve the actor's guardian principal ID. For JWT-verified actors this
102
115
  // comes from the token claims. For dev bypass (HTTP auth disabled) the
103
116
  // synthetic "dev-bypass" principal won't match the real guardian binding,
@@ -193,15 +206,17 @@ export function listGuardianDecisionPrompts(params: {
193
206
  * Map a canonical guardian request to the client-facing prompt format.
194
207
  *
195
208
  * Generates kind-specific questionText and action sets:
196
- * - `tool_approval`: temporal modes (approve_once, approve_10m, approve_conversation) + reject
209
+ * - `tool_approval`: temporal modes (approve_once, approve_10m, approve_conversation) + reject in legacy mode,
210
+ * approve_once + reject only under v2
197
211
  * - `pending_question`: approve_once + reject only
198
212
  * - `access_request`: approve_once + reject only, with text fallback instructions
199
213
  * (request code + "open invite flow")
200
214
  * - `tool_grant_request`: approve_once + reject only
201
215
  *
202
- * Only `tool_approval` receives temporal modes because time-scoped grants
203
- * are meaningful only for tool execution. All other kinds get a simple
204
- * approve_once/reject pair.
216
+ * Under permission-controls-v2 we collapse all deterministic guardian action
217
+ * prompts to approve_once + reject only. Outside v2, only `tool_approval`
218
+ * receives temporal modes because time-scoped grants are meaningful only for
219
+ * tool execution.
205
220
  */
206
221
  function mapCanonicalRequestToPrompt(
207
222
  req: CanonicalGuardianRequest,
@@ -209,15 +224,11 @@ function mapCanonicalRequestToPrompt(
209
224
  ): GuardianDecisionPrompt {
210
225
  const questionText = buildKindAwareQuestionText(req);
211
226
 
212
- // Only tool_approval gets temporal modes (approve_10m, approve_conversation);
213
- // all other kinds get a simple approve_once + reject pair.
214
- const actions =
215
- req.kind === "tool_approval"
227
+ const actions = isPermissionControlsV2Enabled()
228
+ ? buildOneTimeDecisionActions()
229
+ : req.kind === "tool_approval"
216
230
  ? buildDecisionActions({ forGuardianOnBehalf: true })
217
- : [
218
- GUARDIAN_DECISION_ACTIONS.approve_once,
219
- GUARDIAN_DECISION_ACTIONS.reject,
220
- ];
231
+ : buildOneTimeDecisionActions();
221
232
 
222
233
  const expiresAt = req.expiresAt
223
234
  ? new Date(req.expiresAt).getTime()