@vellumai/assistant 0.8.10 → 0.8.11-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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -14,7 +14,7 @@
14
14
  * 2. Walks {@link getRegisteredPlugins} in registration order.
15
15
  * 3. For each plugin, consults `manifest.requiresFlag` against
16
16
  * {@link isAssistantFeatureFlagEnabled}. If any listed flag is disabled,
17
- * the plugin is skipped wholesale — no `init()`, no tool/route/skill
17
+ * the plugin is skipped wholesale — no `init()`, no tool/route
18
18
  * contributions, no entry in the shutdown hook, and the plugin is also
19
19
  * dropped from the registry via {@link unregisterPlugin} so none of its
20
20
  * hooks participate in the turn lifecycle. This is the primary mechanism for
@@ -29,22 +29,21 @@
29
29
  * through untouched).
30
30
  * 6. Creates `<workspaceDir>/plugins-data/<plugin>/` on demand for per-plugin
31
31
  * writable state and exposes it via {@link PluginInitContext.pluginStorageDir}.
32
- * 7. For each surviving plugin, registers its contributed tools, routes,
33
- * and skills into their global registries via
34
- * {@link registerPluginTools}, {@link registerSkillRoute}, and
35
- * {@link registerPluginSkills}. Contributions land BEFORE `init()` so
32
+ * 7. For each surviving plugin, registers its contributed tools and routes
33
+ * into their global registries via {@link registerPluginTools} and
34
+ * {@link registerSkillRoute}. Contributions land BEFORE `init()` so
36
35
  * the plugin's hook can observe a registry where its own model-visible
37
36
  * surface is already wired — useful for plugins that want to attach
38
37
  * metadata, warm caches, or otherwise interact with their own
39
38
  * contributions during initialization.
40
- * 8. Awaits `plugin.init(ctx)` sequentially. An init failure surfaces as a
41
- * {@link PluginExecutionError} naming the offending plugin and aborts
42
- * bootstrap — later plugins' `init()` never runs and the daemon fails
43
- * startup cleanly rather than coming up in a half-wired state. The
44
- * failing plugin's already-registered tools, routes, and skills are
45
- * rolled back before the error propagates so the registry never
46
- * carries state contributed by a plugin that never finished
47
- * initializing.
39
+ * 8. Awaits `plugin.init(ctx)` sequentially. An init failure is contained to
40
+ * the offending plugin: its already-registered tools and routes are rolled
41
+ * back, it is dropped from the registry, the failure is logged, and
42
+ * bootstrap continues with the remaining plugins. A single plugin's failure
43
+ * never deregisters plugins that already initialized — in particular the
44
+ * first-party defaults, which carry core turn behavior (memory retrieval,
45
+ * history repair, title generation) — so the daemon comes up with the
46
+ * failing plugin absent rather than blacking out the whole plugin layer.
48
47
  *
49
48
  * A single shutdown hook is registered via
50
49
  * {@link registerShutdownHook} that walks the plugin list in **reverse
@@ -65,12 +64,7 @@ import { getConfig } from "../config/loader.js";
65
64
  import type { AssistantConfig } from "../config/schema.js";
66
65
  import { HOOKS } from "../plugin-api/constants.js";
67
66
  import { registerDefaultPlugins } from "../plugins/defaults/index.js";
68
- import { installPluginRuntime } from "../plugins/external-api.js";
69
67
  import { buildExternalPlugin } from "../plugins/external-plugin-loader.js";
