@vellumai/assistant 0.7.1 → 0.7.2

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 (535) hide show
  1. package/ARCHITECTURE.md +32 -49
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/security.md +20 -0
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +1 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -0
  19. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  20. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  21. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  22. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  23. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  24. package/openapi.yaml +565 -12
  25. package/package.json +6 -3
  26. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  27. package/src/__tests__/app-bundler.test.ts +170 -1
  28. package/src/__tests__/app-control-flow.test.ts +374 -0
  29. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  30. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  31. package/src/__tests__/app-executors.test.ts +30 -43
  32. package/src/__tests__/approval-routes-http.test.ts +23 -6
  33. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  34. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  35. package/src/__tests__/assistant-event-hub.test.ts +109 -2
  36. package/src/__tests__/assistant-event.test.ts +10 -0
  37. package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
  39. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  40. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  41. package/src/__tests__/btw-routes.test.ts +13 -4
  42. package/src/__tests__/call-controller.test.ts +49 -1
  43. package/src/__tests__/call-domain.test.ts +0 -2
  44. package/src/__tests__/call-routes-http.test.ts +0 -2
  45. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  46. package/src/__tests__/checker.test.ts +3 -4
  47. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  48. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  49. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  50. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  51. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  52. package/src/__tests__/config-watcher.test.ts +2 -2
  53. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  54. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  55. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  56. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  57. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  58. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  59. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  60. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  61. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  62. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  63. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  64. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  65. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  66. package/src/__tests__/credentials-cli.test.ts +5 -12
  67. package/src/__tests__/cu-unified-flow.test.ts +185 -23
  68. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  69. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  70. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  71. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  72. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  73. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  74. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  75. package/src/__tests__/heartbeat-service.test.ts +718 -1
  76. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  77. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  78. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  79. package/src/__tests__/host-bash-proxy.test.ts +246 -47
  80. package/src/__tests__/host-bash-routes.test.ts +294 -0
  81. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  82. package/src/__tests__/host-browser-routes.test.ts +39 -13
  83. package/src/__tests__/host-cu-proxy.test.ts +41 -52
  84. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  85. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  86. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  87. package/src/__tests__/host-file-proxy.test.ts +37 -43
  88. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  89. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  90. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  91. package/src/__tests__/host-proxy-base.test.ts +312 -0
  92. package/src/__tests__/host-shell-tool.test.ts +22 -4
  93. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  94. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  95. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  96. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  97. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  98. package/src/__tests__/identity-routes.test.ts +103 -1
  99. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  100. package/src/__tests__/inline-command-runner.test.ts +0 -1
  101. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  102. package/src/__tests__/integration-status.test.ts +85 -5
  103. package/src/__tests__/intent-routing.test.ts +0 -1
  104. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  105. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  106. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  107. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  108. package/src/__tests__/mcp-cli.test.ts +338 -2
  109. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  110. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  111. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  112. package/src/__tests__/oauth-cli.test.ts +0 -2
  113. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  114. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  115. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  116. package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
  117. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  118. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  119. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  120. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  121. package/src/__tests__/retry-backoff.test.ts +87 -0
  122. package/src/__tests__/runtime-events-sse.test.ts +10 -6
  123. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  124. package/src/__tests__/schedule-retry.test.ts +715 -0
  125. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  126. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  127. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  128. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  129. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  130. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  131. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  132. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  133. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  134. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  135. package/src/__tests__/slack-channel-config.test.ts +9 -14
  136. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  137. package/src/__tests__/system-prompt.test.ts +0 -1
  138. package/src/__tests__/telegram-config.test.ts +0 -1
  139. package/src/__tests__/test-preload.ts +8 -0
  140. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  141. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  142. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  143. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  144. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  145. package/src/__tests__/tool-executor.test.ts +0 -1
  146. package/src/__tests__/twilio-config.test.ts +3 -16
  147. package/src/__tests__/twilio-routes.test.ts +3 -5
  148. package/src/__tests__/twilio-validation.test.ts +93 -0
  149. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  150. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  151. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  152. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  153. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  154. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  155. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  156. package/src/backup/__tests__/paths.test.ts +0 -22
  157. package/src/backup/__tests__/restore.test.ts +51 -151
  158. package/src/backup/paths.ts +2 -18
  159. package/src/backup/restore.ts +107 -231
  160. package/src/bundler/app-bundler.ts +51 -3
  161. package/src/calls/relay-server.ts +4 -44
  162. package/src/calls/twilio-config.ts +2 -17
  163. package/src/calls/twilio-rest.ts +33 -105
  164. package/src/calls/twilio-routes.ts +11 -12
  165. package/src/channels/types.ts +8 -7
  166. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  167. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  168. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  169. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  170. package/src/cli/commands/backup.ts +6 -331
  171. package/src/cli/commands/clients.ts +36 -37
  172. package/src/cli/commands/contacts.ts +73 -0
  173. package/src/cli/commands/conversations.ts +2 -5
  174. package/src/cli/commands/credentials.ts +15 -7
  175. package/src/cli/commands/domain.ts +66 -15
  176. package/src/cli/commands/gateway.ts +183 -0
  177. package/src/cli/commands/keys.ts +9 -6
  178. package/src/cli/commands/mcp.ts +116 -156
  179. package/src/cli/commands/memory-v2.ts +296 -1
  180. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  181. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  182. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  183. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  184. package/src/cli/commands/platform/disconnect.ts +5 -4
  185. package/src/cli/commands/platform/index.ts +0 -18
  186. package/src/cli/lib/daemon-credential-client.ts +110 -28
  187. package/src/cli/program.ts +2 -0
  188. package/src/config/assistant-feature-flags.ts +67 -10
  189. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  190. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  191. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  192. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  193. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  194. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  195. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  196. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  197. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  198. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  199. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  200. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  201. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  202. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  203. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  204. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  205. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  206. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  207. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  208. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  209. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  210. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  211. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  212. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  213. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  214. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  215. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  216. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  217. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  218. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  219. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  220. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  221. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  222. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  223. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  224. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  225. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  226. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  227. package/src/config/bundled-tool-registry.ts +21 -0
  228. package/src/config/env-registry.ts +0 -2
  229. package/src/config/env.ts +19 -12
  230. package/src/config/feature-flag-registry.json +21 -133
  231. package/src/config/loader.ts +73 -99
  232. package/src/config/sanitize-for-transfer.ts +2 -0
  233. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  234. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -4
  235. package/src/config/schemas/calls.ts +0 -9
  236. package/src/config/schemas/heartbeat.ts +63 -0
  237. package/src/config/schemas/ingress.ts +10 -6
  238. package/src/config/schemas/llm.ts +5 -10
  239. package/src/config/schemas/memory-lifecycle.ts +77 -24
  240. package/src/config/schemas/memory-v2.ts +48 -4
  241. package/src/config/schemas/platform.ts +6 -0
  242. package/src/config/schemas/services.ts +1 -15
  243. package/src/config/schemas/skills.ts +0 -6
  244. package/src/config/seed-inference-profiles.ts +1 -1
  245. package/src/contacts/contact-store.ts +0 -30
  246. package/src/contacts/contacts-write.ts +0 -27
  247. package/src/context/window-manager.ts +1 -2
  248. package/src/credential-execution/feature-gates.ts +10 -10
  249. package/src/credential-execution/process-manager.ts +12 -41
  250. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  251. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  252. package/src/daemon/config-watcher.ts +4 -3
  253. package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
  254. package/src/daemon/conversation-agent-loop.ts +32 -28
  255. package/src/daemon/conversation-lifecycle.ts +8 -1
  256. package/src/daemon/conversation-process.ts +16 -11
  257. package/src/daemon/conversation-runtime-assembly.ts +2 -2
  258. package/src/daemon/conversation-surfaces.ts +125 -4
  259. package/src/daemon/conversation-tool-setup.ts +16 -55
  260. package/src/daemon/conversation.ts +21 -2
  261. package/src/daemon/doordash-steps.ts +1 -1
  262. package/src/daemon/handlers/shared.ts +4 -1
  263. package/src/daemon/host-app-control-proxy.ts +293 -0
  264. package/src/daemon/host-bash-proxy.ts +84 -74
  265. package/src/daemon/host-browser-proxy.ts +67 -82
  266. package/src/daemon/host-cu-proxy.ts +81 -86
  267. package/src/daemon/host-file-proxy.ts +93 -69
  268. package/src/daemon/host-proxy-base.ts +294 -0
  269. package/src/daemon/host-proxy-preactivation.ts +82 -0
  270. package/src/daemon/host-transfer-proxy.ts +247 -129
  271. package/src/daemon/lifecycle.ts +115 -117
  272. package/src/daemon/message-protocol.ts +3 -8
  273. package/src/daemon/message-types/contacts.ts +23 -1
  274. package/src/daemon/message-types/conversations.ts +11 -8
  275. package/src/daemon/message-types/host-app-control.ts +150 -0
  276. package/src/daemon/message-types/host-bash.ts +4 -0
  277. package/src/daemon/message-types/host-cu.ts +2 -0
  278. package/src/daemon/message-types/host-file.ts +4 -0
  279. package/src/daemon/message-types/host-transfer.ts +3 -0
  280. package/src/daemon/message-types/schedules.ts +8 -3
  281. package/src/daemon/message-types/skills.ts +2 -2
  282. package/src/daemon/process-message.ts +18 -1
  283. package/src/daemon/shutdown-handlers.ts +0 -3
  284. package/src/daemon/tool-setup-types.ts +51 -0
  285. package/src/daemon/tool-side-effects.ts +1 -1
  286. package/src/events/tool-audit-listener.ts +2 -1
  287. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
  288. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  289. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  290. package/src/heartbeat/heartbeat-service.ts +280 -49
  291. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  292. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  293. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  294. package/src/home/post-connect-feed.ts +68 -0
  295. package/src/home/relationship-state-writer.ts +17 -92
  296. package/src/home/suggested-prompts.ts +46 -10
  297. package/src/inbound/public-ingress-urls.ts +32 -34
  298. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  299. package/src/ipc/assistant-server.ts +14 -1
  300. package/src/ipc/cli-client.ts +32 -1
  301. package/src/live-voice/live-voice-metrics.ts +10 -10
  302. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  303. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  304. package/src/mcp/mcp-auth-state.ts +133 -0
  305. package/src/mcp/mcp-oauth-provider.ts +19 -0
  306. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  307. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  308. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  309. package/src/memory/anisotropy.test.ts +247 -0
  310. package/src/memory/anisotropy.ts +443 -0
  311. package/src/memory/auto-analysis-constants.ts +17 -0
  312. package/src/memory/auto-analysis-guard.ts +5 -15
  313. package/src/memory/canonical-guardian-store.ts +7 -7
  314. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  315. package/src/memory/context-search/agent-protocol.ts +6 -6
  316. package/src/memory/context-search/agent-runner.ts +32 -7
  317. package/src/memory/context-search/sources/memory-v2.ts +17 -5
  318. package/src/memory/conversation-crud.ts +1 -1
  319. package/src/memory/conversation-key-store.ts +2 -15
  320. package/src/memory/db-init.ts +4 -0
  321. package/src/memory/embedding-backend.ts +9 -21
  322. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
  323. package/src/memory/graph/conversation-graph-memory.ts +1 -24
  324. package/src/memory/graph/graph-search.ts +8 -0
  325. package/src/memory/graph/retriever.ts +28 -0
  326. package/src/memory/graph/tools.ts +1 -1
  327. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  328. package/src/memory/jobs/embed-concept-page.ts +28 -2
  329. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  330. package/src/memory/jobs-store.ts +66 -22
  331. package/src/memory/jobs-worker.ts +112 -63
  332. package/src/memory/memory-v2-activation-log-store.ts +1 -1
  333. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  334. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  335. package/src/memory/migrations/index.ts +5 -0
  336. package/src/memory/migrations/registry.ts +8 -0
  337. package/src/memory/pkb/pkb-search.ts +7 -0
  338. package/src/memory/qdrant-client.ts +50 -20
  339. package/src/memory/schema/infrastructure.ts +15 -0
  340. package/src/memory/search/semantic.ts +7 -0
  341. package/src/memory/sparse-tokenize.ts +49 -0
  342. package/src/memory/v2/__tests__/activation.test.ts +77 -95
  343. package/src/memory/v2/__tests__/injection.test.ts +43 -21
  344. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  345. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  346. package/src/memory/v2/__tests__/static-context.test.ts +0 -1
  347. package/src/memory/v2/activation.ts +69 -88
  348. package/src/memory/v2/consolidation-job.ts +3 -5
  349. package/src/memory/v2/constants.ts +7 -0
  350. package/src/memory/v2/injection.ts +86 -53
  351. package/src/memory/v2/prompts/consolidation.ts +312 -91
  352. package/src/memory/v2/qdrant.ts +99 -1
  353. package/src/memory/v2/sim.ts +126 -16
  354. package/src/memory/v2/skill-qdrant.ts +12 -3
  355. package/src/memory/v2/skill-store.ts +16 -1
  356. package/src/memory/v2/sparse-bm25.ts +245 -0
  357. package/src/memory/v2/static-context.ts +6 -5
  358. package/src/messaging/providers/gmail/types.ts +0 -49
  359. package/src/messaging/providers/slack/adapter.ts +1 -31
  360. package/src/messaging/providers/slack/types.ts +0 -32
  361. package/src/notifications/README.md +10 -10
  362. package/src/notifications/broadcaster.ts +1 -1
  363. package/src/notifications/guardian-question-mode.ts +5 -5
  364. package/src/oauth/connect-orchestrator.ts +4 -0
  365. package/src/oauth/credential-token-resolver.ts +1 -3
  366. package/src/oauth/manual-token-connection.ts +0 -4
  367. package/src/outbound-proxy/index.ts +1 -37
  368. package/src/outbound-proxy/logging.ts +1 -1
  369. package/src/outbound-proxy/policy.ts +6 -5
  370. package/src/outbound-proxy/router.ts +2 -1
  371. package/src/permissions/approval-policy.test.ts +6 -275
  372. package/src/permissions/approval-policy.ts +0 -51
  373. package/src/permissions/checker.test.ts +0 -1
  374. package/src/permissions/checker.ts +3 -17
  375. package/src/permissions/gateway-threshold-reader.ts +2 -0
  376. package/src/permissions/prompter.ts +34 -1
  377. package/src/permissions/secret-prompter.ts +6 -2
  378. package/src/prompts/bootstrap-cleanup.ts +27 -0
  379. package/src/prompts/system-prompt.ts +3 -18
  380. package/src/prompts/templates/SOUL.md +13 -1
  381. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  382. package/src/runtime/assistant-event-hub.ts +118 -96
  383. package/src/runtime/assistant-event.ts +1 -0
  384. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  385. package/src/runtime/auth/middleware.ts +0 -96
  386. package/src/runtime/auth/route-policy.ts +19 -0
  387. package/src/runtime/btw-sidechain.ts +2 -3
  388. package/src/runtime/channel-invite-transport.ts +2 -48
  389. package/src/runtime/channel-invite-transports/email.ts +1 -1
  390. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  391. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  392. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  393. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  394. package/src/runtime/channel-invite-types.ts +54 -0
  395. package/src/runtime/channel-readiness-service.ts +32 -13
  396. package/src/runtime/http-server.ts +3 -329
  397. package/src/runtime/http-types.ts +0 -5
  398. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  399. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  400. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  401. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  402. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  403. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  404. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  405. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  406. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  407. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  408. package/src/runtime/migrations/migration-transport.ts +7 -7
  409. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  410. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  411. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  412. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  413. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  414. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  415. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  416. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  417. package/src/runtime/pending-interactions.ts +35 -9
  418. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  419. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
  420. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  421. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  422. package/src/runtime/routes/approval-interception-types.ts +13 -0
  423. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  424. package/src/runtime/routes/backup-routes.ts +15 -38
  425. package/src/runtime/routes/btw-routes.ts +14 -37
  426. package/src/runtime/routes/client-routes.ts +1 -0
  427. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  428. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  429. package/src/runtime/routes/conversation-routes.ts +30 -13
  430. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  431. package/src/runtime/routes/documents-routes.ts +30 -0
  432. package/src/runtime/routes/errors.ts +19 -4
  433. package/src/runtime/routes/events-routes.ts +12 -6
  434. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  435. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  436. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  437. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  438. package/src/runtime/routes/host-bash-routes.ts +36 -6
  439. package/src/runtime/routes/host-browser-routes.ts +108 -13
  440. package/src/runtime/routes/host-cu-routes.ts +44 -14
  441. package/src/runtime/routes/host-file-routes.ts +33 -10
  442. package/src/runtime/routes/host-transfer-routes.ts +64 -24
  443. package/src/runtime/routes/http-adapter.ts +1 -0
  444. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  445. package/src/runtime/routes/identity-routes.ts +15 -43
  446. package/src/runtime/routes/inbound-message-handler.ts +1 -9
  447. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  448. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  449. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  450. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  451. package/src/runtime/routes/index.ts +8 -0
  452. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  453. package/src/runtime/routes/memory-item-routes.ts +10 -12
  454. package/src/runtime/routes/memory-v2-routes.ts +441 -1
  455. package/src/runtime/routes/migration-routes.ts +96 -0
  456. package/src/runtime/routes/schedule-routes.ts +7 -0
  457. package/src/runtime/verification-templates.ts +4 -7
  458. package/src/schedule/integration-status.ts +66 -2
  459. package/src/schedule/recurrence-engine.ts +4 -1
  460. package/src/schedule/retry-backoff.ts +18 -0
  461. package/src/schedule/retry-policy.ts +82 -0
  462. package/src/schedule/schedule-recovery.ts +64 -0
  463. package/src/schedule/schedule-store.ts +106 -2
  464. package/src/schedule/scheduler-types.ts +25 -0
  465. package/src/schedule/scheduler.ts +63 -38
  466. package/src/security/oauth-callback-registry.ts +8 -0
  467. package/src/sequence/analytics.ts +5 -5
  468. package/src/sequence/engine.ts +1 -1
  469. package/src/skills/catalog-files.ts +2 -8
  470. package/src/skills/include-graph.ts +5 -5
  471. package/src/skills/remote-skill-policy.ts +5 -5
  472. package/src/skills/skill-file-provider.ts +1 -1
  473. package/src/skills/skill-file-types.ts +13 -0
  474. package/src/skills/skillssh-audit-types.ts +28 -0
  475. package/src/skills/skillssh-registry.ts +8 -21
  476. package/src/telemetry/types.ts +2 -0
  477. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  478. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  479. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  480. package/src/tools/apps/executors.ts +56 -69
  481. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  482. package/src/tools/browser/browser-execution.ts +2 -2
  483. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  484. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  485. package/src/tools/browser/cdp-client/factory.ts +23 -24
  486. package/src/tools/browser/cdp-client/index.ts +1 -14
  487. package/src/tools/computer-use/definitions.ts +42 -20
  488. package/src/tools/executor.ts +2 -0
  489. package/src/tools/host-filesystem/edit.ts +26 -0
  490. package/src/tools/host-filesystem/read.ts +26 -0
  491. package/src/tools/host-filesystem/transfer.ts +31 -1
  492. package/src/tools/host-filesystem/write.ts +26 -0
  493. package/src/tools/host-terminal/host-shell.ts +58 -0
  494. package/src/tools/schedule/create.ts +6 -0
  495. package/src/tools/schedule/list.ts +2 -0
  496. package/src/tools/schedule/update.ts +10 -0
  497. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  498. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  499. package/src/tools/skills/load.ts +0 -32
  500. package/src/tools/tool-approval-handler.ts +1 -5
  501. package/src/tools/types.ts +4 -0
  502. package/src/usage/pricing.ts +1 -1
  503. package/src/workspace/hatched-date.ts +86 -0
  504. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  505. package/src/workspace/migrations/006-services-config.ts +8 -5
  506. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  507. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  508. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  509. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  510. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  511. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  512. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  513. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  514. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  515. package/src/workspace/migrations/AGENTS.md +1 -1
  516. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  517. package/src/workspace/migrations/utils.ts +21 -0
  518. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  519. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  520. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  521. package/src/__tests__/twilio-rest.test.ts +0 -34
  522. package/src/backup/__tests__/backup-key.test.ts +0 -152
  523. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  524. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  525. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  526. package/src/backup/backup-key.ts +0 -137
  527. package/src/backup/backup-worker.ts +0 -472
  528. package/src/backup/offsite-writer.ts +0 -222
  529. package/src/backup/stream-crypt.ts +0 -263
  530. package/src/daemon/message-types/pairing.ts +0 -58
  531. package/src/outbound-proxy/config.ts +0 -20
  532. package/src/outbound-proxy/health.ts +0 -18
  533. package/src/outbound-proxy/types.ts +0 -150
  534. package/src/runtime/capability-tokens.ts +0 -190
  535. package/src/signals/mcp-reload.ts +0 -18
