@vellumai/assistant 0.11.7 → 0.11.8-staging.1

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 (446) hide show
  1. package/ARCHITECTURE.md +10 -10
  2. package/Dockerfile +1 -0
  3. package/docs/architecture/security.md +1 -1
  4. package/docs/error-handling.md +1 -2
  5. package/docs/guardian-request-flow.md +51 -5
  6. package/docs/trusted-contact-access.md +13 -12
  7. package/knip.json +1 -0
  8. package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
  9. package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
  10. package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
  11. package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
  12. package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
  13. package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  18. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
  19. package/node_modules/@vellumai/environments/package.json +2 -1
  20. package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
  21. package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  24. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  25. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  26. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
  27. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
  28. package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
  29. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
  30. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
  31. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
  32. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
  33. package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
  34. package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
  35. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  36. package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
  37. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  38. package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
  39. package/openapi.yaml +481 -9
  40. package/package.json +3 -1
  41. package/scripts/sync-llm-catalog.ts +3 -0
  42. package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
  43. package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
  44. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
  45. package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
  46. package/src/__tests__/anthropic-provider.test.ts +56 -0
  47. package/src/__tests__/approval-cascade.test.ts +0 -13
  48. package/src/__tests__/attachments-store.test.ts +117 -6
  49. package/src/__tests__/attachments.test.ts +122 -0
  50. package/src/__tests__/bundled-asset.test.ts +22 -1
  51. package/src/__tests__/channel-approval.test.ts +14 -7
  52. package/src/__tests__/channel-approvals.test.ts +3 -3
  53. package/src/__tests__/channel-delivery-store.test.ts +58 -0
  54. package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
  55. package/src/__tests__/channel-invite-transport.test.ts +1 -0
  56. package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
  57. package/src/__tests__/compaction-events.test.ts +0 -12
  58. package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
  59. package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
  60. package/src/__tests__/config-schema.test.ts +35 -0
  61. package/src/__tests__/contacts-write.test.ts +141 -3
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
  65. package/src/__tests__/conversation-agent-loop.test.ts +95 -13
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
  67. package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
  68. package/src/__tests__/conversation-media-retry.test.ts +33 -2
  69. package/src/__tests__/conversation-pairing.test.ts +54 -0
  70. package/src/__tests__/conversation-process-callsite.test.ts +0 -12
  71. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
  72. package/src/__tests__/conversation-queue.test.ts +4 -13
  73. package/src/__tests__/conversation-retry-route.test.ts +5 -0
  74. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  75. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  76. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  77. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  78. package/src/__tests__/conversation-sight-frames.test.ts +615 -0
  79. package/src/__tests__/conversation-slash-queue.test.ts +0 -13
  80. package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
  81. package/src/__tests__/conversation-speed-override.test.ts +0 -12
  82. package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
  83. package/src/__tests__/conversation-summarize-route.test.ts +5 -0
  84. package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
  85. package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
  86. package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
  87. package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
  88. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
  89. package/src/__tests__/credential-health-service.test.ts +45 -0
  90. package/src/__tests__/credential-record-retire.test.ts +256 -0
  91. package/src/__tests__/credential-record-write-through.test.ts +8 -4
  92. package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
  93. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
  94. package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
  95. package/src/__tests__/delete-propagation.test.ts +121 -3
  96. package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
  97. package/src/__tests__/file-ops-service.test.ts +8 -3
  98. package/src/__tests__/gemini-provider.test.ts +64 -0
  99. package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
  100. package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
  101. package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
  102. package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
  103. package/src/__tests__/guardian-gateway-sim.ts +56 -20
  104. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
  105. package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
  106. package/src/__tests__/guardian-wait-controller.test.ts +6 -0
  107. package/src/__tests__/headless-browser-mode.test.ts +27 -0
  108. package/src/__tests__/helpers/mock-conversation.ts +2 -0
  109. package/src/__tests__/list-messages-system-card.test.ts +70 -0
  110. package/src/__tests__/llm-catalog-parity.test.ts +2 -0
  111. package/src/__tests__/managed-profile-guard.test.ts +2 -2
  112. package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
  113. package/src/__tests__/media-stream-server-integration.test.ts +6 -0
  114. package/src/__tests__/non-member-access-request.test.ts +12 -0
  115. package/src/__tests__/notification-discord-adapter.test.ts +76 -4
  116. package/src/__tests__/oauth-commands-routes.test.ts +21 -0
  117. package/src/__tests__/openai-provider.test.ts +63 -0
  118. package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
  119. package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
  120. package/src/__tests__/persist-unsendable-image.test.ts +19 -0
  121. package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
  122. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
  123. package/src/__tests__/process-message-background-slack.test.ts +1 -1
  124. package/src/__tests__/process-message-display-content.test.ts +1 -1
  125. package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
  126. package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
  127. package/src/__tests__/qdrant-manager.test.ts +6 -0
  128. package/src/__tests__/reaction-persistence.test.ts +58 -62
  129. package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
  130. package/src/__tests__/server-history-render.test.ts +22 -0
  131. package/src/__tests__/sight-frame-retention.test.ts +362 -0
  132. package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
  133. package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
  134. package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
  135. package/src/__tests__/subagent-manager-notify.test.ts +2 -0
  136. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  137. package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
  138. package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
  139. package/src/__tests__/terminal-tools.test.ts +9 -9
  140. package/src/__tests__/tool-result-spool.test.ts +5 -8
  141. package/src/__tests__/usage-attribution.test.ts +26 -0
  142. package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
  143. package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
  144. package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
  145. package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
  146. package/src/acp/acp-auth-marker-store.ts +196 -0
  147. package/src/acp/acp-claude-oauth.ts +154 -4
  148. package/src/acp/acp-connect-card-state.ts +49 -8
  149. package/src/acp/agent-process.ts +1 -0
  150. package/src/acp/auto-install.ts +1 -0
  151. package/src/acp/client-handler.ts +1 -0
  152. package/src/acp/prepare-agent-env.ts +79 -1
  153. package/src/acp/resolve-agent.ts +12 -0
  154. package/src/acp/session-manager.test.ts +183 -0
  155. package/src/acp/session-manager.ts +110 -6
  156. package/src/acp/types.ts +17 -0
  157. package/src/agent/attachments.ts +7 -2
  158. package/src/agent/loop.ts +59 -1
  159. package/src/api/events/contact-form-closed.ts +32 -0
  160. package/src/api/events/contact-record-request.ts +63 -0
  161. package/src/api/events/contact-request.ts +10 -3
  162. package/src/api/index.ts +20 -0
  163. package/src/api/responses/conversation-message.ts +20 -0
  164. package/src/api/responses/home.ts +101 -0
  165. package/src/approvals/AGENTS.md +1 -1
  166. package/src/approvals/guardian-card-withdrawal.ts +127 -30
  167. package/src/approvals/guardian-decision-primitive.ts +9 -0
  168. package/src/approvals/guardian-expiry-notifier.ts +22 -16
  169. package/src/approvals/guardian-request-status-sync.ts +98 -0
  170. package/src/avatar/svg-compositor.ts +1 -1
  171. package/src/avatar/traits-png-sync.ts +1 -1
  172. package/src/bundler/app-compiler.ts +1 -0
  173. package/src/bundler/compiler-tools.ts +1 -0
  174. package/src/bundler/package-resolver.ts +1 -0
  175. package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
  176. package/src/calls/guardian-action-sweep.ts +6 -1
  177. package/src/calls/voice-session-bridge.ts +46 -1
  178. package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
  179. package/src/channels/gateway-guardian-requests.ts +37 -18
  180. package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
  181. package/src/cli/commands/__tests__/browser.test.ts +65 -1
  182. package/src/cli/commands/browser.ts +30 -10
  183. package/src/cli/commands/contacts.help.ts +173 -11
  184. package/src/cli/commands/contacts.ts +343 -4
  185. package/src/cli/commands/db/refresh.ts +1 -0
  186. package/src/cli/commands/oauth/index.help.ts +6 -3
  187. package/src/cli/commands/oauth/request.test.ts +37 -0
  188. package/src/cli/commands/oauth/request.ts +7 -6
  189. package/src/cli/lib/bundled-marketplace.json +1 -1
  190. package/src/cli/lib/install-from-github.ts +20 -19
  191. package/src/cli/lib/install-plugin-dependencies.ts +3 -5
  192. package/src/cli/lib/merge-plugin-tree.ts +1 -0
  193. package/src/cli/lib/publish-plugin.ts +4 -1
  194. package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
  195. package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
  196. package/src/config/balanced-model-experiment.ts +15 -9
  197. package/src/config/bundled-skills/contacts/SKILL.md +61 -18
  198. package/src/config/default-profile-catalog.ts +12 -11
  199. package/src/config/feature-flag-registry.json +22 -13
  200. package/src/config/schema.ts +2 -0
  201. package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
  202. package/src/config/schemas/live-voice.ts +7 -0
  203. package/src/config/schemas/sight.ts +45 -0
  204. package/src/config/skills.ts +2 -1
  205. package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
  206. package/src/contacts/contact-store.ts +47 -4
  207. package/src/contacts/contacts-write.ts +5 -2
  208. package/src/contacts/mirror-reconciler.ts +210 -0
  209. package/src/contacts/mirror-tombstones.ts +42 -0
  210. package/src/contacts/notify-contacts-changed.ts +11 -2
  211. package/src/context/compactor.ts +47 -19
  212. package/src/context/tool-result-spool.ts +21 -14
  213. package/src/conversations/message-consolidation.ts +2 -2
  214. package/src/credential-health/credential-health-service.ts +37 -6
  215. package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
  216. package/src/daemon/conversation-agent-loop.ts +100 -1
  217. package/src/daemon/conversation-media-retry.ts +11 -6
  218. package/src/daemon/conversation-messaging.ts +293 -30
  219. package/src/daemon/conversation-runtime-assembly.ts +67 -0
  220. package/src/daemon/conversation-sight-frames.ts +187 -0
  221. package/src/daemon/conversation-store.ts +18 -28
  222. package/src/daemon/conversation-surfaces.ts +12 -2
  223. package/src/daemon/conversation-tool-setup.ts +39 -10
  224. package/src/daemon/conversation.ts +159 -16
  225. package/src/daemon/daemon-control.ts +6 -17
  226. package/src/daemon/handlers/config-model.test.ts +1 -0
  227. package/src/daemon/handlers/conversations.ts +10 -11
  228. package/src/daemon/install-assistant-command.ts +1 -0
  229. package/src/daemon/lifecycle.ts +10 -13
  230. package/src/daemon/message-types/sync.ts +4 -0
  231. package/src/daemon/process-message.ts +2 -17
  232. package/src/daemon/reaction-history-render.test.ts +162 -0
  233. package/src/daemon/reaction-history-render.ts +115 -0
  234. package/src/daemon/reaction-record.test.ts +162 -0
  235. package/src/daemon/reaction-record.ts +83 -0
  236. package/src/daemon/shutdown-handlers.ts +0 -2
  237. package/src/daemon/summarize-boundary.ts +12 -0
  238. package/src/daemon/video-thumbnail.ts +1 -1
  239. package/src/home/feed-types.ts +4 -0
  240. package/src/home/feed-writer.ts +25 -0
  241. package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
  242. package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
  243. package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
  244. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
  245. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
  246. package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
  247. package/src/live-voice/live-voice-photo.ts +420 -65
  248. package/src/live-voice/live-voice-session.ts +311 -27
  249. package/src/live-voice/protocol.ts +147 -11
  250. package/src/messaging/provider-message-metadata.ts +7 -0
  251. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
  252. package/src/messaging/providers/channel-transport.ts +37 -0
  253. package/src/messaging/providers/discord/api.ts +4 -4
  254. package/src/messaging/providers/discord/send.test.ts +177 -2
  255. package/src/messaging/providers/discord/send.ts +197 -10
  256. package/src/messaging/providers/discord/transport.ts +10 -0
  257. package/src/messaging/providers/index.ts +42 -0
  258. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
  259. package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
  260. package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
  261. package/src/messaging/providers/slack/adapter.ts +112 -27
  262. package/src/messaging/providers/slack/api.test.ts +10 -0
  263. package/src/messaging/providers/slack/auth.ts +32 -6
  264. package/src/messaging/providers/slack/client.ts +0 -15
  265. package/src/messaging/providers/slack/message-content.ts +137 -0
  266. package/src/messaging/providers/slack/send.test.ts +43 -2
  267. package/src/messaging/providers/slack/send.ts +18 -5
  268. package/src/messaging/providers/slack/transport.ts +10 -0
  269. package/src/messaging/providers/slack/types.ts +21 -2
  270. package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
  271. package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
  272. package/src/messaging/providers/telegram-bot/send.ts +41 -1
  273. package/src/messaging/providers/telegram-bot/transport.ts +10 -0
  274. package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
  275. package/src/messaging/reaction-envelopes.ts +79 -0
  276. package/src/monitoring/control.ts +2 -2
  277. package/src/monitoring/page-cache.ts +5 -1
  278. package/src/notifications/AGENTS.md +1 -1
  279. package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
  280. package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
  281. package/src/notifications/adapters/discord.ts +62 -9
  282. package/src/notifications/conversation-pairing.ts +31 -4
  283. package/src/notifications/emit-signal.ts +8 -5
  284. package/src/notifications/guardian-delivery-recorder.ts +47 -6
  285. package/src/notifications/guardian-feed-projection.ts +492 -0
  286. package/src/notifications/home-feed-side-effect.ts +59 -11
  287. package/src/oauth/byo-connection.test.ts +22 -0
  288. package/src/oauth/byo-connection.ts +11 -5
  289. package/src/oauth/connection.ts +3 -2
  290. package/src/oauth/platform-connection.test.ts +35 -0
  291. package/src/oauth/platform-connection.ts +18 -13
  292. package/src/oauth/scope-utils.ts +48 -0
  293. package/src/oauth/seed-providers.ts +12 -0
  294. package/src/outbound-proxy/certs.ts +5 -4
  295. package/src/permissions/question-guardian-request.test.ts +7 -1
  296. package/src/persistence/attachments-store.ts +134 -31
  297. package/src/persistence/conversation-crud.ts +137 -4
  298. package/src/persistence/conversation-types.ts +109 -0
  299. package/src/persistence/db-connection.ts +3 -21
  300. package/src/persistence/delivery-crud.ts +42 -12
  301. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
  302. package/src/persistence/embeddings/embedding-local.ts +7 -112
  303. package/src/persistence/embeddings/qdrant-manager.ts +4 -1
  304. package/src/persistence/llm-request-log-store.ts +1 -1
  305. package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
  306. package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
  307. package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
  308. package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
  309. package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
  310. package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
  311. package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
  312. package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
  313. package/src/persistence/schema/acp.ts +25 -0
  314. package/src/persistence/steps.ts +8 -0
  315. package/src/platform/platform-patch-queue.test.ts +43 -41
  316. package/src/plugin-api/index.ts +12 -0
  317. package/src/plugins/defaults/compaction/window-manager.ts +2 -0
  318. package/src/plugins/defaults/image-recovery/recover.ts +12 -1
  319. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
  320. package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
  321. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
  322. package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
  323. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
  324. package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
  325. package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
  326. package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
  327. package/src/plugins/defaults/memory/worker-control.ts +1 -2
  328. package/src/providers/content-block-schema.ts +1 -0
  329. package/src/providers/fireworks/client.ts +9 -1
  330. package/src/providers/gemini/client.ts +22 -15
  331. package/src/providers/media-resolve.ts +24 -10
  332. package/src/providers/model-catalog.ts +91 -0
  333. package/src/providers/openai/chat-completions-provider.ts +44 -5
  334. package/src/providers/retry.ts +16 -11
  335. package/src/providers/types.ts +56 -0
  336. package/src/routes/__tests__/control.test.ts +4 -3
  337. package/src/routes/control.ts +1 -1
  338. package/src/routes/route-host-client.ts +1 -1
  339. package/src/runtime/AGENTS.md +30 -12
  340. package/src/runtime/__tests__/agent-wake.test.ts +71 -6
  341. package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
  342. package/src/runtime/access-request-helper.ts +15 -7
  343. package/src/runtime/agent-wake.ts +16 -4
  344. package/src/runtime/channel-approval-types.ts +8 -5
  345. package/src/runtime/channel-invite-transport.ts +2 -0
  346. package/src/runtime/channel-invite-transports/discord.ts +27 -0
  347. package/src/runtime/channel-readiness-service.ts +4 -4
  348. package/src/runtime/channel-reply-delivery.ts +72 -28
  349. package/src/runtime/guardian-action-service.ts +9 -5
  350. package/src/runtime/guardian-form-registry.ts +256 -0
  351. package/src/runtime/guardian-reply-router.ts +33 -14
  352. package/src/runtime/invite-instruction-generator.ts +14 -2
  353. package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
  354. package/src/runtime/no-response.ts +13 -74
  355. package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
  356. package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
  357. package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
  358. package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
  359. package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
  360. package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
  361. package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
  362. package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
  363. package/src/runtime/routes/acp-routes-list.test.ts +58 -0
  364. package/src/runtime/routes/acp-routes.ts +253 -47
  365. package/src/runtime/routes/app-management-routes.ts +80 -47
  366. package/src/runtime/routes/approval-routes.ts +9 -0
  367. package/src/runtime/routes/archive-utils.ts +6 -1
  368. package/src/runtime/routes/avatar-routes.ts +1 -1
  369. package/src/runtime/routes/channel-route-shared.ts +18 -20
  370. package/src/runtime/routes/contact-prompt-routes.ts +271 -105
  371. package/src/runtime/routes/conversation-routes.ts +41 -32
  372. package/src/runtime/routes/credential-prompt-routes.ts +10 -1
  373. package/src/runtime/routes/debug-bash-routes.ts +1 -0
  374. package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
  375. package/src/runtime/routes/guardian-form-routes.ts +115 -0
  376. package/src/runtime/routes/inbound-message-handler.ts +82 -29
  377. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
  378. package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
  379. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
  380. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
  381. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
  382. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
  383. package/src/runtime/routes/index.ts +2 -0
  384. package/src/runtime/routes/integrations/slack/channel.ts +39 -18
  385. package/src/runtime/routes/migration-routes.ts +1 -3
  386. package/src/runtime/routes/monitoring-routes.ts +0 -3
  387. package/src/runtime/routes/oauth-commands-routes.ts +18 -5
  388. package/src/runtime/routes/route-host-worker-routes.ts +1 -1
  389. package/src/runtime/routes/schedule-worker-routes.ts +1 -1
  390. package/src/runtime/tool-grant-request-helper.ts +4 -0
  391. package/src/schedule/run-script.ts +1 -0
  392. package/src/schedule/scheduler.ts +1 -1
  393. package/src/schedule/worker-control.ts +2 -2
  394. package/src/security/oauth2.ts +2 -11
  395. package/src/security/secure-keys.ts +55 -8
  396. package/src/skills/catalog-install.ts +7 -4
  397. package/src/skills/clawhub.ts +1 -0
  398. package/src/skills/inline-command-runner.ts +1 -0
  399. package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
  400. package/src/stt/room-noise-floor.ts +140 -0
  401. package/src/subagent/notify.ts +49 -3
  402. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  403. package/src/tools/acp/spawn.ts +2 -2
  404. package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
  405. package/src/tools/browser/browser-execution.ts +10 -43
  406. package/src/tools/browser/browser-manager.ts +1 -0
  407. package/src/tools/browser/browser-status-constants.ts +6 -5
  408. package/src/tools/browser/runtime-check.ts +2 -0
  409. package/src/tools/channel/react-to-message.test.ts +193 -0
  410. package/src/tools/channel/react-to-message.ts +159 -0
  411. package/src/tools/credentials/metadata-store.ts +115 -19
  412. package/src/tools/credentials/store.ts +0 -2
  413. package/src/tools/filesystem/read.ts +2 -2
  414. package/src/tools/host-filesystem/read.ts +2 -2
  415. package/src/tools/host-terminal/host-shell.test.ts +24 -0
  416. package/src/tools/host-terminal/host-shell.ts +11 -38
  417. package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
  418. package/src/tools/shared/shell-output.test.ts +104 -0
  419. package/src/tools/shared/shell-output.ts +112 -39
  420. package/src/tools/skills/sandbox-runner.ts +1 -0
  421. package/src/tools/system/request-permission.test.ts +34 -0
  422. package/src/tools/system/request-permission.ts +29 -5
  423. package/src/tools/terminal/shell.test.ts +63 -0
  424. package/src/tools/terminal/shell.ts +12 -44
  425. package/src/tools/tool-input-schemas.ts +2 -0
  426. package/src/tools/tool-manifest.ts +2 -0
  427. package/src/usage/attribution.ts +12 -4
  428. package/src/util/__tests__/worker-ownership.test.ts +77 -0
  429. package/src/util/__tests__/worker-process-command.test.ts +246 -1
  430. package/src/util/bun-runtime.ts +44 -13
  431. package/src/util/bundled-asset.ts +9 -1
  432. package/src/util/disk-usage.ts +1 -0
  433. package/src/util/file-use.ts +4 -1
  434. package/src/util/guardian-form-timeouts.ts +50 -0
  435. package/src/util/image-conversion.ts +1 -0
  436. package/src/util/oauth-request-body.test.ts +59 -0
  437. package/src/util/oauth-request-body.ts +56 -5
  438. package/src/util/platform.ts +69 -73
  439. package/src/util/process-table.ts +16 -5
  440. package/src/util/read-stdin.ts +5 -0
  441. package/src/util/spawn.ts +1 -0
  442. package/src/util/sqlite3-runtime.ts +25 -5
  443. package/src/util/worker-ownership.ts +193 -0
  444. package/src/util/worker-process.ts +375 -37
  445. package/src/workspace/git-service.ts +4 -12
  446. package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Tests for what `contacts create` / `update` / `delete` put in front of the
