@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -0,0 +1,304 @@
1
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Module mocks — must be in place before dynamic imports resolve transitive
5
+ // dependencies (emit-feed-event -> feed-writer -> assistant-event-hub).
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
9
+
10
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
11
+ assistantEventHub: {
12
+ publish: publishSpy,
13
+ subscribe: () => () => {},
14
+ },
15
+ }));
16
+
17
+ // Capture emitFeedEvent calls without hitting the real persistence layer.
18
+ const emitFeedEventCalls: Array<{
19
+ source: string;
20
+ title: string;
21
+ summary: string;
22
+ dedupKey?: string;
23
+ urgency?: string;
24
+ }> = [];
25
+
26
+ mock.module("../../home/emit-feed-event.js", () => ({
27
+ emitFeedEvent: async (params: {
28
+ source: string;
29
+ title: string;
30
+ summary: string;
31
+ dedupKey?: string;
32
+ urgency?: string;
33
+ }) => {
34
+ emitFeedEventCalls.push(params);
35
+ return { id: params.dedupKey ?? "mock-id", ...params };
36
+ },
37
+ }));
38
+
39
+ // Stub heavy transitive dependencies that the resolvers import so the
40
+ // test can load the module without standing up a full daemon environment.
41
+
42
+ mock.module("../../calls/call-domain.js", () => ({
43
+ answerCall: async () => ({ ok: true }),
44
+ }));
45
+
46
+ mock.module("../../config/env.js", () => ({
47
+ getGatewayInternalBaseUrl: () => "http://localhost:0",
48
+ }));
49
+
50
+ mock.module("../../contacts/contact-store.js", () => ({
51
+ findContactChannel: () => null,
52
+ }));
53
+
54
+ mock.module("../../contacts/contacts-write.js", () => ({
55
+ upsertContactChannel: () => {},
56
+ }));
57
+
58
+ mock.module("../../memory/canonical-guardian-store.js", () => ({
59
+ getCanonicalGuardianRequest: () => null,
60
+ }));
61
+
62
+ mock.module("../../notifications/emit-signal.js", () => ({
63
+ emitNotificationSignal: async () => {},
64
+ }));
65
+
66
+ mock.module("../../notifications/signal.js", () => ({
67
+ isNotificationSourceChannel: () => false,
68
+ }));
69
+
70
+ mock.module("../../permissions/trust-store.js", () => ({
71
+ addRule: () => {},
72
+ }));
73
+
74
+ mock.module("../../permissions/v2-consent-policy.js", () => ({
75
+ isPermissionControlsV2Enabled: () => false,
76
+ }));
77
+
78
+ mock.module("../../runtime/assistant-scope.js", () => ({
79
+ DAEMON_INTERNAL_ASSISTANT_ID: "self",
80
+ }));
81
+
82
+ mock.module("../../runtime/auth/token-service.js", () => ({
83
+ mintDaemonDeliveryToken: () => "mock-token",
84
+ }));
85
+
86
+ mock.module("../../runtime/channel-approval-types.js", () => ({}));
87
+
88
+ mock.module("../../runtime/channel-verification-service.js", () => ({
89
+ createOutboundSession: () => ({
90
+ sessionId: "mock-session",
91
+ secret: "123456",
92
+ }),
93
+ }));
94
+
95
+ mock.module("../../runtime/gateway-client.js", () => ({
96
+ deliverChannelReply: async () => {},
97
+ }));
98
+
99
+ // Stub pending-interactions so tool_approval resolver can find/resolve.
100
+ let mockInteraction: Record<string, unknown> | null = null;
101
+ let mockResolved: Record<string, unknown> | null = null;
102
+
103
+ mock.module("../../runtime/pending-interactions.js", () => ({
104
+ get: () => mockInteraction,
105
+ resolve: () => mockResolved,
106
+ }));
107
+
108
+ mock.module("../../tools/registry.js", () => ({
109
+ getTool: () => null,
110
+ }));
111
+
112
+ mock.module("../../tools/tool-approval-handler.js", () => ({
113
+ TC_GRANT_WAIT_MAX_MS: 30_000,
114
+ }));
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Import the resolvers after all mocks are in place.
118
+ // ---------------------------------------------------------------------------
119
+
120
+ const { getResolver } = await import("../guardian-request-resolvers.js");
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Helpers
124
+ // ---------------------------------------------------------------------------
125
+
126
+ function makeRequest(overrides: Record<string, unknown> = {}) {
127
+ return {
128
+ id: "req-001",
129
+ kind: "tool_approval",
130
+ status: "pending",
131
+ conversationId: "conv-abc",
132
+ toolName: "web_fetch",
133
+ sourceChannel: "vellum",
134
+ createdAt: new Date().toISOString(),
135
+ updatedAt: new Date().toISOString(),
136
+ ...overrides,
137
+ } as never;
138
+ }
139
+
140
+ function makeCtx(
141
+ request: ReturnType<typeof makeRequest>,
142
+ decision: { action: string; userText?: string },
143
+ ) {
144
+ return {
145
+ request,
146
+ decision,
147
+ actor: {
148
+ actorPrincipalId: "principal-1",
149
+ actorExternalUserId: undefined,
150
+ channel: "vellum",
151
+ guardianPrincipalId: "principal-1",
152
+ },
153
+ } as never;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Tests
158
+ // ---------------------------------------------------------------------------
159
+
160
+ describe("guardian approval feed events", () => {
161
+ beforeEach(() => {
162
+ emitFeedEventCalls.length = 0;
163
+ mockInteraction = {
164
+ confirmationDetails: { toolName: "web_fetch" },
165
+ };
166
+ mockResolved = {
167
+ conversation: {
168
+ handleConfirmationResponse: () => {},
169
+ },
170
+ };
171
+ });
172
+
173
+ afterEach(() => {
174
+ mockInteraction = null;
175
+ mockResolved = null;
176
+ });
177
+
178
+ // -----------------------------------------------------------------------
179
+ // tool_approval (pendingInteractionResolver)
180
+ // -----------------------------------------------------------------------
181
+
182
+ describe("tool_approval", () => {
183
+ test("approval emits with title 'Tool Request Approved'", async () => {
184
+ const resolver = getResolver("tool_approval")!;
185
+ expect(resolver).toBeDefined();
186
+
187
+ const request = makeRequest();
188
+ const ctx = makeCtx(request, { action: "approve_once" });
189
+ await resolver.resolve(ctx);
190
+
191
+ // Allow microtask for the void promise to settle.
192
+ await new Promise((r) => setTimeout(r, 10));
193
+
194
+ const call = emitFeedEventCalls.find(
195
+ (c) => c.title === "Tool Request Approved",
196
+ );
197
+ expect(call).toBeDefined();
198
+ expect(call!.source).toBe("assistant");
199
+ expect(call!.summary).toContain("Approved");
200
+ expect(call!.summary).toContain("web_fetch");
201
+ expect(call!.urgency).toBeUndefined();
202
+ });
203
+
204
+ test("rejection emits with title 'Tool Request Denied' and urgency 'medium'", async () => {
205
+ const resolver = getResolver("tool_approval")!;
206
+
207
+ const request = makeRequest();
208
+ const ctx = makeCtx(request, { action: "reject" });
209
+ await resolver.resolve(ctx);
210
+
211
+ await new Promise((r) => setTimeout(r, 10));
212
+
213
+ const call = emitFeedEventCalls.find(
214
+ (c) => c.title === "Tool Request Denied",
215
+ );
216
+ expect(call).toBeDefined();
217
+ expect(call!.urgency).toBe("medium");
218
+ expect(call!.summary).toContain("Denied");
219
+ });
220
+
221
+ test("dedupKey includes request ID", async () => {
222
+ const resolver = getResolver("tool_approval")!;
223
+
224
+ const request = makeRequest({ id: "req-unique-42" });
225
+ const ctx = makeCtx(request, { action: "approve_once" });
226
+ await resolver.resolve(ctx);
227
+
228
+ await new Promise((r) => setTimeout(r, 10));
229
+
230
+ const call = emitFeedEventCalls[0];
231
+ expect(call).toBeDefined();
232
+ expect(call!.dedupKey).toBe("guardian-approval:req-unique-42");
233
+ });
234
+ });
235
+
236
+ // -----------------------------------------------------------------------
237
+ // access_request (accessRequestResolver)
238
+ // -----------------------------------------------------------------------
239
+
240
+ describe("access_request", () => {
241
+ test("approval emits with title 'Access Request Approved'", async () => {
242
+ const resolver = getResolver("access_request")!;
243
+ expect(resolver).toBeDefined();
244
+
245
+ const request = makeRequest({
246
+ kind: "access_request",
247
+ requesterExternalUserId: "user-123",
248
+ requesterChatId: "chat-456",
249
+ });
250
+ const ctx = makeCtx(request, { action: "approve_once" });
251
+ await resolver.resolve(ctx);
252
+
253
+ await new Promise((r) => setTimeout(r, 10));
254
+
255
+ const call = emitFeedEventCalls.find(
256
+ (c) => c.title === "Access Request Approved",
257
+ );
258
+ expect(call).toBeDefined();
259
+ expect(call!.source).toBe("assistant");
260
+ expect(call!.summary).toContain("Granted");
261
+ expect(call!.urgency).toBeUndefined();
262
+ });
263
+
264
+ test("denial emits with title 'Access Request Denied' and urgency 'medium'", async () => {
265
+ const resolver = getResolver("access_request")!;
266
+
267
+ const request = makeRequest({
268
+ kind: "access_request",
269
+ requesterExternalUserId: "user-123",
270
+ requesterChatId: "chat-456",
271
+ });
272
+ const ctx = makeCtx(request, { action: "reject" });
273
+ await resolver.resolve(ctx);
274
+
275
+ await new Promise((r) => setTimeout(r, 10));
276
+
277
+ const call = emitFeedEventCalls.find(
278
+ (c) => c.title === "Access Request Denied",
279
+ );
280
+ expect(call).toBeDefined();
281
+ expect(call!.urgency).toBe("medium");
282
+ expect(call!.summary).toContain("Denied");
283
+ });
284
+
285
+ test("dedupKey includes request ID", async () => {
286
+ const resolver = getResolver("access_request")!;
287
+
288
+ const request = makeRequest({
289
+ id: "req-access-99",
290
+ kind: "access_request",
291
+ requesterExternalUserId: "user-123",
292
+ requesterChatId: "chat-456",
293
+ });
294
+ const ctx = makeCtx(request, { action: "approve_once" });
295
+ await resolver.resolve(ctx);
296
+
297
+ await new Promise((r) => setTimeout(r, 10));
298
+
299
+ const call = emitFeedEventCalls[0];
300
+ expect(call).toBeDefined();
301
+ expect(call!.dedupKey).toBe("guardian-access:req-access-99");
302
+ });
303
+ });
304
+ });
@@ -15,6 +15,7 @@ import { answerCall } from "../calls/call-domain.js";
15
15
  import { getGatewayInternalBaseUrl } from "../config/env.js";
16
16
  import { findContactChannel } from "../contacts/contact-store.js";
17
17
  import { upsertContactChannel } from "../contacts/contacts-write.js";
18
+ import { emitFeedEvent } from "../home/emit-feed-event.js";
18
19
  import {
19
20
  type CanonicalGuardianRequest,
20
21
  getCanonicalGuardianRequest,
@@ -266,6 +267,20 @@ const pendingInteractionResolver: GuardianRequestResolver = {
266
267
  ctx.emissionContext,
267
268
  );
268
269
 
270
+ const approved = decision.action !== "reject";
271
+ void emitFeedEvent({
272
+ source: "assistant",
273
+ title: approved ? "Tool Request Approved" : "Tool Request Denied",
274
+ summary: `${approved ? "Approved" : "Denied"} access to ${request.toolName ?? "unknown tool"}.`,
275
+ dedupKey: `guardian-approval:${request.id}`,
276
+ urgency: approved ? undefined : "medium",
277
+ }).catch((err) => {
278
+ log.warn(
279
+ { err, requestId: request.id },
280
+ "Failed to emit guardian approval feed event",
281
+ );
282
+ });
283
+
269
284
  log.info(
270
285
  {
271
286
  event: "resolver_tool_approval_applied",
@@ -518,6 +533,19 @@ const accessRequestResolver: GuardianRequestResolver = {
518
533
  }
519
534
  }
520
535
 
536
+ void emitFeedEvent({
537
+ source: "assistant",
538
+ title: "Access Request Denied",
539
+ summary: `Denied access request.`,
540
+ dedupKey: `guardian-access:${request.id}`,
541
+ urgency: "medium",
542
+ }).catch((err) => {
543
+ log.warn(
544
+ { err, requestId: request.id },
545
+ "Failed to emit access request feed event",
546
+ );
547
+ });
548
+
521
549
  return {
522
550
  ok: true,
523
551
  applied: true,
@@ -558,6 +586,19 @@ const accessRequestResolver: GuardianRequestResolver = {
558
586
  "Access request resolver: voice approval — direct trusted-contact activation (no verification session)",
559
587
  );
560
588
 
589
+ void emitFeedEvent({
590
+ source: "assistant",
591
+ title: "Access Request Approved",
592
+ summary: `Granted access request.`,
593
+ dedupKey: `guardian-access:${request.id}`,
594
+ urgency: undefined,
595
+ }).catch((err) => {
596
+ log.warn(
597
+ { err, requestId: request.id },
598
+ "Failed to emit access request feed event",
599
+ );
600
+ });
601
+
561
602
  return { ok: true, applied: true };
562
603
  }
563
604
 
@@ -793,6 +834,19 @@ const accessRequestResolver: GuardianRequestResolver = {
793
834
  ? `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes.`
794
835
  : `Access approved for ${requesterLabel}. Give them this verification code: \`${session.secret}\`. The code expires in 10 minutes. I could not notify them automatically, so please tell them to send the code manually.`;
795
836
 
837
+ void emitFeedEvent({
838
+ source: "assistant",
839
+ title: "Access Request Approved",
840
+ summary: `Granted access request.`,
841
+ dedupKey: `guardian-access:${request.id}`,
842
+ urgency: undefined,
843
+ }).catch((err) => {
844
+ log.warn(
845
+ { err, requestId: request.id },
846
+ "Failed to emit access request feed event",
847
+ );
848
+ });
849
+
796
850
  return {
797
851
  ok: true,
798
852
  applied: true,
@@ -867,6 +921,19 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
867
921
  }
868
922
  }
869
923
 
924
+ void emitFeedEvent({
925
+ source: "assistant",
926
+ title: "Tool Grant Denied",
927
+ summary: `Denied grant request for ${request.toolName ?? "unknown tool"}.`,
928
+ dedupKey: `guardian-grant:${request.id}`,
929
+ urgency: "medium",
930
+ }).catch((err) => {
931
+ log.warn(
932
+ { err, requestId: request.id },
933
+ "Failed to emit tool grant denial feed event",
934
+ );
935
+ });
936
+
870
937
  return { ok: true, applied: true };
871
938
  }
872
939
 
@@ -965,6 +1032,19 @@ const toolGrantRequestResolver: GuardianRequestResolver = {
965
1032
  }
966
1033
  }
967
1034
 
1035
+ void emitFeedEvent({
1036
+ source: "assistant",
1037
+ title: "Tool Grant Approved",
1038
+ summary: `Approved grant request for ${request.toolName ?? "unknown tool"}.`,
1039
+ dedupKey: `guardian-grant:${request.id}`,
1040
+ urgency: undefined,
1041
+ }).catch((err) => {
1042
+ log.warn(
1043
+ { err, requestId: request.id },
1044
+ "Failed to emit tool grant approval feed event",
1045
+ );
1046
+ });
1047
+
968
1048
  return { ok: true, applied: true, grantMinted: false };
969
1049
  },