@@ -63,7 +63,13 @@ export async function callHandler(
63
63
  } catch (err) {
64
64
  if (err instanceof RouteError) {
65
65
  return Response.json(
66
- { error: { code: err.code, message: err.message } },
66
+ {
67
+ error: {
68
+ code: err.code,
69
+ message: err.message,
70
+ ...(err.details !== undefined && { details: err.details }),
71
+ },
72
+ },
67
73
  { status: err.statusCode },
68
74
  );
69
75
  }
@@ -0,0 +1,602 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ const sentMessages: unknown[] = [];
4
+ const resolvedInteractionIds: string[] = [];
5
+ let mockHasClient = false;
6
+
7
+ mock.module("../runtime/assistant-event-hub.js", () => ({
8
+ broadcastMessage: (msg: unknown) => sentMessages.push(msg),
9
+ assistantEventHub: {
10
+ getMostRecentClientByCapability: (cap: string) =>
11
+ cap === "host_app_control" && mockHasClient
12
+ ? { id: "mock-client" }
13
+ : null,
14
+ },
15
+ }));
16
+
17
+ mock.module("../runtime/pending-interactions.js", () => ({
18
+ register: () => undefined,
19
+ resolve: (requestId: string) => {
20
+ resolvedInteractionIds.push(requestId);
21
+ return undefined;
22
+ },
23
+ get: () => undefined,
24
+ getByKind: () => [],
25
+ getByConversation: () => [],
26
+ removeByConversation: () => {},
27
+ }));
28
+
29
+ const {
30
+ HostAppControlProxy,
31
+ _getActiveAppControlConversationId,
32
+ _resetActiveAppControlConversationId,
33
+ } = await import("../daemon/host-app-control-proxy.js");
34
+
35
+ import type { HostAppControlResultPayload } from "../daemon/message-types/host-app-control.js";
36
+
37
+ /**
38
+ * Build a result payload with stable defaults plus per-test overrides.
39
+ * Default state is "running" so `start` succeeds and the singleton lock
40
+ * is acquired.
41
+ */
42
+ function payload(
43
+ overrides: Partial<HostAppControlResultPayload> = {},
44
+ ): HostAppControlResultPayload {
45
+ return {
46
+ requestId: "ignored-by-proxy",
47
+ state: "running",
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ /** Tiny base64-encoded PNG-ish blob — content is irrelevant to the hashing logic. */
53
+ const PNG_A = "AAAA";
54
+ const PNG_B = "BBBB";
55
+
56
+ describe("HostAppControlProxy", () => {
57
+ beforeEach(() => {
58
+ sentMessages.length = 0;
59
+ resolvedInteractionIds.length = 0;
60
+ mockHasClient = false;
61
+ _resetActiveAppControlConversationId();
62
+ });
63
+
64
+ afterEach(() => {
65
+ _resetActiveAppControlConversationId();
66
+ });
67
+
68
+ // -------------------------------------------------------------------------
69
+ // (a) Start round-trip
70
+ // -------------------------------------------------------------------------
71
+
72
+ describe("start round-trip", () => {
73
+ test("dispatches host_app_control_request and resolves with formatted result", async () => {
74
+ const proxy = new HostAppControlProxy("conv-1");
75
+ const controller = new AbortController();
76
+
77
+ const resultPromise = proxy.request(
78
+ "app_control_start",
79
+ { tool: "start", app: "com.example.editor" },
80
+ "conv-1",
81
+ controller.signal,
82
+ );
83
+
84
+ expect(sentMessages).toHaveLength(1);
85
+ const sent = sentMessages[0] as Record<string, unknown>;
86
+ expect(sent.type).toBe("host_app_control_request");
87
+ expect(sent.conversationId).toBe("conv-1");
88
+ expect(sent.toolName).toBe("app_control_start");
89
+ expect(sent.input).toEqual({
90
+ tool: "start",
91
+ app: "com.example.editor",
92
+ });
93
+ expect(typeof sent.requestId).toBe("string");
94
+
95
+ const requestId = sent.requestId as string;
96
+ expect(proxy.hasPendingRequest(requestId)).toBe(true);
97
+
98
+ proxy.resolve(
99
+ requestId,
100
+ payload({
101
+ pngBase64: PNG_A,
102
+ windowBounds: { x: 10, y: 20, width: 800, height: 600 },
103
+ executionResult: "Editor launched",
104
+ }),
105
+ );
106
+
107
+ const result = await resultPromise;
108
+ expect(result.isError).toBe(false);
109
+ expect(result.content).toContain("State: running");
110
+ expect(result.content).toContain("800x600 at (10, 20)");
111
+ expect(result.content).toContain("Editor launched");
112
+ expect(result.contentBlocks).toBeDefined();
113
+ expect(result.contentBlocks).toHaveLength(1);
114
+ expect(result.contentBlocks![0]).toEqual({
115
+ type: "image",
116
+ source: {
117
+ type: "base64",
118
+ media_type: "image/png",
119
+ data: PNG_A,
120
+ },
121
+ });
122
+
123
+ // Singleton lock acquired by this conversation.
124
+ expect(_getActiveAppControlConversationId()).toBe("conv-1");
125
+
126
+ proxy.dispose();
127
+ });
128
+
129
+ test("formats execution error with isError=true", async () => {
130
+ const proxy = new HostAppControlProxy("conv-1");
131
+ const controller = new AbortController();
132
+
133
+ const resultPromise = proxy.request(
134
+ "app_control_click",
135
+ { tool: "click", app: "com.example.editor", x: 100, y: 200 },
136
+ "conv-1",
137
+ controller.signal,
138
+ );
139
+
140
+ const sent = sentMessages[0] as Record<string, unknown>;
141
+ proxy.resolve(
142
+ sent.requestId as string,
143
+ payload({
144
+ executionError: "Element not found at (100, 200)",
145
+ }),
146
+ );
147
+
148
+ const result = await resultPromise;
149
+ expect(result.isError).toBe(true);
150
+ expect(result.content).toContain(
151
+ "Action failed: Element not found at (100, 200)",
152
+ );
153
+ expect(result.content).toContain("State: running");
154
+
155
+ proxy.dispose();
156
+ });
157
+ });
158
+
159
+ // -------------------------------------------------------------------------
160
+ // (b) Singleton lock
161
+ // -------------------------------------------------------------------------
162
+
163
+ describe("singleton lock", () => {
164
+ test("second conversation's start returns isError naming the holder", async () => {
165
+ const proxy1 = new HostAppControlProxy("conv-1");
166
+ const ctrl1 = new AbortController();
167
+
168
+ const p1 = proxy1.request(
169
+ "app_control_start",
170
+ { tool: "start", app: "com.example.editor" },
171
+ "conv-1",
172
+ ctrl1.signal,
173
+ );
174
+ const sent1 = sentMessages[0] as Record<string, unknown>;
175
+ proxy1.resolve(sent1.requestId as string, payload({ pngBase64: PNG_A }));
176
+ await p1;
177
+
178
+ expect(_getActiveAppControlConversationId()).toBe("conv-1");
179
+
180
+ // Second conversation tries to start — should be rejected without
181
+ // sending any envelope.
182
+ const proxy2 = new HostAppControlProxy("conv-2");
183
+ const ctrl2 = new AbortController();
184
+ sentMessages.length = 0;
185
+
186
+ const result = await proxy2.request(
187
+ "app_control_start",
188
+ { tool: "start", app: "com.example.editor" },
189
+ "conv-2",
190
+ ctrl2.signal,
191
+ );
192
+
193
+ expect(result.isError).toBe(true);
194
+ expect(result.content).toContain("conv-1");
195
+ expect(result.content.toLowerCase()).toContain(
196
+ "currently holds the app-control session",
197
+ );
198
+ expect(sentMessages).toHaveLength(0); // No envelope dispatched
199
+
200
+ proxy1.dispose();
201
+ proxy2.dispose();
202
+ });
203
+
204
+ test("same conversation re-starting is allowed", async () => {
205
+ const proxy = new HostAppControlProxy("conv-1");
206
+ const ctrl = new AbortController();
207
+
208
+ const p1 = proxy.request(
209
+ "app_control_start",
210
+ { tool: "start", app: "com.example.editor" },
211
+ "conv-1",
212
+ ctrl.signal,
213
+ );
214
+ proxy.resolve(
215
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
216
+ payload({ pngBase64: PNG_A }),
217
+ );
218
+ await p1;
219
+
220
+ // Same conversation can re-start without being blocked.
221
+ const p2 = proxy.request(
222
+ "app_control_start",
223
+ { tool: "start", app: "com.example.editor" },
224
+ "conv-1",
225
+ ctrl.signal,
226
+ );
227
+ expect(sentMessages).toHaveLength(2);
228
+ proxy.resolve(
229
+ (sentMessages[1] as Record<string, unknown>).requestId as string,
230
+ payload({ pngBase64: PNG_B }),
231
+ );
232
+ const result2 = await p2;
233
+ expect(result2.isError).toBe(false);
234
+
235
+ proxy.dispose();
236
+ });
237
+
238
+ test("non-start tools are not gated by the lock", async () => {
239
+ const proxy = new HostAppControlProxy("conv-2");
240
+ const ctrl = new AbortController();
241
+
242
+ // Pretend another conversation already owns the session by manually
243
+ // priming the lock via a successful start in conv-1.
244
+ const proxyOwner = new HostAppControlProxy("conv-1");
245
+ const startCtrl = new AbortController();
246
+ const pStart = proxyOwner.request(
247
+ "app_control_start",
248
+ { tool: "start", app: "com.example.editor" },
249
+ "conv-1",
250
+ startCtrl.signal,
251
+ );
252
+ proxyOwner.resolve(
253
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
254
+ payload({ pngBase64: PNG_A }),
255
+ );
256
+ await pStart;
257
+
258
+ sentMessages.length = 0;
259
+
260
+ // conv-2 issues a non-start tool — proxy must NOT short-circuit on
261
+ // the lock; the dispatch goes through.
262
+ const observePromise = proxy.request(
263
+ "app_control_observe",
264
+ { tool: "observe", app: "com.example.editor" },
265
+ "conv-2",
266
+ ctrl.signal,
267
+ );
268
+ expect(sentMessages).toHaveLength(1);
269
+ const sent = sentMessages[0] as Record<string, unknown>;
270
+ expect(sent.toolName).toBe("app_control_observe");
271
+
272
+ proxy.resolve(sent.requestId as string, payload({ pngBase64: PNG_B }));
273
+ const result = await observePromise;
274
+ expect(result.isError).toBe(false);
275
+
276
+ proxy.dispose();
277
+ proxyOwner.dispose();
278
+ });
279
+ });
280
+
281
+ // -------------------------------------------------------------------------
282
+ // (c) PNG-hash loop guard
283
+ // -------------------------------------------------------------------------
284
+
285
+ describe("PNG-hash loop guard", () => {
286
+ test("attaches stuck warning after 5 identical observations", async () => {
287
+ const proxy = new HostAppControlProxy("conv-1");
288
+ const ctrl = new AbortController();
289
+
290
+ // First observation establishes the baseline (count = 0).
291
+ const p0 = proxy.request(
292
+ "app_control_observe",
293
+ { tool: "observe", app: "com.example.editor" },
294
+ "conv-1",
295
+ ctrl.signal,
296
+ );
297
+ proxy.resolve(
298
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
299
+ payload({ pngBase64: PNG_A }),
300
+ );
301
+ const r0 = await p0;
302
+ expect(r0.content).not.toContain("WARNING");
303
+ expect(proxy.observationRepeatCount).toBe(0);
304
+
305
+ // 3 additional identical observations bring the repeat count to 3 —
306
+ // still below the threshold (4).
307
+ for (let i = 0; i < 3; i++) {
308
+ const p = proxy.request(
309
+ "app_control_observe",
310
+ { tool: "observe", app: "com.example.editor" },
311
+ "conv-1",
312
+ ctrl.signal,
313
+ );
314
+ const sent = sentMessages[i + 1] as Record<string, unknown>;
315
+ proxy.resolve(sent.requestId as string, payload({ pngBase64: PNG_A }));
316
+ const r = await p;
317
+ expect(r.content).not.toContain("WARNING");
318
+ }
319
+ expect(proxy.observationRepeatCount).toBe(3);
320
+
321
+ // 5th total identical observation — count reaches 4, warning fires.
322
+ const pFinal = proxy.request(
323
+ "app_control_observe",
324
+ { tool: "observe", app: "com.example.editor" },
325
+ "conv-1",
326
+ ctrl.signal,
327
+ );
328
+ const sentFinal = sentMessages[4] as Record<string, unknown>;
329
+ proxy.resolve(
330
+ sentFinal.requestId as string,
331
+ payload({ pngBase64: PNG_A }),
332
+ );
333
+ const rFinal = await pFinal;
334
+ expect(rFinal.content).toContain("WARNING");
335
+ expect(rFinal.content.toLowerCase()).toContain("stuck");
336
+ expect(proxy.observationRepeatCount).toBe(4);
337
+
338
+ proxy.dispose();
339
+ });
340
+
341
+ test("resets repeat count when the screenshot hash differs", async () => {
342
+ const proxy = new HostAppControlProxy("conv-1");
343
+ const ctrl = new AbortController();
344
+
345
+ // Establish baseline at PNG_A.
346
+ const p1 = proxy.request(
347
+ "app_control_observe",
348
+ { tool: "observe", app: "com.example.editor" },
349
+ "conv-1",
350
+ ctrl.signal,
351
+ );
352
+ proxy.resolve(
353
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
354
+ payload({ pngBase64: PNG_A }),
355
+ );
356
+ await p1;
357
+
358
+ // Repeat 3 times to bring count to 3.
359
+ for (let i = 0; i < 3; i++) {
360
+ const p = proxy.request(
361
+ "app_control_observe",
362
+ { tool: "observe", app: "com.example.editor" },
363
+ "conv-1",
364
+ ctrl.signal,
365
+ );
366
+ const sent = sentMessages[i + 1] as Record<string, unknown>;
367
+ proxy.resolve(sent.requestId as string, payload({ pngBase64: PNG_A }));
368
+ await p;
369
+ }
370
+ expect(proxy.observationRepeatCount).toBe(3);
371
+
372
+ // A different PNG resets the count to 0.
373
+ const pDiff = proxy.request(
374
+ "app_control_observe",
375
+ { tool: "observe", app: "com.example.editor" },
376
+ "conv-1",
377
+ ctrl.signal,
378
+ );
379
+ const sentDiff = sentMessages[4] as Record<string, unknown>;
380
+ proxy.resolve(
381
+ sentDiff.requestId as string,
382
+ payload({ pngBase64: PNG_B }),
383
+ );
384
+ const rDiff = await pDiff;
385
+ expect(rDiff.content).not.toContain("WARNING");
386
+ expect(proxy.observationRepeatCount).toBe(0);
387
+
388
+ proxy.dispose();
389
+ });
390
+
391
+ test("non-running states do not feed the loop guard", async () => {
392
+ const proxy = new HostAppControlProxy("conv-1");
393
+ const ctrl = new AbortController();
394
+
395
+ // Several observations with state != running (and identical PNGs)
396
+ // should not increment the repeat count.
397
+ for (let i = 0; i < 6; i++) {
398
+ const p = proxy.request(
399
+ "app_control_observe",
400
+ { tool: "observe", app: "com.example.editor" },
401
+ "conv-1",
402
+ ctrl.signal,
403
+ );
404
+ const sent = sentMessages[i] as Record<string, unknown>;
405
+ proxy.resolve(
406
+ sent.requestId as string,
407
+ payload({ state: "minimized", pngBase64: PNG_A }),
408
+ );
409
+ const r = await p;
410
+ expect(r.content).not.toContain("WARNING");
411
+ }
412
+ expect(proxy.observationRepeatCount).toBe(0);
413
+
414
+ proxy.dispose();
415
+ });
416
+ });
417
+
418
+ // -------------------------------------------------------------------------
419
+ // (d) dispose releases the lock
420
+ // -------------------------------------------------------------------------
421
+
422
+ describe("dispose lock release", () => {
423
+ test("releases singleton lock so a new conversation can start", async () => {
424
+ const proxy1 = new HostAppControlProxy("conv-1");
425
+ const ctrl1 = new AbortController();
426
+
427
+ const p1 = proxy1.request(
428
+ "app_control_start",
429
+ { tool: "start", app: "com.example.editor" },
430
+ "conv-1",
431
+ ctrl1.signal,
432
+ );
433
+ proxy1.resolve(
434
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
435
+ payload({ pngBase64: PNG_A }),
436
+ );
437
+ await p1;
438
+ expect(_getActiveAppControlConversationId()).toBe("conv-1");
439
+
440
+ proxy1.dispose();
441
+ expect(_getActiveAppControlConversationId()).toBeUndefined();
442
+
443
+ // Now a new conversation can acquire the lock.
444
+ sentMessages.length = 0;
445
+ const proxy2 = new HostAppControlProxy("conv-2");
446
+ const ctrl2 = new AbortController();
447
+
448
+ const p2 = proxy2.request(
449
+ "app_control_start",
450
+ { tool: "start", app: "com.example.editor" },
451
+ "conv-2",
452
+ ctrl2.signal,
453
+ );
454
+ expect(sentMessages).toHaveLength(1); // Dispatch happened — not blocked
455
+ proxy2.resolve(
456
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
457
+ payload({ pngBase64: PNG_B }),
458
+ );
459
+ const result = await p2;
460
+ expect(result.isError).toBe(false);
461
+ expect(_getActiveAppControlConversationId()).toBe("conv-2");
462
+
463
+ proxy2.dispose();
464
+ });
465
+
466
+ test("dispose by a non-holder does not clear the lock", async () => {
467
+ const proxyOwner = new HostAppControlProxy("conv-1");
468
+ const ctrl = new AbortController();
469
+
470
+ const pStart = proxyOwner.request(
471
+ "app_control_start",
472
+ { tool: "start", app: "com.example.editor" },
473
+ "conv-1",
474
+ ctrl.signal,
475
+ );
476
+ proxyOwner.resolve(
477
+ (sentMessages[0] as Record<string, unknown>).requestId as string,
478
+ payload({ pngBase64: PNG_A }),
479
+ );
480
+ await pStart;
481
+ expect(_getActiveAppControlConversationId()).toBe("conv-1");
482
+
483
+ // A different conversation's proxy disposes — the lock should remain
484
+ // with conv-1.
485
+ const proxyOther = new HostAppControlProxy("conv-2");
486
+ proxyOther.dispose();
487
+ expect(_getActiveAppControlConversationId()).toBe("conv-1");
488
+
489
+ proxyOwner.dispose();
490
+ expect(_getActiveAppControlConversationId()).toBeUndefined();
491
+ });
492
+ });
493
+
494
+ // -------------------------------------------------------------------------
495
+ // (e) Abort
496
+ // -------------------------------------------------------------------------
497
+
498
+ describe("abort", () => {
499
+ test("propagates abort and emits cancel envelope", async () => {
500
+ const proxy = new HostAppControlProxy("conv-1");
501
+ const controller = new AbortController();
502
+
503
+ const resultPromise = proxy.request(
504
+ "app_control_observe",
505
+ { tool: "observe", app: "com.example.editor" },
506
+ "conv-1",
507
+ controller.signal,
508
+ );
509
+
510
+ expect(sentMessages).toHaveLength(1);
511
+ const sent = sentMessages[0] as Record<string, unknown>;
512
+ const requestId = sent.requestId as string;
513
+ expect(proxy.hasPendingRequest(requestId)).toBe(true);
514
+
515
+ controller.abort();
516
+
517
+ const result = await resultPromise;
518
+ expect(result.isError).toBe(true);
519
+ expect(result.content).toContain("Aborted");
520
+ expect(proxy.hasPendingRequest(requestId)).toBe(false);
521
+
522
+ // Cancel envelope was broadcast.
523
+ expect(sentMessages).toHaveLength(2);
524
+ const cancel = sentMessages[1] as Record<string, unknown>;
525
+ expect(cancel.type).toBe("host_app_control_cancel");
526
+ expect(cancel.requestId).toBe(requestId);
527
+ expect(cancel.conversationId).toBe("conv-1");
528
+
529
+ proxy.dispose();
530
+ });
531
+
532
+ test("returns immediately when signal is already aborted", async () => {
533
+ const proxy = new HostAppControlProxy("conv-1");
534
+ const controller = new AbortController();
535
+ controller.abort();
536
+
537
+ const result = await proxy.request(
538
+ "app_control_observe",
539
+ { tool: "observe", app: "com.example.editor" },
540
+ "conv-1",
541
+ controller.signal,
542
+ );
543
+
544
+ expect(result.isError).toBe(true);
545
+ expect(result.content).toContain("Aborted");
546
+ expect(sentMessages).toHaveLength(0); // No envelope sent
547
+
548
+ proxy.dispose();
549
+ });
550
+ });
551
+
552
+ // -------------------------------------------------------------------------
553
+ // No 50-step cap (different policy from CU)
554
+ // -------------------------------------------------------------------------
555
+
556
+ describe("no step cap", () => {
557
+ test("100 sequential requests dispatch without an artificial limit", async () => {
558
+ const proxy = new HostAppControlProxy("conv-1");
559
+ const ctrl = new AbortController();
560
+
561
+ for (let i = 0; i < 100; i++) {
562
+ const p = proxy.request(
563
+ "app_control_observe",
564
+ { tool: "observe", app: "com.example.editor" },
565
+ "conv-1",
566
+ ctrl.signal,
567
+ );
568
+ const sent = sentMessages[i] as Record<string, unknown>;
569
+ // Alternate PNGs so the loop guard does not fire.
570
+ proxy.resolve(
571
+ sent.requestId as string,
572
+ payload({ pngBase64: i % 2 === 0 ? PNG_A : PNG_B }),
573
+ );
574
+ const r = await p;
575
+ expect(r.isError).toBe(false);
576
+ }
577
+ expect(sentMessages).toHaveLength(100);
578
+
579
+ proxy.dispose();
580
+ });
581
+ });
582
+
583
+ // -------------------------------------------------------------------------
584
+ // isAvailable
585
+ // -------------------------------------------------------------------------
586
+
587
+ describe("isAvailable", () => {
588
+ test("returns false when no host_app_control client is connected", () => {
589
+ const proxy = new HostAppControlProxy("conv-1");
590
+ mockHasClient = false;
591
+ expect(proxy.isAvailable()).toBe(false);
592
+ proxy.dispose();
593
+ });
594
+
595
+ test("returns true when a host_app_control client is connected", () => {
596
+ const proxy = new HostAppControlProxy("conv-1");
597
+ mockHasClient = true;
598
+ expect(proxy.isAvailable()).toBe(true);
599
+ proxy.dispose();
600
+ });
601
+ });
602
+ });