3
+ * guardian.
4
+ *
5
+ * The form is seeded from these bodies and the guardian submits what it shows,
6
+ * so a field the CLI leaves empty is a field the guardian unknowingly writes
7
+ * over. That makes the request body, not just the eventual write, the thing
8
+ * worth pinning.
9
+ */
10
+
11
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
12
+
13
+ import { GUARDIAN_FORM_SETTLE_MS } from "../../util/guardian-form-timeouts.js";
14
+
15
+ interface IpcCall {
16
+ operationId: string;
17
+ options?: Record<string, unknown>;
18
+ /** Transport options, where the socket deadline lives. */
19
+ callOptions?: { timeoutMs?: number };
20
+ }
21
+
22
+ let calls: IpcCall[] = [];
23
+
24
+ const contact = {
25
+ id: "ct_1",
26
+ displayName: "Alice",
27
+ notes: "Dentist, referred by Bob",
28
+ contactType: "human",
29
+ createdAt: 0,
30
+ updatedAt: 0,
31
+ interactionCount: 0,
32
+ channels: [
33
+ {
34
+ id: "ch_1",
35
+ contactId: "ct_1",
36
+ type: "email",
37
+ address: "alice@example.com",
38
+ },
39
+ ],
40
+ };
41
+
42
+ /** What a contact with no notes actually looks like on the wire. */
43
+ const contactWithoutNotes = { ...contact, notes: null };
44
+
45
+ let contactForRead: Record<string, unknown> = contact;
46
+ /** Make the display readback fail, as a transient IPC hiccup would. */
47
+ let readFails = false;
48
+
49
+ /**
50
+ * The ordinary answers. Held as a named function so `beforeEach` can put it
51
+ * back: a test that installs its own implementation would otherwise leave it
52
+ * in place for everything after it, since clearing a mock keeps the last
53
+ * implementation.
54
+ */
55
+ /** Whether the daemon reports the submitted notes as having reached storage. */
56
+ let notesSaved: boolean | undefined;
57
+ /** Whether the daemon reports that nothing the guardian submitted landed. */
58
+ let nothingWritten: boolean | undefined;
59
+
60
+ const baseIpcImplementation = async (
61
+ operationId: string,
62
+ options?: Record<string, unknown>,
63
+ callOptions?: { timeoutMs?: number },
64
+ ) => {
65
+ calls.push({ operationId, options, callOptions });
66
+ if (operationId === "getContact") {
67
+ if (readFails) {
68
+ // 404-shaped, as a gateway-backed read reports a contact it cannot see.
69
+ return { ok: false, error: "Contact not found", statusCode: 404 };
70
+ }
71
+ return { ok: true, result: { ok: true, contact: contactForRead } };
72
+ }
73
+ return {
74
+ ok: true,
75
+ result: { ok: true, contactId: contact.id, notesSaved, nothingWritten },
76
+ };
77
+ };
78
+
79
+ const cliIpcCallMock = mock(baseIpcImplementation);
80
+
81
+ const actualCliClient = await import("../../ipc/cli-client.js");
82
+ mock.module("../../ipc/cli-client.js", () => ({
83
+ ...actualCliClient,
84
+ cliIpcCall: cliIpcCallMock,
85
+ }));
86
+
87
+ const { runAssistantCommand } = await import("./run-assistant-command.js");
88
+
89
+ function recordPromptBody(): Record<string, unknown> {
90
+ const call = calls.find((c) => c.operationId === "contacts_record_prompt");
91
+ expect(call).toBeDefined();
92
+ return (call!.options as { body: Record<string, unknown> }).body;
93
+ }
94
+
95
+ describe("contacts record prompts", () => {
96
+ beforeEach(() => {
97
+ calls = [];
98
+ contactForRead = contact;
99
+ readFails = false;
100
+ notesSaved = undefined;
101
+ nothingWritten = undefined;
102
+ // Global and sticky: the failure-path cases below set it, and a later test
103
+ // asserting success would otherwise read their exit code as its own.
104
+ // Cleared to 0 rather than undefined, which does not reset it.
105
+ process.exitCode = 0;
106
+ cliIpcCallMock.mockClear();
107
+ cliIpcCallMock.mockImplementation(baseIpcImplementation);
108
+ });
109
+
110
+ test("a name-only update carries the notes the contact already has", async () => {
111
+ await runAssistantCommand(
112
+ "contacts",
113
+ "update",
114
+ "ct_1",
115
+ "--name",
116
+ "Alice Chen",
117
+ );
118
+
119
+ const body = recordPromptBody();
120
+ expect(body.operation).toBe("update");
121
+ expect(body.displayName).toBe("Alice Chen");
122
+ // The form compares against what is stored, so it has to be told.
123
+ expect(body.currentNotes).toBe("Dentist, referred by Bob");
124
+ // Without this the form shows an empty notes box and the guardian submits
125
+ // it, erasing the notes as a side effect of a rename.
126
+ expect(body.notes).toBe("Dentist, referred by Bob");
127
+ expect(body.currentDisplayName).toBe("Alice");
128
+ });
129
+
130
+ test("an explicit empty --notes stays empty, so clearing still works", async () => {
131
+ await runAssistantCommand("contacts", "update", "ct_1", "--notes", "");
132
+
133
+ expect(recordPromptBody().notes).toBe("");
134
+ });
135
+
136
+ test("--notes overrides the existing value", async () => {
137
+ await runAssistantCommand(
138
+ "contacts",
139
+ "update",
140
+ "ct_1",
141
+ "--notes",
142
+ "Moved to Berlin",
143
+ );
144
+
145
+ expect(recordPromptBody().notes).toBe("Moved to Berlin");
146
+ });
147
+
148
+ test("a contact with no notes still opens the form", async () => {
149
+ // `notes` is nullable on the wire, and the daemon's schema takes a string
150
+ // or nothing. Passing the null through would reject the request and the
151
+ // guardian would never see a form.
152
+ contactForRead = contactWithoutNotes;
153
+
154
+ await runAssistantCommand(
155
+ "contacts",
156
+ "update",
157
+ "ct_1",
158
+ "--name",
159
+ "Alice Chen",
160
+ );
161
+
162
+ const body = recordPromptBody();
163
+ expect(body.notes).toBeUndefined();
164
+ expect("notes" in body ? body.notes : undefined).not.toBeNull();
165
+ });
166
+
167
+ test("the requested timeout bounds the form, not just the socket", async () => {
168
+ // A CLI that gave up first would report failure while the form stayed
169
+ // open, and a later answer would write something the caller was told had
170
+ // not happened.
171
+ await runAssistantCommand(
172
+ "contacts",
173
+ "create",
174
+ "--name",
175
+ "Alice",
176
+ "--timeout",
177
+ "1000",
178
+ );
179
+
180
+ const call = calls.find((c) => c.operationId === "contacts_record_prompt");
181
+ const body = (call!.options as { body: Record<string, unknown> }).body;
182
+ expect(body.timeoutMs).toBe(1000);
183
+ // The socket has to outlast an answer arriving at the deadline, which
184
+ // starts the settle window from there. Giving up sooner would report a
185
+ // failure while the write went on to commit.
186
+ expect(call!.callOptions?.timeoutMs).toBeGreaterThan(
187
+ 1000 + GUARDIAN_FORM_SETTLE_MS,
188
+ );
189
+ });
190
+
191
+ test.each(["1e3", "100.5", "300000ms", "abc", "-5"])(
192
+ "a timeout of %p is refused rather than partly parsed",
193
+ async (raw) => {
194
+ // parseInt takes a valid prefix, so "1e3" would otherwise become a 1ms
195
+ // form: open and shut before anyone could answer it.
196
+ await runAssistantCommand(
197
+ "contacts",
198
+ "create",
199
+ "--name",
200
+ "Alice",
201
+ "--timeout",
202
+ raw,
203
+ );
204
+
205
+ expect(
206
+ calls.some((c) => c.operationId === "contacts_record_prompt"),
207
+ ).toBe(false);
208
+ },
209
+ );
210
+
211
+ test("a timeout past the ceiling is refused before the guardian is bothered", async () => {
212
+ await runAssistantCommand(
213
+ "contacts",
214
+ "create",
215
+ "--name",
216
+ "Alice",
217
+ "--timeout",
218
+ "99999999",
219
+ );
220
+
221
+ expect(calls.some((c) => c.operationId === "contacts_record_prompt")).toBe(
222
+ false,
223
+ );
224
+ });
225
+
226
+ test("a write still reports success when only the display readback fails", async () => {
227
+ // The guardian has answered and the write is done. Exiting non-zero here
228
+ // says otherwise, and a retried create makes a second contact.
229
+ readFails = true;
230
+
231
+ await runAssistantCommand("contacts", "create", "--name", "Alice");
232
+
233
+ expect(calls.some((c) => c.operationId === "contacts_record_prompt")).toBe(
234
+ true,
235
+ );
236
+ expect(process.exitCode).toBeFalsy();
237
+ });
238
+
239
+ test("an update that wrote nothing exits nonzero", async () => {
240
+ // The guardian submits only what they changed, so a proposed name they
241
+ // left at its stored value never reaches the write. Whether anything
242
+ // landed is the gateway's to report, not this command's to infer.
243
+ notesSaved = false;
244
+ nothingWritten = true;
245
+
246
+ await runAssistantCommand(
247
+ "contacts",
248
+ "update",
249
+ "ct_1",
250
+ "--name",
251
+ "Alice",
252
+ "--notes",
253
+ "Moved",
254
+ );
255
+
256
+ expect(process.exitCode).toBe(1);
257
+ });
258
+
259
+ test("a rename that loses its notes still succeeds, since the rename landed", async () => {
260
+ notesSaved = false;
261
+ nothingWritten = false;
262
+
263
+ await runAssistantCommand(
264
+ "contacts",
265
+ "update",
266
+ "ct_1",
267
+ "--name",
268
+ "Alice Chen",
269
+ "--notes",
270
+ "Moved",
271
+ );
272
+
273
+ expect(process.exitCode).toBeFalsy();
274
+ });
275
+
276
+ test("create proposes only what was asked for", async () => {
277
+ await runAssistantCommand("contacts", "create", "--name", "Alice");
278
+
279
+ const body = recordPromptBody();
280
+ expect(body.operation).toBe("create");
281
+ expect(body.displayName).toBe("Alice");
282
+ expect(body.contactId).toBeUndefined();
283
+ });
284
+
285
+ test("delete reads the contact first, so a bad id fails before the guardian sees a form", async () => {
286
+ await runAssistantCommand("contacts", "delete", "ct_1");
287
+
288
+ const body = recordPromptBody();
289
+ expect(body.operation).toBe("delete");
290
+ expect(body.contactId).toBe("ct_1");
291
+ expect(body.currentDisplayName).toBe("Alice");
292
+ // Two contacts can share a name, so the confirmation carries the channels
293
+ // that identify this one and that the delete will take with it.
294
+ expect(body.channels).toEqual([
295
+ { type: "email", address: "alice@example.com" },
296
+ ]);
297
+ // The read comes first: the form names the contact it is about.
298
+ expect(calls[0]!.operationId).toBe("getContact");
299
+ });
300
+
301
+ test("delete reaches the confirmation for a contact the read cannot see", async () => {
302
+ // A dual-write gap leaves contacts in the assistant mirror that this
303
+ // gateway-backed read misses. `contacts list --query` surfaces them and
304
+ // the delete supports removing them, so refusing here would strand an id
305
+ // the guardian can see.
306
+ readFails = true;
307
+
308
+ await runAssistantCommand("contacts", "delete", "ct_orphan");
309
+
310
+ const body = recordPromptBody();
311
+ expect(body.operation).toBe("delete");
312
+ expect(body.contactId).toBe("ct_orphan");
313
+ // Nothing to show but the id, and no channels to compare against.
314
+ expect(body.channels).toEqual([]);
315
+ });
316
+
317
+ // The refusing side of that branch (an update against a contact the read
318
+ // cannot see, or a delete for an id nothing knows about) has no test: it
319
+ // ends in `exitFromIpcResult`, which calls `process.exit` and takes the test
320
+ // runner with it.
321
+
322
+ test("update refuses when neither --name nor --notes is given", async () => {
323
+ await runAssistantCommand("contacts", "update", "ct_1");
324
+
325
+ expect(calls.some((c) => c.operationId === "contacts_record_prompt")).toBe(
326
+ false,
327
+ );
328
+ });
329
+ });
@@ -67,7 +67,9 @@ mock.module("../../../util/logger.js", () => ({
67
67
  // Import module under test (after mocks)
68
68
  // ---------------------------------------------------------------------------
69
69
 
70
- const { registerBrowserCommand } = await import("../browser.js");
70
+ const { formatBrowserStatusLines, registerBrowserCommand } = await import(
71
+ "../browser.js"
72
+ );
71
73
 
72
74
  // ---------------------------------------------------------------------------
73
75
  // Test helper
@@ -533,6 +535,68 @@ describe("--json output", () => {
533
535
  });
534
536
  });
535
537
 
538
+ describe("formatBrowserStatusLines", () => {
539
+ test("prints userActions for a disconnected extension", () => {
540
+ const lines = formatBrowserStatusLines(
541
+ JSON.stringify({
542
+ requestedMode: "auto",
543
+ recommendedMode: "cdp-inspect",
544
+ stickyConversationMode: null,
545
+ modes: [
546
+ {
547
+ mode: "extension",
548
+ available: false,
549
+ autoCandidate: true,
550
+ summary:
551
+ "Extension mode is unavailable: no Chrome Extension is connected.",
552
+ userActions: [
553
+ "Install the Vellum Assistant Chrome extension from the Chrome Web Store: https://chromewebstore.google.com/detail/vellum-assistant-browser/hphbdmpffeigpcdjkckleobjmhhokpne",
554
+ "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
555
+ ],
556
+ },
557
+ ],
558
+ }),
559
+ );
560
+
561
+ expect(lines).not.toBeNull();
562
+ const rendered = lines!.join("\n");
563
+ expect(rendered).toContain("Requested mode: auto");
564
+ expect(rendered).toContain("✗ extension (auto-candidate)");
565
+ expect(rendered).toContain(
566
+ "Install the Vellum Assistant Chrome extension from the Chrome Web Store:",
567
+ );
568
+ expect(rendered).toContain(
569
+ "Tell the user to make sure a browser is open with the Vellum Chrome extension on.",
570
+ );
571
+ });
572
+
573
+ test("omits userActions when the mode is ready", () => {
574
+ const lines = formatBrowserStatusLines(
575
+ JSON.stringify({
576
+ requestedMode: "extension",
577
+ recommendedMode: "extension",
578
+ stickyConversationMode: null,
579
+ modes: [
580
+ {
581
+ mode: "extension",
582
+ available: true,
583
+ autoCandidate: true,
584
+ summary: "Extension mode is ready.",
585
+ userActions: [],
586
+ },
587
+ ],
588
+ }),
589
+ );
590
+
591
+ expect(lines).not.toBeNull();
592
+ expect(lines!.some((line) => line.trim().startsWith("- "))).toBe(false);
593
+ });
594
+
595
+ test("returns null for non-JSON status content", () => {
596
+ expect(formatBrowserStatusLines("not json")).toBeNull();
597
+ });
598
+ });
599
+
536
600
  describe("error exit codes", () => {
537
601
  test("exits with non-zero code when IPC fails", async () => {
538
602
  mockIpcResult = {
@@ -262,6 +262,7 @@ interface StatusModeEntry {
262
262
  available: boolean;
263
263
  autoCandidate: boolean;
264
264
  summary: string;
265
+ userActions?: string[];
265
266
  }
266
267
 
267
268
  interface StatusPayload {
@@ -271,31 +272,50 @@ interface StatusPayload {
271
272
  modes: StatusModeEntry[];
272
273
  }
273
274
 
274
- function formatBrowserStatus(content: string): void {
275
+ /** Render `assistant browser status` JSON as human-readable lines. */
276
+ export function formatBrowserStatusLines(content: string): string[] | null {
275
277
  let data: StatusPayload;
276
278
  try {
277
279
  data = JSON.parse(content);
278
280
  } catch {
279
- log.info(content);
280
- return;
281
+ return null;
281
282
  }
282
283
 
283
- log.info(`Requested mode: ${data.requestedMode}`);
284
+ const lines: string[] = [];
285
+ lines.push(`Requested mode: ${data.requestedMode}`);
284
286
  if (data.recommendedMode) {
285
- log.info(`Recommended: ${data.recommendedMode}`);
287
+ lines.push(`Recommended: ${data.recommendedMode}`);
286
288
  }
287
289
  if (data.stickyConversationMode) {
288
- log.info(`Sticky mode: ${data.stickyConversationMode}`);
290
+ lines.push(`Sticky mode: ${data.stickyConversationMode}`);
289
291
  }
290
- log.info("");
292
+ lines.push("");
291
293
 
292
294
  const modes = data.modes ?? [];
293
295
  for (const mode of modes) {
294
296
  const icon = mode.available ? "✓" : "✗";
295
297
  const auto = mode.autoCandidate ? " (auto-candidate)" : "";
296
- log.info(` ${icon} ${mode.mode}${auto}`);
297
- log.info(` ${mode.summary}`);
298
- log.info("");
298
+ lines.push(` ${icon} ${mode.mode}${auto}`);
299
+ lines.push(` ${mode.summary}`);
300
+ for (const action of mode.userActions ?? []) {
301
+ if (!action) {
302
+ continue;
303
+ }
304
+ lines.push(` - ${action}`);
305
+ }
306
+ lines.push("");
307
+ }
308
+ return lines;
309
+ }
310
+
311
+ function formatBrowserStatus(content: string): void {
312
+ const lines = formatBrowserStatusLines(content);
313
+ if (lines == null) {
314
+ log.info(content);
315
+ return;
316
+ }
317
+ for (const line of lines) {
318
+ log.info(line);
299
319
  }
300
320
  }
301
321