970
1050
  };
@@ -20,23 +20,13 @@ import {
20
20
  import { unlink, writeFile } from "node:fs/promises";
21
21
  import { tmpdir } from "node:os";
22
22
  import { join } from "node:path";
23
- import {
24
- afterEach,
25
- beforeEach,
26
- describe,
27
- expect,
28
- mock,
29
- test,
30
- } from "bun:test";
23
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
31
24
 
32
25
  import type { BackupConfig, BackupDestination } from "../../config/schema.js";
33
26
  import { BackupConfigSchema } from "../../config/schema.js";
34
27
  import type { StreamExportVBundleResult } from "../../runtime/migrations/vbundle-builder.js";
35
28
  import type { BackupDeps } from "../backup-worker.js";
36
- import {
37
- createSnapshotNow,
38
- runBackupTick,
39
- } from "../backup-worker.js";
29
+ import { createSnapshotNow, runBackupTick } from "../backup-worker.js";
40
30
 
41
31
  // ---------------------------------------------------------------------------
42
32
  // Test fixtures
@@ -190,7 +180,6 @@ describe("runBackupTick — gating", () => {
190
180
  snapshotLockPath: join(ROOT, ".snapshot.lock"),
191
181
  // Explicit plaintext to avoid touching the key file
192
182
  trustPath: join(ROOT, "trust.json"),
193
- hooksDir: join(ROOT, "hooks"),
194
183
  });
195
184
 
196
185
  expect(result).toBeNull();
@@ -42,14 +42,10 @@ import {
42
42
  getDbPath,
43
43
  getProtectedDir,
44
44
  getWorkspaceDir,
45
- getWorkspaceHooksDir,
46
45
  } from "../util/platform.js";
47
46
  import { ensureBackupKey as realEnsureBackupKey } from "./backup-key.js";
48
47
  import type { SnapshotEntry } from "./list-snapshots.js";
49
- import {
50
- pruneLocalSnapshots,
51
- writeLocalSnapshot,
52
- } from "./local-writer.js";
48
+ import { pruneLocalSnapshots, writeLocalSnapshot } from "./local-writer.js";
53
49
  import type { OffsiteWriteResult } from "./offsite-writer.js";
54
50
  import {
55
51
  pruneOffsiteSnapshotsInAll,
@@ -60,10 +56,7 @@ import {
60
56
  getLocalBackupsDir,
61
57
  resolveOffsiteDestinations,
62
58
  } from "./paths.js";
63
- import {
64
- acquireSnapshotLock,
65
- getSnapshotLockPath,
66
- } from "./snapshot-lock.js";
59
+ import { acquireSnapshotLock, getSnapshotLockPath } from "./snapshot-lock.js";
67
60
 
68
61
  const log = getLogger("backup-worker");
69
62
 
@@ -118,8 +111,6 @@ export interface BackupDeps {
118
111
  localDir?: string;
119
112
  /** Override for the trust.json path (tests). */
120
113
  trustPath?: string;
121
- /** Override for the hooks directory (tests). */
122
- hooksDir?: string;
123
114
  /** Override for the backup key file path (tests). */
124
115
  backupKeyPath?: string;
125
116
  /**
@@ -187,9 +178,7 @@ async function performBackup(
187
178
  const ensureKey = deps.ensureBackupKey ?? realEnsureBackupKey;
188
179
  const workspaceDir = deps.workspaceDir ?? getWorkspaceDir();
189
180
  const localDir = deps.localDir ?? getLocalBackupsDir(config.localDirectory);
190
- const trustPath =
191
- deps.trustPath ?? join(getProtectedDir(), "trust.json");
192
- const hooksDir = deps.hooksDir ?? getWorkspaceHooksDir();
181
+ const trustPath = deps.trustPath ?? join(getProtectedDir(), "trust.json");
193
182
  const backupKeyPath = deps.backupKeyPath ?? getBackupKeyPath();
194
183
 
195
184
  const startTimestamp = Date.now();
@@ -207,7 +196,6 @@ async function performBackup(
207
196
  const result = await streamExport({
208
197
  workspaceDir,
209
198
  trustPath,
210
- hooksDir,
211
199
  source: "backup-worker",
212
200
  description: "Automated backup snapshot",
213
201
  checkpoint: () => {
@@ -268,14 +268,97 @@ async function resolveAppImports(srcDir: string): Promise<void> {
268
268
  }
269
269
  }
270
270
 
271
+ /**
272
+ * Per-appDir compile serialisation.
273
+ *
274
+ * compileApp() begins by `rm -rf dist/`, so two concurrent compiles on the
275
+ * same appDir can wipe each other's intermediate output. To prevent that
276
+ * while still picking up source edits that arrive mid-build, we track a
277
+ * two-slot queue per appDir:
278
+ *
279
+ * - `current`: the compile that is currently writing to dist/.
280
+ * - `pending`: at most one coalesced follow-up compile queued because a new
281
+ * caller arrived while `current` was running. Additional callers arriving
282
+ * during that window share `pending` — they do not spawn yet another run.
283
+ * Once `current` settles, `pending` is promoted to `current` and begins
284
+ * executing; new callers arriving after promotion queue a fresh `pending`.
285
+ *
286
+ * This keeps dist/ consistent under concurrency while guaranteeing that any
287
+ * source mutation observed after a compile starts will be reflected in a
288
+ * subsequent compile pass rather than silently dropped.
289
+ */
290
+ interface CompileSlot {
291
+ current: Promise<CompileResult>;
292
+ pending?: Promise<CompileResult>;
293
+ }
294
+
295
+ const compileSlots = new Map<string, CompileSlot>();
296
+
271
297
  /**
272
298
  * Compile a TSX app from appDir/src/ into appDir/dist/.
273
299
  *
274
300
  * Expects appDir/src/main.tsx as the entry point and appDir/src/index.html
275
301
  * as the HTML shell. Produces appDir/dist/main.js and appDir/dist/index.html
276
302
  * (with script and optional stylesheet tags injected).
303
+ *
304
+ * Concurrent calls for the same appDir are serialised (see `compileSlots`
305
+ * above). Callers never see a partial or racing dist/ write; callers that
306
+ * represent work requested after a compile started always get a subsequent
307
+ * fresh compile.
277
308
  */
278
- export async function compileApp(appDir: string): Promise<CompileResult> {
309
+ export function compileApp(appDir: string): Promise<CompileResult> {
310
+ const slot = compileSlots.get(appDir);
311
+
312
+ if (!slot) {
313
+ const current = runCompile(appDir);
314
+ const onSettled = () => slotCompileSettled(appDir, current);
315
+ current.then(onSettled, onSettled);
316
+ compileSlots.set(appDir, { current });
317
+ return current;
318
+ }
319
+
320
+ if (slot.pending) return slot.pending;
321
+
322
+ // A second distinct caller arrived while `current` is running. Queue a
323
+ // follow-up that starts once `current` settles (success or failure) so
324
+ // any source edits that happened mid-build still get compiled.
325
+ const rerun = async (): Promise<CompileResult> => {
326
+ try {
327
+ await slot.current;
328
+ } catch {
329
+ // Ignore: we want to rerun regardless of the prior compile's outcome.
330
+ }
331
+ return runCompile(appDir);
332
+ };
333
+ const pending = rerun();
334
+ const onSettled = () => slotCompileSettled(appDir, pending);
335
+ pending.then(onSettled, onSettled);
336
+ slot.pending = pending;
337
+ return pending;
338
+ }
339
+
340
+ function slotCompileSettled(
341
+ appDir: string,
342
+ finished: Promise<CompileResult>,
343
+ ): void {
344
+ const slot = compileSlots.get(appDir);
345
+ if (!slot) return;
346
+
347
+ if (slot.current !== finished) {
348
+ // finished is a rerun that hasn't been promoted yet, or a stale entry.
349
+ // Promotion happens below when `current` settles; there is nothing to do
350
+ // here because a subsequent slotCompileSettled(current) will run first.
351
+ return;
352
+ }
353
+
354
+ if (slot.pending) {
355
+ compileSlots.set(appDir, { current: slot.pending });
356
+ } else {
357
+ compileSlots.delete(appDir);
358
+ }
359
+ }
360
+
361
+ async function runCompile(appDir: string): Promise<CompileResult> {
279
362
  const start = performance.now();
280
363
  const srcDir = join(appDir, "src");
281
364
  const distDir = join(appDir, "dist");
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Call session notifiers and controller registry.
3
3
  *
4
- * Follows the same notifier pattern as watch-state.ts: module-level Maps
5
- * with register/unregister/fire helpers keyed by conversationId.
4
+ * Uses module-level Maps with register/unregister/fire helpers keyed by
5
+ * conversationId.
6
6
  */
7
7
 
8
8
  import { getLogger } from "../util/logger.js";
@@ -58,7 +58,7 @@ describe("isInterfaceId", () => {
58
58
  });
59
59
 
60
60
  describe("supportsHostProxy", () => {
61
- // ── macOS: supports host_bash / host_file / host_cu, but NOT host_browser. ──
61
+ // ── macOS: supports all four host proxy capabilities. ──
62
62
  test("macos returns true (no capability)", () => {
63
63
  expect(supportsHostProxy("macos")).toBe(true);
64
64
  });
@@ -75,8 +75,8 @@ describe("supportsHostProxy", () => {
75
75
  expect(supportsHostProxy("macos", "host_cu")).toBe(true);
76
76
  });
77
77
 
78
- test("macos returns false for host_browser", () => {
79
- expect(supportsHostProxy("macos", "host_browser")).toBe(false);
78
+ test("macos returns true for host_browser", () => {
79
+ expect(supportsHostProxy("macos", "host_browser")).toBe(true);
80
80
  });
81
81
 
82
82
  // ── chrome-extension: only host_browser. ──
@@ -137,10 +137,12 @@ export function supportsHostProxy(
137
137
  id: InterfaceId,
138
138
  capability?: HostProxyCapability,
139
139
  ): boolean {
140
- // host_browser is excluded for macos because the proxy path requires a
141
- // Chrome extension that isn't guaranteed to be attached; browser tools
142
- // fall back to the local Playwright Chromium instead.
143
- if (id === "macos") return capability !== "host_browser";
140
+ // macOS supports all four host proxy capabilities including host_browser.
141
+ // The host_browser proxy is provisioned via the SSE sender path (or via the
142
+ // ChromeExtensionRegistry when an extension connection is present). When no
143
+ // extension is connected, browser tools fall through to cdp-inspect/local
144
+ // via the CDP factory's candidate chain.
145
+ if (id === "macos") return true;
144
146
  if (id === "chrome-extension" && capability === "host_browser") return true;
145
147
  return false;
146
148
  }