@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,406 @@
1
+ /**
2
+ * Tests for the `circuitBreaker` plugin pipeline.
3
+ *
4
+ * The default plugin (`plugins/defaults/circuit-breaker.ts`) replaces the
5
+ * inline compaction circuit-breaker logic that previously lived in
6
+ * `daemon/conversation-agent-loop.ts`. These tests exercise the default
7
+ * plugin through the pipeline runner and assert the threshold (3 consecutive
8
+ * failures) and cooldown (1 hour) exactly match the legacy behavior.
9
+ *
10
+ * Coverage mirrors the eight scenarios the deleted
11
+ * `compaction-circuit-breaker.test.ts` exercised before the wrap:
12
+ * (a) counter increments on each failure outcome
13
+ * (b) circuit opens after exactly 3 consecutive failures
14
+ * (c) successful compaction resets counter and clears the circuit
15
+ * (d) decision.open reflects state and cooldown expiry
16
+ * (d) open circuit admits force:true (exercised at the call site; this
17
+ * file asserts decision.open is true while the breaker is tripped)
18
+ * (e) circuit re-opens after cooldown expiry when 3 more failures
19
+ * accumulate (guards the stale-timestamp regression)
20
+ * (f) callers skip tracking on undefined summaryFailed so early returns
21
+ * don't reset the counter (documented from the caller's perspective)
22
+ * (g) open→closed transition emits `compaction_circuit_closed` exactly once
23
+ * (h) closed→closed transition emits nothing
24
+ */
25
+
26
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
27
+
28
+ import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
29
+ import type { ServerMessage } from "../daemon/message-protocol.js";
30
+ import {
31
+ COMPACTION_CIRCUIT_COOLDOWN_MS,
32
+ COMPACTION_CIRCUIT_FAILURE_THRESHOLD,
33
+ defaultCircuitBreakerPlugin,
34
+ } from "../plugins/defaults/circuit-breaker.js";
35
+ import { runPipeline } from "../plugins/pipeline.js";
36
+ import {
37
+ getMiddlewaresFor,
38
+ registerPlugin,
39
+ resetPluginRegistryForTests,
40
+ } from "../plugins/registry.js";
41
+ import type {
42
+ CircuitBreakerArgs,
43
+ CircuitBreakerResult,
44
+ TurnContext,
45
+ } from "../plugins/types.js";
46
+
47
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
48
+
49
+ interface BreakerState {
50
+ readonly conversationId: string;
51
+ consecutiveCompactionFailures: number;
52
+ compactionCircuitOpenUntil: number | null;
53
+ }
54
+
55
+ function makeState(conversationId = "conv-breaker-test"): BreakerState {
56
+ return {
57
+ conversationId,
58
+ consecutiveCompactionFailures: 0,
59
+ compactionCircuitOpenUntil: null,
60
+ };
61
+ }
62
+
63
+ function collectEvents(): {
64
+ events: ServerMessage[];
65
+ onEvent: (msg: ServerMessage) => void;
66
+ } {
67
+ const events: ServerMessage[] = [];
68
+ return { events, onEvent: (msg) => events.push(msg) };
69
+ }
70
+
71
+ const trust: TrustContext = {
72
+ sourceChannel: "vellum",
73
+ trustClass: "guardian",
74
+ };
75
+
76
+ function makeTurnCtx(conversationId = "conv-breaker-test"): TurnContext {
77
+ return {
78
+ requestId: "req-test",
79
+ conversationId,
80
+ turnIndex: 0,
81
+ trust,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Run the `circuitBreaker` pipeline through the registered plugin chain.
87
+ * Mirrors how `conversation-agent-loop.ts` invokes it, with the same
88
+ * terminal fallback used in production.
89
+ */
90
+ async function runCircuit(
91
+ args: CircuitBreakerArgs,
92
+ ctx: TurnContext = makeTurnCtx(args.state.conversationId),
93
+ ): Promise<CircuitBreakerResult> {
94
+ return runPipeline<CircuitBreakerArgs, CircuitBreakerResult>(
95
+ "circuitBreaker",
96
+ getMiddlewaresFor("circuitBreaker"),
97
+ async (terminalArgs) => {
98
+ const openUntil = terminalArgs.state.compactionCircuitOpenUntil;
99
+ const now = Date.now();
100
+ if (openUntil !== null && now < openUntil) {
101
+ return { open: true, cooldownRemainingMs: openUntil - now };
102
+ }
103
+ return { open: false };
104
+ },
105
+ args,
106
+ ctx,
107
+ 500,
108
+ );
109
+ }
110
+
111
+ describe("circuit-breaker pipeline", () => {
112
+ let originalDateNow: () => number;
113
+
114
+ beforeEach(() => {
115
+ resetPluginRegistryForTests();
116
+ registerPlugin(defaultCircuitBreakerPlugin);
117
+ originalDateNow = Date.now;
118
+ });
119
+
120
+ afterEach(() => {
121
+ Date.now = originalDateNow;
122
+ });
123
+
124
+ test("threshold and cooldown match legacy constants exactly", () => {
125
+ // Sanity — the plugin must expose the same constants the legacy inline
126
+ // helpers used. Any drift would silently change user-visible behavior.
127
+ expect(COMPACTION_CIRCUIT_FAILURE_THRESHOLD).toBe(3);
128
+ expect(COMPACTION_CIRCUIT_COOLDOWN_MS).toBe(60 * 60 * 1000);
129
+ });
130
+
131
+ test("(a) counter increments on each failure outcome", async () => {
132
+ const state = makeState();
133
+ const { onEvent, events } = collectEvents();
134
+
135
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
136
+ expect(state.consecutiveCompactionFailures).toBe(1);
137
+ expect(state.compactionCircuitOpenUntil).toBeNull();
138
+ expect(events).toHaveLength(0);
139
+
140
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
141
+ expect(state.consecutiveCompactionFailures).toBe(2);
142
+ expect(state.compactionCircuitOpenUntil).toBeNull();
143
+ expect(events).toHaveLength(0);
144
+ });
145
+
146
+ test("(b) circuit opens after exactly 3 consecutive failures", async () => {
147
+ const fixedNow = 1_700_000_000_000;
148
+ Date.now = () => fixedNow;
149
+
150
+ const state = makeState();
151
+ const { onEvent, events } = collectEvents();
152
+
153
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
154
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
155
+ // Two failures — circuit still closed.
156
+ expect(state.compactionCircuitOpenUntil).toBeNull();
157
+ expect(events).toHaveLength(0);
158
+
159
+ const third = await runCircuit({
160
+ key: "k",
161
+ outcome: "failure",
162
+ state,
163
+ onEvent,
164
+ });
165
+ // Third failure — circuit trips and fires the event exactly once.
166
+ expect(state.consecutiveCompactionFailures).toBe(3);
167
+ expect(state.compactionCircuitOpenUntil).toBe(fixedNow + 60 * 60 * 1000);
168
+ expect(third.open).toBe(true);
169
+ expect(third.cooldownRemainingMs).toBe(60 * 60 * 1000);
170
+ expect(events).toHaveLength(1);
171
+ expect(events[0]).toEqual({
172
+ type: "compaction_circuit_open",
173
+ conversationId: state.conversationId,
174
+ reason: "3_consecutive_failures",
175
+ openUntil: fixedNow + 60 * 60 * 1000,
176
+ });
177
+
178
+ // Further failures do not re-fire the event while the circuit is open.
179
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
180
+ expect(state.consecutiveCompactionFailures).toBe(4);
181
+ expect(events).toHaveLength(1);
182
+ });
183
+
184
+ test("(c) successful outcome resets counter and clears circuit", async () => {
185
+ const fixedNow = 1_700_000_000_000;
186
+ Date.now = () => fixedNow;
187
+
188
+ const state = makeState();
189
+ const { onEvent } = collectEvents();
190
+
191
+ // Trip the breaker.
192
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
193
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
194
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
195
+ expect(state.compactionCircuitOpenUntil).not.toBeNull();
196
+
197
+ // Success resets state.
198
+ await runCircuit({ key: "k", outcome: "success", state, onEvent });
199
+ expect(state.consecutiveCompactionFailures).toBe(0);
200
+ expect(state.compactionCircuitOpenUntil).toBeNull();
201
+ });
202
+
203
+ test("(d) decision.open reflects state and expiry", async () => {
204
+ const fixedNow = 1_700_000_000_000;
205
+ Date.now = () => fixedNow;
206
+
207
+ const state = makeState();
208
+ const { onEvent } = collectEvents();
209
+
210
+ // Query-only on a fresh state: closed, no cooldown.
211
+ const preQuery = await runCircuit({ key: "k", state, onEvent });
212
+ expect(preQuery.open).toBe(false);
213
+ expect(preQuery.cooldownRemainingMs).toBeUndefined();
214
+
215
+ // Trip the breaker.
216
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
217
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
218
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
219
+
220
+ // Query-only while open: open + non-zero cooldown.
221
+ const openQuery = await runCircuit({ key: "k", state, onEvent });
222
+ expect(openQuery.open).toBe(true);
223
+ expect(openQuery.cooldownRemainingMs).toBe(60 * 60 * 1000);
224
+
225
+ // After cooldown expires the breaker reports closed again, even without
226
+ // an explicit reset — the open-until timestamp is the only source of
227
+ // truth for the gate.
228
+ Date.now = () => fixedNow + 60 * 60 * 1000 + 1;
229
+ const postCooldown = await runCircuit({ key: "k", state, onEvent });
230
+ expect(postCooldown.open).toBe(false);
231
+ expect(postCooldown.cooldownRemainingMs).toBeUndefined();
232
+ });
233
+
234
+ test("(e) circuit re-opens after cooldown expiry when 3 more failures accumulate", async () => {
235
+ // Regression: before the fix in the legacy helper, opening the breaker a
236
+ // second time required `compactionCircuitOpenUntil === null`. Once a
237
+ // cooldown expired, the decision correctly reported "closed" but the
238
+ // stale past-timestamp stayed on the state, so the next 3-strike window
239
+ // couldn't trip a new cooldown. The default plugin must treat any
240
+ // expired timestamp the same as null.
241
+ const t0 = 1_700_000_000_000;
242
+ Date.now = () => t0;
243
+
244
+ const state = makeState();
245
+ const { onEvent, events } = collectEvents();
246
+
247
+ // Trip the breaker the first time.
248
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
249
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
250
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
251
+ expect(state.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
252
+ expect(events).toHaveLength(1);
253
+
254
+ // Advance past the cooldown window. Manually reset the counter — in
255
+ // production this happens when a subsequent `maybeCompact` call succeeds
256
+ // (outcome: "success") after the cooldown elapses, but the bug
257
+ // manifests even when the counter is reset: the stale
258
+ // `compactionCircuitOpenUntil` is what breaks re-opening.
259
+ const t1 = t0 + 60 * 60 * 1000 + 1;
260
+ Date.now = () => t1;
261
+ const postCooldown = await runCircuit({ key: "k", state, onEvent });
262
+ expect(postCooldown.open).toBe(false);
263
+ state.consecutiveCompactionFailures = 0;
264
+ // `compactionCircuitOpenUntil` is deliberately left as the old
265
+ // timestamp to reproduce the bug condition.
266
+ expect(state.compactionCircuitOpenUntil).toBe(t0 + 60 * 60 * 1000);
267
+
268
+ // Three more failures must trip a fresh cooldown even though the old
269
+ // timestamp is still set.
270
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
271
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
272
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
273
+ expect(state.consecutiveCompactionFailures).toBe(3);
274
+ expect(state.compactionCircuitOpenUntil).toBe(t1 + 60 * 60 * 1000);
275
+ expect(events).toHaveLength(2);
276
+ expect(events[1]).toEqual({
277
+ type: "compaction_circuit_open",
278
+ conversationId: state.conversationId,
279
+ reason: "3_consecutive_failures",
280
+ openUntil: t1 + 60 * 60 * 1000,
281
+ });
282
+ });
283
+
284
+ test("(f) callers must skip tracking on undefined summaryFailed so early returns don't reset the counter", async () => {
285
+ // Regression: `maybeCompact()` returns `summaryFailed: undefined` on
286
+ // early-return paths (no eligible messages, below threshold, cooldown
287
+ // active, truncation-only). Callers guard with `summaryFailed !==
288
+ // undefined` at every call site — this test asserts that a query-only
289
+ // pipeline invocation (no `outcome`) preserves the counter.
290
+ const state = makeState();
291
+ const { onEvent } = collectEvents();
292
+
293
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
294
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
295
+ expect(state.consecutiveCompactionFailures).toBe(2);
296
+
297
+ // Query-only — should NOT touch the counter.
298
+ await runCircuit({ key: "k", state, onEvent });
299
+ expect(state.consecutiveCompactionFailures).toBe(2);
300
+
301
+ // A third real failure then trips the breaker as expected.
302
+ await runCircuit({ key: "k", outcome: "failure", state, onEvent });
303
+ expect(state.consecutiveCompactionFailures).toBe(3);
304
+ expect(state.compactionCircuitOpenUntil).not.toBeNull();
305
+ });
306
+
307
+ test("(g) open→closed transition emits compaction_circuit_closed exactly once", async () => {
308
+ // Regression: before this fix in the legacy helper, the reset branch
309
+ // silently cleared `compactionCircuitOpenUntil` without notifying the
310
+ // client. The Swift banner set from `compaction_circuit_open` would
311
+ // stay visible until the original `openUntil` deadline (up to 1h),
312
+ // misrepresenting the live state. The default plugin emits
313
+ // `compaction_circuit_closed` on the open→closed transition so the
314
+ // banner dismisses immediately.
315
+ const fixedNow = 1_700_000_000_000;
316
+ Date.now = () => fixedNow;
317
+
318
+ const state = makeState();
319
+ const { onEvent, events } = collectEvents();
320
+
321
+ // Force the circuit into the open state directly — the emitted-event
322
+ // transition logic is what we're testing, not the tripping path.
323
+ state.compactionCircuitOpenUntil = fixedNow + 60 * 60 * 1000;
324
+ state.consecutiveCompactionFailures = 3;
325
+
326
+ await runCircuit({ key: "k", outcome: "success", state, onEvent });
327
+
328
+ expect(state.consecutiveCompactionFailures).toBe(0);
329
+ expect(state.compactionCircuitOpenUntil).toBeNull();
330
+ expect(events).toHaveLength(1);
331
+ expect(events[0]).toEqual({
332
+ type: "compaction_circuit_closed",
333
+ conversationId: state.conversationId,
334
+ });
335
+ });
336
+
337
+ test("(h) successful outcome against an already-closed circuit emits no event", async () => {
338
+ // Emitting `compaction_circuit_closed` on every successful compaction
339
+ // would spam the client (the breaker is closed in the common case).
340
+ // Only the open→closed transition is meaningful.
341
+ const state = makeState();
342
+ const { onEvent, events } = collectEvents();
343
+
344
+ expect(state.compactionCircuitOpenUntil).toBeNull();
345
+ await runCircuit({ key: "k", outcome: "success", state, onEvent });
346
+ expect(state.compactionCircuitOpenUntil).toBeNull();
347
+ expect(events).toHaveLength(0);
348
+
349
+ // A second successful outcome while still closed — still no event.
350
+ await runCircuit({ key: "k", outcome: "success", state, onEvent });
351
+ expect(events).toHaveLength(0);
352
+ });
353
+
354
+ test("omitting onEvent still updates state without emitting", async () => {
355
+ // `onEvent` is optional in `CircuitBreakerArgs`. When omitted the plugin
356
+ // must still mutate the state container correctly — the only missing
357
+ // side effect is the transition notification.
358
+ const state = makeState();
359
+
360
+ for (let i = 0; i < 3; i++) {
361
+ await runCircuit({ key: "k", outcome: "failure", state });
362
+ }
363
+ expect(state.consecutiveCompactionFailures).toBe(3);
364
+ expect(state.compactionCircuitOpenUntil).not.toBeNull();
365
+
366
+ await runCircuit({ key: "k", outcome: "success", state });
367
+ expect(state.consecutiveCompactionFailures).toBe(0);
368
+ expect(state.compactionCircuitOpenUntil).toBeNull();
369
+ });
370
+
371
+ test("pipeline runner applies registered middleware in registration order", async () => {
372
+ // A second plugin registered after the default can observe args/result
373
+ // around the default's behavior. This proves the pipeline composes both
374
+ // middlewares rather than short-circuiting on the default alone.
375
+ const seen: string[] = [];
376
+ registerPlugin({
377
+ manifest: {
378
+ name: "observer",
379
+ version: "0.0.1",
380
+ requires: { pluginRuntime: "v1", circuitBreakerApi: "v1" },
381
+ },
382
+ middleware: {
383
+ circuitBreaker: async (args, next) => {
384
+ seen.push(`before:${args.outcome ?? "query"}`);
385
+ const res = await next(args);
386
+ seen.push(`after:${res.open ? "open" : "closed"}`);
387
+ return res;
388
+ },
389
+ },
390
+ });
391
+
392
+ const state = makeState();
393
+ await runCircuit({ key: "k", outcome: "failure", state });
394
+ await runCircuit({ key: "k", outcome: "failure", state });
395
+ await runCircuit({ key: "k", outcome: "failure", state });
396
+
397
+ expect(seen).toEqual([
398
+ "before:failure",
399
+ "after:closed",
400
+ "before:failure",
401
+ "after:closed",
402
+ "before:failure",
403
+ "after:open",
404
+ ]);
405
+ });
406
+ });