@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,181 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../util/logger.js", () => ({
4
+ getLogger: () =>
5
+ new Proxy({} as Record<string, unknown>, {
6
+ get: () => () => {},
7
+ }),
8
+ }));
9
+
10
+ mock.module("../config/loader.js", () => ({
11
+ getConfig: () => ({
12
+ ui: {},
13
+ model: "test",
14
+ provider: "test",
15
+ memory: { enabled: false },
16
+ rateLimit: { maxRequestsPerMinute: 0 },
17
+ secretDetection: { enabled: false },
18
+ }),
19
+ }));
20
+
21
+ import {
22
+ addMessage,
23
+ clearStrippedInjectionMetadataForConversation,
24
+ createConversation,
25
+ getMessages,
26
+ } from "../memory/conversation-crud.js";
27
+ import { getDb, initializeDb } from "../memory/db.js";
28
+
29
+ initializeDb();
30
+
31
+ function resetTables(): void {
32
+ const db = getDb();
33
+ db.run("DELETE FROM message_attachments");
34
+ db.run("DELETE FROM attachments");
35
+ db.run("DELETE FROM messages");
36
+ db.run("DELETE FROM conversations");
37
+ }
38
+
39
+ describe("clearStrippedInjectionMetadataForConversation", () => {
40
+ beforeEach(() => {
41
+ resetTables();
42
+ });
43
+
44
+ test("removes all three stripped-block fields and preserves the rest", async () => {
45
+ const conv = createConversation("Strip metadata test");
46
+ await addMessage(
47
+ conv.id,
48
+ "user",
49
+ "turn 1",
50
+ {
51
+ memoryInjectedBlock: "mem payload",
52
+ turnContextBlock: "<turn_context>\nctx\n</turn_context>",
53
+ workspaceBlock: "<workspace>\nws\n</workspace>",
54
+ nowScratchpadBlock:
55
+ "<NOW.md Always keep this up to date>\nnow body\n</NOW.md>",
56
+ pkbContextBlock: "<knowledge_base>\npkb body\n</knowledge_base>",
57
+ pkbSystemReminderBlock:
58
+ "<system_reminder>\nreminder body\n</system_reminder>",
59
+ },
60
+ { skipIndexing: true },
61
+ );
62
+
63
+ clearStrippedInjectionMetadataForConversation(conv.id);
64
+
65
+ const [row] = getMessages(conv.id);
66
+ const meta = JSON.parse(row.metadata ?? "{}");
67
+
68
+ expect(meta.pkbSystemReminderBlock).toBeUndefined();
69
+ expect(meta.nowScratchpadBlock).toBeUndefined();
70
+ expect(meta.pkbContextBlock).toBeUndefined();
71
+
72
+ // Non-stripped fields must survive — these back blocks that
73
+ // `stripInjectionsForCompaction` intentionally leaves in-memory.
74
+ expect(meta.memoryInjectedBlock).toBe("mem payload");
75
+ expect(meta.turnContextBlock).toBe("<turn_context>\nctx\n</turn_context>");
76
+ expect(meta.workspaceBlock).toBe("<workspace>\nws\n</workspace>");
77
+ });
78
+
79
+ test("is idempotent — re-running is a no-op on already-cleared rows", async () => {
80
+ const conv = createConversation("Idempotent clear");
81
+ await addMessage(
82
+ conv.id,
83
+ "user",
84
+ "turn 1",
85
+ {
86
+ memoryInjectedBlock: "keep me",
87
+ nowScratchpadBlock: "<NOW.md …>body</NOW.md>",
88
+ },
89
+ { skipIndexing: true },
90
+ );
91
+
92
+ clearStrippedInjectionMetadataForConversation(conv.id);
93
+ clearStrippedInjectionMetadataForConversation(conv.id);
94
+
95
+ const [row] = getMessages(conv.id);
96
+ const meta = JSON.parse(row.metadata ?? "{}");
97
+ expect(meta.nowScratchpadBlock).toBeUndefined();
98
+ expect(meta.memoryInjectedBlock).toBe("keep me");
99
+ });
100
+
101
+ test("only targets user rows — assistant metadata is untouched", async () => {
102
+ const conv = createConversation("Role scoping");
103
+ await addMessage(
104
+ conv.id,
105
+ "user",
106
+ "turn 1",
107
+ { nowScratchpadBlock: "<NOW.md …>body</NOW.md>" },
108
+ { skipIndexing: true },
109
+ );
110
+ await addMessage(
111
+ conv.id,
112
+ "assistant",
113
+ "reply",
114
+ // Assistant rows don't carry these blocks in practice, but guard the
115
+ // role filter anyway so an accidental drop of the WHERE clause is
116
+ // surfaced immediately.
117
+ { nowScratchpadBlock: "should-not-be-cleared" },
118
+ { skipIndexing: true },
119
+ );
120
+
121
+ clearStrippedInjectionMetadataForConversation(conv.id);
122
+
123
+ const rows = getMessages(conv.id);
124
+ const userMeta = JSON.parse(rows[0].metadata ?? "{}");
125
+ const assistantMeta = JSON.parse(rows[1].metadata ?? "{}");
126
+ expect(userMeta.nowScratchpadBlock).toBeUndefined();
127
+ expect(assistantMeta.nowScratchpadBlock).toBe("should-not-be-cleared");
128
+ });
129
+
130
+ test("post-clear, rehydration does not re-inject NOW.md / knowledge_base", async () => {
131
+ // Reproduces the divergence described in the Codex P1 feedback:
132
+ // stripInjectionsForCompaction removes <NOW.md …> and <knowledge_base>
133
+ // from the in-memory history during compaction. Without this clear,
134
+ // a subsequent loadFromDb would rehydrate those blocks from stale
135
+ // metadata — re-injecting exactly what compaction removed.
136
+ const conv = createConversation("Rehydrate after strip");
137
+ await addMessage(
138
+ conv.id,
139
+ "user",
140
+ "historical turn",
141
+ {
142
+ memoryInjectedBlock: "mem",
143
+ turnContextBlock: "<turn_context>\nctx\n</turn_context>",
144
+ workspaceBlock: "<workspace>\nws\n</workspace>",
145
+ nowScratchpadBlock:
146
+ "<NOW.md Always keep this up to date>\nnow\n</NOW.md>",
147
+ pkbContextBlock: "<knowledge_base>\npkb\n</knowledge_base>",
148
+ pkbSystemReminderBlock: "<system_reminder>\nsr\n</system_reminder>",
149
+ },
150
+ { skipIndexing: true },
151
+ );
152
+ await addMessage(conv.id, "assistant", "reply", undefined, {
153
+ skipIndexing: true,
154
+ });
155
+ await addMessage(conv.id, "user", "tail turn", undefined, {
156
+ skipIndexing: true,
157
+ });
158
+
159
+ // Simulate the compaction-strip lifecycle point.
160
+ clearStrippedInjectionMetadataForConversation(conv.id);
161
+
162
+ const rows = getMessages(conv.id);
163
+ const historicalMeta = JSON.parse(rows[0].metadata ?? "{}");
164
+
165
+ // Loading this back with loadFromDb prepends fields only when they
166
+ // are present on the row. Confirm the stripped fields are gone so
167
+ // rehydration cannot resurrect them.
168
+ expect(historicalMeta.nowScratchpadBlock).toBeUndefined();
169
+ expect(historicalMeta.pkbContextBlock).toBeUndefined();
170
+ expect(historicalMeta.pkbSystemReminderBlock).toBeUndefined();
171
+
172
+ // And the fields that back blocks `stripInjectionsForCompaction`
173
+ // intentionally preserves (<turn_context>, <workspace>, <memory __injected>)
174
+ // must still be present so the cache prefix remains stable.
175
+ expect(historicalMeta.turnContextBlock).toBe(
176
+ "<turn_context>\nctx\n</turn_context>",
177
+ );
178
+ expect(historicalMeta.workspaceBlock).toBe("<workspace>\nws\n</workspace>");
179
+ expect(historicalMeta.memoryInjectedBlock).toBe("mem");
180
+ });
181
+ });
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Tests for the compaction call-site recovery path added in JARVIS-587.
3
+ *
4
+ * When the `compaction` pipeline exceeds its 30s budget (manifest-wide
5
+ * `DEFAULT_TIMEOUTS.compaction`), the pipeline runner throws
6
+ * `PluginTimeoutError`. The three compaction call sites in
7
+ * `conversation-agent-loop.ts` (start-of-turn, mid-loop, emergency) catch
8
+ * that error, invoke `trackCompactionOutcome(..., true, onEvent)` so the
9
+ * circuit breaker records the failure, and degrade gracefully.
10
+ *
11
+ * This file asserts the tight coupling between:
12
+ * (1) a `PluginTimeoutError`-driven failure and
13
+ * (2) the compaction circuit breaker's 3-strike threshold.
14
+ *
15
+ * The existing `circuit-breaker-pipeline.test.ts` already exercises the
16
+ * breaker's transitions end-to-end. These tests verify that our new
17
+ * catch-blocks feed the breaker the same `"failure"` outcome a normal
18
+ * summary-LLM throw would, and that repeated timeouts therefore trip the
19
+ * same 3-strike trip.
20
+ */
21
+
22
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
23
+
24
+ import { trackCompactionOutcome } from "../daemon/conversation-agent-loop.js";
25
+ import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
26
+ import type { ServerMessage } from "../daemon/message-protocol.js";
27
+ import {
28
+ COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
29
+ defaultCircuitBreakerPlugin,
30
+ } from "../plugins/defaults/circuit-breaker.js";
31
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
32
+ import {
33
+ getMiddlewaresFor,
34
+ registerPlugin,
35
+ resetPluginRegistryForTests,
36
+ } from "../plugins/registry.js";
37
+ import {
38
+ type CompactionArgs,
39
+ type CompactionResult,
40
+ type Middleware,
41
+ PluginTimeoutError,
42
+ type TurnContext,
43
+ } from "../plugins/types.js";
44
+
45
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
46
+
47
+ interface FakeConversationCtx {
48
+ readonly conversationId: string;
49
+ consecutiveCompactionFailures: number;
50
+ compactionCircuitOpenUntil: number | null;
51
+ currentRequestId?: string;
52
+ currentTurnTrustContext?: TrustContext;
53
+ trustContext?: TrustContext;
54
+ turnCount: number;
55
+ }
56
+
57
+ function makeConversationCtx(
58
+ conversationId = "conv-timeout-test",
59
+ ): FakeConversationCtx {
60
+ return {
61
+ conversationId,
62
+ consecutiveCompactionFailures: 0,
63
+ compactionCircuitOpenUntil: null,
64
+ turnCount: 0,
65
+ trustContext: { sourceChannel: "vellum", trustClass: "guardian" },
66
+ };
67
+ }
68
+
69
+ const trust: TrustContext = {
70
+ sourceChannel: "vellum",
71
+ trustClass: "guardian",
72
+ };
73
+
74
+ function makeTurnCtx(conversationId: string): TurnContext {
75
+ return {
76
+ requestId: "req-timeout-test",
77
+ conversationId,
78
+ turnIndex: 0,
79
+ trust,
80
+ };
81
+ }
82
+
83
+ function collectEvents(): {
84
+ events: ServerMessage[];
85
+ onEvent: (msg: ServerMessage) => void;
86
+ } {
87
+ const events: ServerMessage[] = [];
88
+ return { events, onEvent: (msg) => events.push(msg) };
89
+ }
90
+
91
+ // ─── Tests ──────────────────────────────────────────────────────────────────
92
+
93
+ describe("compaction timeout recovery (JARVIS-587)", () => {
94
+ beforeEach(() => {
95
+ resetPluginRegistryForTests();
96
+ registerPlugin(defaultCircuitBreakerPlugin);
97
+ });
98
+
99
+ afterEach(() => {
100
+ resetPluginRegistryForTests();
101
+ });
102
+
103
+ test("runPipeline('compaction', ...) throws PluginTimeoutError on budget breach", async () => {
104
+ // Baseline: the compaction pipeline still surfaces PluginTimeoutError when
105
+ // its timer fires. This guards the outer race from silently swallowing
106
+ // the timeout when the inner call is aborted by our Part A wiring.
107
+ const hang: Middleware<CompactionArgs, CompactionResult> = async (
108
+ _args,
109
+ _next,
110
+ ) =>
111
+ new Promise<CompactionResult>(() => {
112
+ // intentionally never resolves
113
+ });
114
+
115
+ let caught: unknown;
116
+ try {
117
+ await runPipeline<CompactionArgs, CompactionResult>(
118
+ "compaction",
119
+ [hang],
120
+ async () => ({ compacted: false }) as unknown as CompactionResult,
121
+ { messages: [] as unknown, signal: undefined, options: undefined },
122
+ makeTurnCtx("conv-budget-breach"),
123
+ // Override the manifest timeout to keep the test fast.
124
+ 20,
125
+ );
126
+ } catch (err) {
127
+ caught = err;
128
+ }
129
+ expect(caught).toBeInstanceOf(PluginTimeoutError);
130
+ expect((caught as PluginTimeoutError).pipeline).toBe("compaction");
131
+ });
132
+
133
+ test("trackCompactionOutcome(failed=true) driven by PluginTimeoutError trips the breaker at the 3rd strike", async () => {
134
+ // Simulates the production sequence: each mid-loop compaction hits the
135
+ // pipeline's 30s ceiling, the orchestrator's catch block calls
136
+ // `trackCompactionOutcome(ctx, true, onEvent)`. After three such catches
137
+ // the circuit breaker must be open — matching the same invariant the
138
+ // existing breaker test file exercises for normal summary-LLM throws.
139
+ const ctx = makeConversationCtx();
140
+ const { onEvent, events } = collectEvents();
141
+
142
+ // First two timeouts — circuit still closed.
143
+ await trackCompactionOutcome(ctx, true, onEvent);
144
+ await trackCompactionOutcome(ctx, true, onEvent);
145
+ expect(ctx.consecutiveCompactionFailures).toBe(2);
146
+ expect(ctx.compactionCircuitOpenUntil).toBeNull();
147
+ expect(events).toHaveLength(0);
148
+
149
+ // Third timeout — breaker trips and emits the canonical transition event.
150
+ await trackCompactionOutcome(ctx, true, onEvent);
151
+ expect(ctx.consecutiveCompactionFailures).toBe(
152
+ COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
153
+ );
154
+ expect(ctx.compactionCircuitOpenUntil).not.toBeNull();
155
+ expect(events).toHaveLength(1);
156
+ expect(events[0]).toEqual({
157
+ type: "compaction_circuit_open",
158
+ conversationId: ctx.conversationId,
159
+ reason: "3_consecutive_failures",
160
+ openUntil: ctx.compactionCircuitOpenUntil as number,
161
+ });
162
+ });
163
+
164
+ test("a successful compaction after two timeouts resets the counter", async () => {
165
+ // The recovery path doesn't interfere with the breaker's normal reset —
166
+ // once a compaction call eventually succeeds, the streak is broken and
167
+ // the next failure starts counting from 1.
168
+ const ctx = makeConversationCtx();
169
+ const { onEvent } = collectEvents();
170
+
171
+ await trackCompactionOutcome(ctx, true, onEvent);
172
+ await trackCompactionOutcome(ctx, true, onEvent);
173
+ expect(ctx.consecutiveCompactionFailures).toBe(2);
174
+
175
+ await trackCompactionOutcome(ctx, false, onEvent);
176
+ expect(ctx.consecutiveCompactionFailures).toBe(0);
177
+ expect(ctx.compactionCircuitOpenUntil).toBeNull();
178
+
179
+ await trackCompactionOutcome(ctx, true, onEvent);
180
+ expect(ctx.consecutiveCompactionFailures).toBe(1);
181
+ });
182
+
183
+ test("compaction call-site recovery remains defense-in-depth even when DEFAULT_TIMEOUTS.compaction is null", () => {
184
+ // At the time this PR landed, `DEFAULT_TIMEOUTS.compaction` was null
185
+ // (pipeline timeouts globally disabled — see #27608). That makes the
186
+ // call-site catch blocks unreachable in production right now, but the
187
+ // catch blocks still matter: any future reintroduction of a per-pipeline
188
+ // compaction timeout immediately benefits from circuit-breaker recording
189
+ // and graceful-degradation without needing to re-touch every call site.
190
+ //
191
+ // This test just documents the current value so a future change that
192
+ // reintroduces a timeout must also decide (intentionally) whether the
193
+ // recovery path should continue to fire.
194
+ const value = DEFAULT_TIMEOUTS.compaction;
195
+ expect(value === null || typeof value === "number").toBe(true);
196
+ });
197
+ });
198
+
199
+ describe("abort propagation end-to-end (Part A + updateSummary fallback)", () => {
200
+ beforeEach(() => {
201
+ resetPluginRegistryForTests();
202
+ registerPlugin(defaultCircuitBreakerPlugin);
203
+ });
204
+
205
+ afterEach(() => {
206
+ resetPluginRegistryForTests();
207
+ });
208
+
209
+ test("caller-provided signal is replaced with a linked signal that fires on timeout", async () => {
210
+ // Minimal proof that Part A actually wires the signal through the
211
+ // compaction pipeline: when the pipeline runner's timer fires, the
212
+ // signal seen by the inner terminal is aborted — allowing
213
+ // `updateSummary`'s try/catch around `provider.sendMessage` to trigger
214
+ // the local-fallback path instead of the call hanging indefinitely.
215
+ let observedSignal: AbortSignal | undefined;
216
+ const waitForAbort: Middleware<CompactionArgs, CompactionResult> = async (
217
+ args,
218
+ _next,
219
+ ) => {
220
+ observedSignal = args.signal;
221
+ return new Promise<CompactionResult>((_resolve, reject) => {
222
+ args.signal?.addEventListener("abort", () => {
223
+ reject(
224
+ Object.assign(new Error("aborted by signal"), {
225
+ name: "AbortError",
226
+ }),
227
+ );
228
+ });
229
+ });
230
+ };
231
+
232
+ const callerController = new AbortController();
233
+
234
+ let caught: unknown;
235
+ try {
236
+ await runPipeline<CompactionArgs, CompactionResult>(
237
+ "compaction",
238
+ [waitForAbort, ...getMiddlewaresFor("compaction")],
239
+ async () => ({ compacted: false }) as unknown as CompactionResult,
240
+ {
241
+ messages: [] as unknown,
242
+ signal: callerController.signal,
243
+ options: undefined,
244
+ },
245
+ makeTurnCtx("conv-abort-test"),
246
+ 20,
247
+ );
248
+ } catch (err) {
249
+ caught = err;
250
+ }
251
+
252
+ expect(caught).toBeInstanceOf(PluginTimeoutError);
253
+ expect(observedSignal).toBeDefined();
254
+ // The runner swaps the caller's signal for a linked signal — the two are
255
+ // distinct objects but both should end up aborted once the timer fires.
256
+ expect(observedSignal).not.toBe(callerController.signal);
257
+ expect(observedSignal!.aborted).toBe(true);
258
+ // Caller's own signal is untouched — pipeline timeout does not cascade
259
+ // outward onto the caller's controller.
260
+ expect(callerController.signal.aborted).toBe(false);
261
+ });
262
+ });
@@ -0,0 +1,110 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
4
+
5
+ import { invalidateConfigCache } from "../config/loader.js";
6
+ import {
7
+ type ModelSetContext,
8
+ setImageGenModel,
9
+ } from "../daemon/handlers/config-model.js";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const WORKSPACE_DIR = process.env.VELLUM_WORKSPACE_DIR!;
16
+ const CONFIG_PATH = join(WORKSPACE_DIR, "config.json");
17
+
18
+ function ensureTestDir(): void {
19
+ const dirs = [WORKSPACE_DIR, join(WORKSPACE_DIR, "data")];
20
+ for (const dir of dirs) {
21
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ function writeConfig(obj: unknown): void {
26
+ writeFileSync(CONFIG_PATH, JSON.stringify(obj));
27
+ }
28
+
29
+ function readConfig(): Record<string, unknown> {
30
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
31
+ }
32
+
33
+ function makeCtx(): ModelSetContext {
34
+ return {
35
+ conversations: new Map(),
36
+ suppressConfigReload: false,
37
+ setSuppressConfigReload: () => {},
38
+ updateConfigFingerprint: () => {},
39
+ debounceTimers: {
40
+ // No-op scheduler: the test asserts the persisted config shape, not
41
+ // the debounce behaviour. Firing the callback synchronously would
42
+ // mutate state after the assertion; dropping it keeps the test
43
+ // deterministic.
44
+ schedule: (_key: string, _fn: () => void, _ms: number) => {},
45
+ },
46
+ };
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Setup / Teardown
51
+ // ---------------------------------------------------------------------------
52
+
53
+ beforeEach(() => {
54
+ ensureTestDir();
55
+ writeConfig({});
56
+ invalidateConfigCache();
57
+ });
58
+
59
+ afterEach(() => {
60
+ try {
61
+ writeConfig({});
62
+ invalidateConfigCache();
63
+ } catch {
64
+ // best-effort cleanup
65
+ }
66
+ });
67
+
68
+ // ---------------------------------------------------------------------------
69
+ // Tests
70
+ // ---------------------------------------------------------------------------
71
+
72
+ describe("setImageGenModel — provider derived from model prefix", () => {
73
+ test("gemini model writes provider=gemini", () => {
74
+ setImageGenModel("gemini-3.1-flash-image-preview", makeCtx());
75
+
76
+ const config = readConfig();
77
+ const imageGen = (config.services as any)?.["image-generation"];
78
+ expect(imageGen?.model).toBe("gemini-3.1-flash-image-preview");
79
+ expect(imageGen?.provider).toBe("gemini");
80
+ });
81
+
82
+ test("gpt-image-2 writes provider=openai", () => {
83
+ setImageGenModel("gpt-image-2", makeCtx());
84
+
85
+ const config = readConfig();
86
+ const imageGen = (config.services as any)?.["image-generation"];
87
+ expect(imageGen?.model).toBe("gpt-image-2");
88
+ expect(imageGen?.provider).toBe("openai");
89
+ });
90
+
91
+ test("dall-e-3 writes provider=openai", () => {
92
+ setImageGenModel("dall-e-3", makeCtx());
93
+
94
+ const config = readConfig();
95
+ const imageGen = (config.services as any)?.["image-generation"];
96
+ expect(imageGen?.model).toBe("dall-e-3");
97
+ expect(imageGen?.provider).toBe("openai");
98
+ });
99
+
100
+ test("switching from gemini to openai flips provider in place", () => {
101
+ setImageGenModel("gemini-3.1-flash-image-preview", makeCtx());
102
+ let imageGen = (readConfig().services as any)?.["image-generation"];
103
+ expect(imageGen?.provider).toBe("gemini");
104
+
105
+ setImageGenModel("gpt-image-2", makeCtx());
106
+ imageGen = (readConfig().services as any)?.["image-generation"];
107
+ expect(imageGen?.model).toBe("gpt-image-2");
108
+ expect(imageGen?.provider).toBe("openai");
109
+ });
110
+ });
@@ -85,7 +85,7 @@ describe("AssistantConfigSchema", () => {
85
85
  // llm.default.{provider,model} (see PR 19 of unify-llm-callsites).
86
86
  expect(result.services.inference.mode).toBe("your-own");
87
87
  expect(result.llm.default.provider).toBe("anthropic");
88
- expect(result.llm.default.model).toBe("claude-opus-4-7");
88
+ expect(result.llm.default.model).toBe("claude-sonnet-4-6");
89
89
  expect(result.services["image-generation"].provider).toBe("gemini");
90
90
  expect(result.services["image-generation"].model).toBe(
91
91
  "gemini-3.1-flash-image-preview",
@@ -170,7 +170,7 @@ describe("AssistantConfigSchema", () => {
170
170
  expect(result.llm).toBeDefined();
171
171
  expect(result.llm.default).toEqual({
172
172
  provider: "anthropic",
173
- model: "claude-opus-4-7",
173
+ model: "claude-sonnet-4-6",
174
174
  maxTokens: 64000,
175
175
  effort: "max",
176
176
  speed: "standard",
@@ -305,7 +305,7 @@ describe("AssistantConfigSchema", () => {
305
305
  (result.services.inference as Record<string, unknown>).model,
306
306
  ).toBeUndefined();
307
307
  expect(result.llm.default.provider).toBe("anthropic");
308
- expect(result.llm.default.model).toBe("claude-opus-4-7");
308
+ expect(result.llm.default.model).toBe("claude-sonnet-4-6");
309
309
  });
310
310
 
311
311
  test("partial llm config (empty `llm: {}`) doesn't trigger full config reset", () => {
@@ -320,7 +320,7 @@ describe("AssistantConfigSchema", () => {
320
320
  });
321
321
  expect(result.llm.default.maxTokens).toBe(32000);
322
322
  expect(result.llm.default.provider).toBe("anthropic");
323
- expect(result.llm.default.model).toBe("claude-opus-4-7");
323
+ expect(result.llm.default.model).toBe("claude-sonnet-4-6");
324
324
  });
325
325
 
326
326
  test("llm.default with one missing field still parses (defaults applied)", () => {
@@ -748,11 +748,11 @@ describe("AssistantConfigSchema", () => {
748
748
  expect(result.permissions.autoApproveUpTo).toBe("medium");
749
749
  });
750
750
 
751
- test("rejects autoApproveUpTo high", () => {
752
- const result = AssistantConfigSchema.safeParse({
751
+ test("accepts autoApproveUpTo high", () => {
752
+ const result = AssistantConfigSchema.parse({
753
753
  permissions: { autoApproveUpTo: "high" },
754
754
  });
755
- expect(result.success).toBe(false);
755
+ expect(result.permissions.autoApproveUpTo).toBe("high");
756
756
  });
757
757
 
758
758
  test("rejects invalid autoApproveUpTo string", () => {
@@ -804,12 +804,25 @@ describe("AssistantConfigSchema", () => {
804
804
  test("per-context object rejects invalid enum values", () => {
805
805
  const result = AssistantConfigSchema.safeParse({
806
806
  permissions: {
807
- autoApproveUpTo: { conversation: "high" },
807
+ autoApproveUpTo: { conversation: "extreme" },
808
808
  },
809
809
  });
810
810
  expect(result.success).toBe(false);
811
811
  });
812
812
 
813
+ test("per-context object accepts high enum value", () => {
814
+ const result = AssistantConfigSchema.parse({
815
+ permissions: {
816
+ autoApproveUpTo: { conversation: "high" },
817
+ },
818
+ });
819
+ expect(result.permissions.autoApproveUpTo).toEqual({
820
+ conversation: "high",
821
+ background: "medium",
822
+ headless: "none",
823
+ });
824
+ });
825
+
813
826
  test("per-context object round-trips through JSON serialization", () => {
814
827
  const original = AssistantConfigSchema.parse({
815
828
  permissions: {
@@ -2254,7 +2267,7 @@ describe("loadConfig with schema validation", () => {
2254
2267
  writeConfig({});
2255
2268
  const config = loadConfig();
2256
2269
  expect(config.llm.default.provider).toBe("anthropic");
2257
- expect(config.llm.default.model).toBe("claude-opus-4-7");
2270
+ expect(config.llm.default.model).toBe("claude-sonnet-4-6");
2258
2271
  expect(config.llm.default.maxTokens).toBe(64000);
2259
2272
  expect(config.llm.default.thinking).toEqual({
2260
2273
  enabled: true,
@@ -219,10 +219,6 @@ mock.module("../signals/mcp-reload.js", () => ({
219
219
  handleMcpReloadSignal: () => {},
220
220
  }));
221
221
 
222
- mock.module("../signals/shotgun.js", () => ({
223
- handleShotgunSignal: () => {},
224
- }));
225
-
226
222
  mock.module("../signals/user-message.js", () => ({
227
223
  handleUserMessageSignal: () => {},
228
224
  }));
@@ -37,6 +37,32 @@ mock.module("../runtime/auth/token-service.js", () => ({
37
37
  mintEdgeRelayToken: () => "test-token",
38
38
  }));
39
39
 
40
+ // contact-search now calls cliIpcCall instead of the gateway HTTP.
41
+ // Mock the IPC client to dispatch search_contacts to the real store
42
+ // (backed by the test DB) without needing a running IPC server.
43
+ mock.module("../ipc/cli-client.js", () => ({
44
+ cliIpcCall: async (method: string, params?: Record<string, unknown>) => {
45
+ const store = await import("../contacts/contact-store.js");
46
+ if (method === "search_contacts") {
47
+ return { ok: true, result: store.searchContacts(params ?? {}) };
48
+ }
49
+ if (method === "upsert_contact") {
50
+ return { ok: true, result: store.upsertContact(params as never) };
51
+ }
52
+ if (method === "get_contact") {
53
+ return {
54
+ ok: true,
55
+ result: store.getContact((params as { id: string }).id) ?? null,
56
+ };
57
+ }
58
+ if (method === "merge_contacts") {
59
+ const { keepId, mergeId } = params as { keepId: string; mergeId: string };
60
+ return { ok: true, result: store.mergeContacts(keepId, mergeId) };
61
+ }
62
+ return { ok: false, error: `Unknown IPC method: ${method}` };
63
+ },
64
+ }));
65
+
40
66
  import type { Database } from "bun:sqlite";
41
67
 
42
68
  import { executeContactMerge } from "../config/bundled-skills/contacts/tools/contact-merge.js";