70
- import {
71
- registerPluginSkills,
72
- unregisterPluginSkills,
73
- } from "../plugins/plugin-skill-contributions.js";
74
68
  import {
75
69
  getRegisteredPlugin,
76
70
  getRegisteredPlugins,
@@ -81,7 +75,6 @@ import {
81
75
  type Plugin,
82
76
  PluginExecutionError,
83
77
  type PluginShutdownContext,
84
- type PluginSkillRegistration,
85
78
  } from "../plugins/types.js";
86
79
  import { loadUserPlugins } from "../plugins/user-loader.js";
87
80
  import {
@@ -187,21 +180,17 @@ function getDisabledPluginFlag(
187
180
  * Bring the plugin layer up during daemon startup. Runs the full sequence in
188
181
  * the one order the rest of the system depends on:
189
182
  *
190
- * 1. Install the `globalThis.__vellumPluginRuntime` bridge so plugins can touch
191
- * it from their module body (see `plugins/external-api.ts` — compiled-binary
192
- * module identity).
193
- * 2. Register the first-party defaults so their middleware composes innermost,
183
+ * 1. Register the first-party defaults so their middleware composes innermost,
194
184
  * ahead of any user plugins.
195
- * 3. Load user plugins from `<workspaceDir>/plugins/*`. A failing user plugin is
185
+ * 2. Load user plugins from `<workspaceDir>/plugins/*`. A failing user plugin is
196
186
  * logged and skipped; `loadUserPlugins()` closes the registration window
197
187
  * when it returns, so the defaults must already be registered by then.
198
- * 4. Run every registered plugin's `init()` via {@link bootstrapPlugins}.
188
+ * 3. Run every registered plugin's `init()` via {@link bootstrapPlugins}.
199
189
  *
200
190
  * Plugin bootstrap is wrapped so a failing plugin cannot block daemon startup —
201
191
  * the daemon comes up with degraded plugin functionality instead.
202
192
  */
203
193
  export async function initializePlugins(): Promise<void> {
204
- installPluginRuntime();
205
194
  registerDefaultPlugins();
206
195
  await loadUserPlugins();
207
196
  try {
@@ -218,9 +207,10 @@ export async function initializePlugins(): Promise<void> {
218
207
  * Run every registered plugin's `init()` hook sequentially and install a
219
208
  * reverse-order shutdown hook. See the module docstring for full semantics.
220
209
  *
221
- * Returns once every plugin has finished initialising successfully. Throws a
222
- * {@link PluginExecutionError} on the first failure — the error message names
223
- * the offending plugin so operators see which `init()` bailed.
210
+ * Returns once every plugin has been processed. A plugin whose `init()` throws
211
+ * is rolled back, dropped from the registry, and logged; bootstrap continues
212
+ * with the rest, so one failing plugin never blocks the others — or the
213
+ * first-party defaults — from coming up.
224
214
  *
225
215
  * Must be called after any custom/third-party plugin registrations have run
226
216
  * and before the first conversation is served. First-party defaults are
@@ -259,35 +249,14 @@ export async function bootstrapPlugins(): Promise<void> {
259
249
  // onShutdown" invariant.
260
250
  const activePlugins: ActivePlugin[] = [];
261
251
 
262
- // Tear down a plugin's contributions AND remove it from the registry. The
263
- // two steps always move together on the bootstrap failure path: the former
264
- // clears tools/routes/skills (so they stop appearing to the model/HTTP
265
- // server), the latter drops the plugin's entry from the Map (so the registry
266
- // doesn't expose an uninitialized plugin's hooks).
267
252
  // Shutdown context is identical for every plugin in this boot — construct
268
- // once and reuse across the bootstrap-failure rollback and the normal
269
- // shutdown hook below. Only `assistantVersion` is exposed today; future
270
- // additions live on {@link PluginShutdownContext}.
253
+ // once and reuse across the per-plugin teardown and the normal shutdown
254
+ // hook below. Only `assistantVersion` is exposed today; future additions
255
+ // live on {@link PluginShutdownContext}.
271
256
  const shutdownContext: PluginShutdownContext = {
272
257
  assistantVersion: APP_VERSION,
273
258
  };
274
259
 
275
- async function rollbackPlugin(active: ActivePlugin): Promise<void> {
276
- await teardownPlugin(active, "bootstrap-failed", shutdownContext);
277
- unregisterPlugin(active.plugin.manifest.name);
278
- }
279
-
280
- // If one plugin's init or contribution phase throws, tear down any plugins
281
- // that already fully initialized (in reverse registration order) before
282
- // re-throwing. Without this, a mid-loop failure would leave earlier plugins
283
- // with live tools/routes/skills and no `onShutdown()` call — the shutdown
284
- // hook is only registered once the loop completes successfully.
285
- async function teardownPartialInit(): Promise<void> {
286
- for (let i = activePlugins.length - 1; i >= 0; i--) {
287
- await rollbackPlugin(activePlugins[i]!);
288
- }
289
- }
290
-
291
260
  for (const plugin of plugins) {
292
261
  const name = plugin.manifest.name;
293
262
  const disabledFlag = getDisabledPluginFlag(plugin, assistantConfig);
@@ -303,9 +272,18 @@ export async function bootstrapPlugins(): Promise<void> {
303
272
  try {
304
273
  activePlugins.push(await initializePlugin(plugin, assistantConfig));
305
274
  } catch (err) {
275
+ // Contain the failure to this plugin. `initializePlugin` already rolled
276
+ // back its own partial tool/route contributions, so we just drop
277
+ // it from the registry and move on. A single plugin's init failure must
278
+ // never deregister the plugins that already came up — above all the
279
+ // first-party defaults, which carry core turn behavior. The daemon stays
280
+ // reachable with the failing plugin absent rather than losing the whole
281
+ // plugin layer.
306
282
  unregisterPlugin(name);
307
- await teardownPartialInit();
308
- throw err;
283
+ log.warn(
284
+ { err, plugin: name },
285
+ `plugin ${name} failed to initialize — skipping (continuing with remaining plugins)`,
286
+ );
309
287
  }
310
288
  }
311
289
 
@@ -323,12 +301,6 @@ export async function bootstrapPlugins(): Promise<void> {
323
301
  // 3. Call `onShutdown()` (if defined) so the plugin can release resources
324
302
  // (background tasks, timers, connections) with its tools and routes
325
303
  // already removed.
326
- // 4. Unregister contributed skills via the ref-counted helper. Skills tear
327
- // down last so `onShutdown()` can still invoke skill-resolving code
328
- // (e.g. to flush pending skill work) before the catalog is emptied.
329
- // This mirrors the symmetry of registerPluginSkills() — every
330
- // successful registration must get a matching unregister call,
331
- // regardless of whether onShutdown throws.
332
304
  const shutdownSnapshot: ActivePlugin[] = [...activePlugins];
333
305
  registerShutdownHook("plugins", async (reason) => {
334
306
  for (let i = shutdownSnapshot.length - 1; i >= 0; i--) {
@@ -404,23 +376,6 @@ async function initializePlugin(
404
376
  );
405
377
  }
406
378
 
407
- if (plugin.skills && plugin.skills.length > 0) {
408
- try {
409
- registerPluginSkills(
410
- name,
411
- plugin.skills as readonly PluginSkillRegistration[],
412
- );
413
- } catch (err) {
414
- throw new PluginExecutionError(
415
- `plugin ${name} skill registration failed: ${
416
- err instanceof Error ? err.message : String(err)
417
- }`,
418
- name,
419
- { cause: err },
420
- );
421
- }
422
- }
423
-
424
379
  if (plugin.hooks?.[HOOKS.INIT]) {
425
380
  try {
426
381
  await plugin.hooks[HOOKS.INIT](initContext);
@@ -448,9 +403,6 @@ async function initializePlugin(
448
403
  unregisterSkillRoute(handle);
449
404
  }
450
405
  unregisterPluginTools(name);
451
- if (plugin.skills && plugin.skills.length > 0) {
452
- unregisterPluginSkills(name);
453
- }
454
406
  }
455
407
  throw err;
456
408
  }
@@ -458,9 +410,9 @@ async function initializePlugin(
458
410
 
459
411
  /**
460
412
  * Tear down a single fully-initialized plugin: unregister routes, unregister
461
- * tools, invoke `onShutdown()` if present, then unregister skills. Every step
462
- * swallows errors and logs with plugin attribution so one bad plugin can't
463
- * block teardown of the rest.
413
+ * tools, then invoke `onShutdown()` if present. Every step swallows errors and
414
+ * logs with plugin attribution so one bad plugin can't block teardown of the
415
+ * rest.
464
416
  *
465
417
  * Shared between the normal shutdown hook and the bootstrap error path; both
466
418
  * consume plugins that already cleared every contribution step.
@@ -514,17 +466,6 @@ async function teardownPlugin(
514
466
  );
515
467
  }
516
468
  }
517
-
518
- if (plugin.skills && plugin.skills.length > 0) {
519
- try {
520
- unregisterPluginSkills(name);
521
- } catch (err) {
522
- log.warn(
523
- { err, plugin: name, reason },
524
- "plugin skill unregistration failed (continuing with remaining plugins)",
525
- );
526
- }
527
- }
528
469
  }
529
470
 
530
471
  /** Rebuild a changed external plugin and swap it into the live registry. */
@@ -31,6 +31,7 @@ import {
31
31
  } from "../../runtime/channel-verification-service.js";
32
32
  import {
33
33
  cancelOutbound,
34
+ deliverVerificationEmail,
34
35
  deliverVerificationSlack,
35
36
  deliverVerificationTelegram,
36
37
  DESTINATION_RATE_WINDOW_MS,
@@ -555,7 +556,11 @@ export async function handleChannelVerificationSession(
555
556
  const { userId, text, assistantId: aid } = result._pendingSlackDm;
556
557
  deliverVerificationSlack(userId, text, aid);
557
558
  }
558
- const { _pendingSlackDm: _, ...publicResult } = result;
559
+ if (result._pendingEmail) {
560
+ const { to, text, subject, assistantId: aid } = result._pendingEmail;
561
+ deliverVerificationEmail(to, text, subject, aid);
562
+ }
563
+ const { _pendingSlackDm: _, _pendingEmail: __, ...publicResult } = result;
559
564
  broadcastMessage({
560
565
  type: "channel_verification_session_response",
561
566
  ...publicResult,
@@ -600,7 +605,11 @@ export async function handleChannelVerificationSession(
600
605
  const { userId, text, assistantId: aid } = result._pendingSlackDm;
601
606
  deliverVerificationSlack(userId, text, aid);
602
607
  }
603
- const { _pendingSlackDm: _, ...publicResult } = result;
608
+ if (result._pendingEmail) {
609
+ const { to, text, subject, assistantId: aid } = result._pendingEmail;
610
+ deliverVerificationEmail(to, text, subject, aid);
611
+ }
612
+ const { _pendingSlackDm: _, _pendingEmail: __, ...publicResult } = result;
604
613
  broadcastMessage({
605
614
  type: "channel_verification_session_response",
606
615
  ...publicResult,
@@ -428,7 +428,15 @@ export function renderHistoryContent(
428
428
  finalizeSegment();
429
429
  thinkingSegments.push(block.thinking);
430
430
  contentOrder.push(`thinking:${thinkingSegments.length - 1}`);
431
- contentBlocks.push({ type: "thinking", thinking: block.thinking });
431
+ const thinkingBlock: Extract<
432
+ ConversationContentBlock,
433
+ { type: "thinking" }
434
+ > = { type: "thinking", thinking: block.thinking };
435
+ if (typeof block._startedAt === "number")
436
+ thinkingBlock.startedAt = block._startedAt;
437
+ if (typeof block._completedAt === "number")
438
+ thinkingBlock.completedAt = block._completedAt;
439
+ contentBlocks.push(thinkingBlock);
432
440
  continue;
433
441
  }
434
442
 
@@ -314,9 +314,10 @@ function deriveKind(
314
314
  ): SlimSkillResponse["kind"] {
315
315
  if (source === "bundled") return "bundled";
316
316
  if (source === "catalog") return "catalog";
317
- // Plugin-contributed skills are framework-provided like bundled skills —
317
+ // Plugin-resident skills are framework-provided like bundled skills —
318
318
  // expose them under the same "bundled" kind so the UI doesn't invent a
319
- // new category that existing clients don't know how to render.
319
+ // new category that existing clients don't know how to render. Attribution
320
+ // to the owning plugin rides on the separate `owner` descriptor.
320
321
  if (source === "plugin") return "bundled";
321
322
  return "installed"; // managed, workspace, extra
322
323
  }
@@ -349,7 +350,8 @@ function toSlimSkillResponse(
349
350
  const origin = deriveOrigin(kind, summary.directoryPath, installMeta);
350
351
  const status: SlimSkillResponse["status"] = state;
351
352
 
352
- const category = getCatalogCategoryMap().get(summary.id) ?? "system";
353
+ const category =
354
+ getCatalogCategoryMap().get(summary.id) ?? summary.category ?? "system";
353
355
  const base = {
354
356
  id: summary.id,
355
357
  name: summary.displayName,
@@ -359,6 +361,7 @@ function toSlimSkillResponse(
359
361
  kind,
360
362
  status,
361
363
  category,
364
+ owner: summary.owner,
362
365
  } as const;
363
366
 
364
367
  switch (origin) {
@@ -701,7 +704,9 @@ export async function getSkill(
701
704
  return { skill: detail };
702
705
  }
703
706
 
704
- // vellum or custom origin — base fields only
707
+ // vellum or custom origin — base fields plus owner attribution. Plugin-
708
+ // resident skills are mapped to the vellum origin, so `owner` is how their
709
+ // `{ kind: "plugin", id }` provenance is surfaced on the detail response.
705
710
  const detail: SkillDetailResponse = {
706
711
  id: slim.id,
707
712
  name: slim.name,
@@ -712,6 +717,7 @@ export async function getSkill(
712
717
  origin: slim.origin,
713
718
  status: slim.status,
714
719
  category: slim.category,
720
+ owner: slim.owner,
715
721
  };
716
722
  return { skill: detail };
717
723
  }
@@ -12,11 +12,12 @@
12
12
  * translation on top.
13
13
  *
14
14
  * **Session lock.** Only one conversation may hold an active app-control
15
- * session at a time, and that session is bound to a specific target app.
15
+ * session at a time, and that session is bound to a specific target app
16
+ * on a specific client.
16
17
  * The lock is module-level (`activeAppControlSession`) because the session
17
18
  * targets the user's actual desktop application, which is a host-wide
18
19
  * resource. It is acquired optimistically when `app_control_start` is
19
- * dispatched (storing `(conversationId, app)`) so that the synchronous
20
+ * dispatched (storing `(conversationId, app, targetClientId)`) so that the synchronous
20
21
  * guard and the asynchronous host round-trip cannot race. A separate
21
22
  * `confirmedAppControlSession` tracks the last session the host reported
22
23
  * `running`; the rollback path on a failed `start` restores from the
@@ -32,7 +33,7 @@
32
33
  * user's medium-risk approval at start time is the consent boundary. All
33
34
  * other tools (observe / press / combo / sequence / type / click / drag)
34
35
  * require the calling conversation to own an active session targeting the
35
- * same `app`; otherwise the call is rejected before any host dispatch.
36
+ * same `app` and client; otherwise the call is rejected before any host dispatch.
36
37
  * This prevents prompt-injected tool calls from sending raw input to
37
38
  * arbitrary apps without the user having approved control of that
38
39
  * specific app.
@@ -78,9 +79,9 @@ const STUCK_REPEAT_THRESHOLD = 4;
78
79
  // ---------------------------------------------------------------------------
79
80
 
80
81
  /**
81
- * Active app-control session: the conversation that owns the lock and the
82
- * `app` it was approved against. Set on a successful `app_control_start`;
83
- * cleared by the owning proxy's `dispose()`.
82
+ * Active app-control session: the conversation that owns the lock, the
83
+ * `app` and client it was approved against. Set on a successful
84
+ * `app_control_start`; cleared by the owning proxy's `dispose()`.
84
85
  */
85
86
  export interface ActiveAppControlSession {
86
87
  conversationId: string;
@@ -90,6 +91,11 @@ export interface ActiveAppControlSession {
90
91
  * the `app` of subsequent non-start tool calls.
91
92
  */
92
93
  app: string;
94
+ /**
95
+ * The client selected for `app_control_start`. Undefined when the start
96
+ * was untargeted; subsequent calls must match the same value.
97
+ */
98
+ targetClientId?: string;
93
99
  /**
94
100
  * Strictly monotonic counter assigned when the session is created (in
95
101
  * `request()` for a `start`). Used by {@link promoteStartIfCurrent} to
@@ -153,11 +159,13 @@ export function _resetActiveAppControlSession(): void {
153
159
  export function _setActiveAppControlSession(session: {
154
160
  conversationId: string;
155
161
  app: string;
162
+ targetClientId?: string;
156
163
  dispatchedAt?: number;
157
164
  }): void {
158
165
  const full: ActiveAppControlSession = {
159
166
  conversationId: session.conversationId,
160
167
  app: session.app,
168
+ targetClientId: session.targetClientId,
161
169
  dispatchedAt: session.dispatchedAt ?? nextDispatchedAt++,
162
170
  };
163
171
  activeAppControlSession = full;
@@ -177,6 +185,7 @@ export function _setActiveAppControlSession(session: {
177
185
  function checkNonStartAuthorization(
178
186
  input: HostAppControlInput,
179
187
  conversationId: string,
188
+ targetClientId: string | undefined,
180
189
  ): ToolExecutionResult | null {
181
190
  if (activeAppControlSession == null) {
182
191
  return {
@@ -219,6 +228,16 @@ function checkNonStartAuthorization(
219
228
  isError: true,
220
229
  };
221
230
  }
231
+ if (activeAppControlSession.targetClientId !== targetClientId) {
232
+ return {
233
+ content:
234
+ `Active app-control session targets client ` +
235
+ `${activeAppControlSession.targetClientId ?? "<default>"}; cannot ` +
236
+ `send actions to client ${targetClientId ?? "<default>"}. Call ` +
237
+ `app_control_stop and app_control_start to switch clients.`,
238
+ isError: true,
239
+ };
240
+ }
222
241
  return null;
223
242
  }
224
243
 
@@ -284,11 +303,53 @@ export class HostAppControlProxy extends HostProxyBase<
284
303
  return { content: "Aborted", isError: true };
285
304
  }
286
305
 
306
+ // Resolve target client and enforce same-actor BEFORE acquiring the
307
+ // session lock. This way early-exit errors don't need rollback, and
308
+ // the resolved client id can be stored in the session.
309
+ let resolvedTargetClientId = targetClientId;
310
+ if (resolvedTargetClientId == null) {
311
+ const resolved = pickSameUserAutoResolve({
312
+ hub: assistantEventHub,
313
+ capability: "host_app_control",
314
+ sourceActorPrincipalId,
315
+ });
316
+ if (resolved.kind === "ambiguous") {
317
+ return {
318
+ content:
319
+ "Multiple host_app_control clients are connected for this user. Specify target_client_id to disambiguate.",
320
+ isError: true,
321
+ };
322
+ }
323
+ if (resolved.kind === "match") {
324
+ resolvedTargetClientId = resolved.clientId;
325
+ } else if (
326
+ assistantEventHub.listClientsByCapability("host_app_control").length > 0
327
+ ) {
328
+ return {
329
+ content:
330
+ "App control is not available for the current actor. Connect a host_app_control-capable client as the same user.",
331
+ isError: true,
332
+ };
333
+ }
334
+ }
335
+
336
+ if (resolvedTargetClientId != null) {
337
+ const rejection = enforceSameActorOrErrorResult({
338
+ hub: assistantEventHub,
339
+ sourceActorPrincipalId,
340
+ targetClientId: resolvedTargetClientId,
341
+ op: "host_app_control",
342
+ });
343
+ if (rejection) {
344
+ return rejection;
345
+ }
346
+ }
347
+
287
348
  // Authorization gate. `start` acquires the session lock (the user's
288
349
  // medium-risk approval is the consent boundary); all other tools must
289
- // belong to the active session and target the same `app`. Without this
290
- // gate, prompt-injected calls would bypass the start-time approval and
291
- // send raw input to arbitrary apps.
350
+ // belong to the active session and target the same `app` and client.
351
+ // Without this gate, prompt-injected calls would bypass the start-time
352
+ // approval and send raw input to arbitrary apps or clients.
292
353
  let attemptedSession: ActiveAppControlSession | undefined;
293
354
  if (input.tool === "start") {
294
355
  if (
@@ -314,6 +375,7 @@ export class HostAppControlProxy extends HostProxyBase<
314
375
  attemptedSession = {
315
376
  conversationId: this.conversationId,
316
377
  app: input.app,
378
+ targetClientId: resolvedTargetClientId,
317
379
  dispatchedAt: nextDispatchedAt++,
318
380
  };
319
381
  activeAppControlSession = attemptedSession;
@@ -321,60 +383,13 @@ export class HostAppControlProxy extends HostProxyBase<
321
383
  const sessionError = checkNonStartAuthorization(
322
384
  input,
323
385
  this.conversationId,
386
+ resolvedTargetClientId,
324
387
  );
325
388
  if (sessionError != null) {
326
389
  return sessionError;
327
390
  }
328
391
  }
329
392
 
330
- let resolvedTargetClientId = targetClientId;
331
- if (resolvedTargetClientId == null) {
332
- const resolved = pickSameUserAutoResolve({
333
- hub: assistantEventHub,
334
- capability: "host_app_control",
335
- sourceActorPrincipalId,
336
- });
337
- if (resolved.kind === "ambiguous") {
338
- if (input.tool === "start") {
339
- this.rollbackStartIfCurrent(attemptedSession);
340
- }
341
- return {
342
- content:
343
- "Multiple host_app_control clients are connected for this user. Specify target_client_id to disambiguate.",
344
- isError: true,
345
- };
346
- }
347
- if (resolved.kind === "match") {
348
- resolvedTargetClientId = resolved.clientId;
349
- } else if (
350
- assistantEventHub.listClientsByCapability("host_app_control").length > 0
351
- ) {
352
- if (input.tool === "start") {
353
- this.rollbackStartIfCurrent(attemptedSession);
354
- }
355
- return {
356
- content:
357
- "App control is not available for the current actor. Connect a host_app_control-capable client as the same user.",
358
- isError: true,
359
- };
360
- }
361
- }
362
-
363
- if (resolvedTargetClientId != null) {
364
- const rejection = enforceSameActorOrErrorResult({
365
- hub: assistantEventHub,
366
- sourceActorPrincipalId,
367
- targetClientId: resolvedTargetClientId,
368
- op: "host_app_control",
369
- });
370
- if (rejection) {
371
- if (input.tool === "start") {
372
- this.rollbackStartIfCurrent(attemptedSession);
373
- }
374
- return rejection;
375
- }
376
- }
377
-
378
393
  try {
379
394
  const payload = await this.dispatchRequest(
380
395
  toolName,