@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,501 @@
1
+ /**
2
+ * Tests for compaction event emission.
3
+ *
4
+ * Verifies that forceCompact() emits a `context_compacted` event (carrying
5
+ * the fresh `estimatedInputTokens` / `maxInputTokens`) after a successful
6
+ * compaction, so the UI indicator refreshes without waiting for the next full
7
+ * turn. The `context_compacted` event is the single source of truth for the
8
+ * indicator — the paired `usage_update` intentionally omits
9
+ * `contextWindowTokens` to avoid a redundant SwiftUI invalidation.
10
+ */
11
+ import { describe, expect, mock, test } from "bun:test";
12
+
13
+ import type {
14
+ AgentEvent,
15
+ AgentLoopConfig,
16
+ CheckpointDecision,
17
+ CheckpointInfo,
18
+ } from "../agent/loop.js";
19
+ import type { ContextWindowResult } from "../context/window-manager.js";
20
+ import type { ServerMessage } from "../daemon/message-protocol.js";
21
+ import type { Message, ProviderResponse } from "../providers/types.js";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Mocks — must precede Conversation import
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function makeLoggerStub(): Record<string, unknown> {
28
+ const stub: Record<string, unknown> = {};
29
+ for (const m of [
30
+ "info",
31
+ "warn",
32
+ "error",
33
+ "debug",
34
+ "trace",
35
+ "fatal",
36
+ "silent",
37
+ "child",
38
+ ]) {
39
+ stub[m] = m === "child" ? () => makeLoggerStub() : () => {};
40
+ }
41
+ return stub;
42
+ }
43
+
44
+ mock.module("../util/logger.js", () => ({
45
+ getLogger: () => makeLoggerStub(),
46
+ }));
47
+
48
+ mock.module("../memory/guardian-action-store.js", () => ({
49
+ getGuardianActionRequest: () => null,
50
+ resolveGuardianActionRequest: () => {},
51
+ }));
52
+
53
+ mock.module("../providers/registry.js", () => ({
54
+ getProvider: () => ({ name: "mock-provider" }),
55
+ initializeProviders: () => {},
56
+ }));
57
+
58
+ mock.module("../config/loader.js", () => ({
59
+ getConfig: () => ({
60
+ ui: {},
61
+ llm: {
62
+ default: {
63
+ provider: "mock-provider",
64
+ model: "mock-model",
65
+ maxTokens: 4096,
66
+ effort: "high" as const,
67
+ speed: "standard",
68
+ temperature: null,
69
+ thinking: { enabled: false, streamThinking: true },
70
+ contextWindow: {
71
+ enabled: true,
72
+ maxInputTokens: 100000,
73
+ targetBudgetRatio: 0.3,
74
+ compactThreshold: 0.8,
75
+ summaryBudgetRatio: 0.05,
76
+ overflowRecovery: {
77
+ enabled: true,
78
+ safetyMarginRatio: 0.05,
79
+ maxAttempts: 3,
80
+ interactiveLatestTurnCompression: "summarize",
81
+ nonInteractiveLatestTurnCompression: "truncate",
82
+ },
83
+ },
84
+ },
85
+ profiles: {},
86
+ callSites: {},
87
+ pricingOverrides: [],
88
+ },
89
+ rateLimit: { maxRequestsPerMinute: 0 },
90
+ timeouts: { permissionTimeoutSec: 1 },
91
+ skills: { entries: {}, allowBundled: true },
92
+ permissions: { mode: "workspace" },
93
+ }),
94
+ loadRawConfig: () => ({}),
95
+ saveRawConfig: () => {},
96
+ invalidateConfigCache: () => {},
97
+ }));
98
+
99
+ mock.module("../config/assistant-feature-flags.js", () => ({
100
+ isAssistantFeatureFlagEnabled: () => true,
101
+ }));
102
+
103
+ mock.module("../prompts/system-prompt.js", () => ({
104
+ buildSystemPrompt: () => "system prompt",
105
+ }));
106
+
107
+ mock.module("../config/skills.js", () => ({
108
+ loadSkillCatalog: () => [],
109
+ loadSkillBySelector: () => ({ skill: null }),
110
+ ensureSkillIcon: async () => null,
111
+ }));
112
+
113
+ mock.module("../config/skill-state.js", () => ({
114
+ resolveSkillStates: () => [],
115
+ }));
116
+
117
+ mock.module("../permissions/trust-store.js", () => ({
118
+ addRule: () => {},
119
+ findHighestPriorityRule: () => null,
120
+ clearCache: () => {},
121
+ }));
122
+
123
+ mock.module("../security/secret-allowlist.js", () => ({
124
+ resetAllowlist: () => {},
125
+ }));
126
+
127
+ mock.module("../memory/conversation-crud.js", () => ({
128
+ getConversationType: () => "default",
129
+ setConversationOriginChannelIfUnset: () => {},
130
+ updateConversationContextWindow: () => {},
131
+ deleteMessageById: () => {},
132
+ provenanceFromTrustContext: () => ({
133
+ source: "user",
134
+ trustContext: undefined,
135
+ }),
136
+ getConversationOriginInterface: () => null,
137
+ getConversationOriginChannel: () => null,
138
+ getMessages: () => [],
139
+ getConversation: () => ({
140
+ id: "conv-1",
141
+ contextSummary: null,
142
+ contextCompactedMessageCount: 0,
143
+ totalInputTokens: 0,
144
+ totalOutputTokens: 0,
145
+ totalEstimatedCost: 0,
146
+ }),
147
+ createConversation: () => ({ id: "conv-1" }),
148
+ addMessage: () => ({ id: `msg-${Date.now()}` }),
149
+ updateConversationUsage: () => {},
150
+ updateConversationTitle: () => {},
151
+ }));
152
+
153
+ mock.module("../memory/conversation-queries.js", () => ({
154
+ listConversations: () => [],
155
+ }));
156
+
157
+ mock.module("../memory/attachments-store.js", () => ({
158
+ uploadAttachment: () => ({ id: `att-${Date.now()}` }),
159
+ linkAttachmentToMessage: () => {},
160
+ }));
161
+
162
+ mock.module("../memory/retriever.js", () => ({
163
+ buildMemoryRecall: async () => ({
164
+ enabled: false,
165
+ degraded: false,
166
+ injectedText: "",
167
+ semanticHits: 0,
168
+ injectedTokens: 0,
169
+ latencyMs: 0,
170
+ }),
171
+ injectMemoryRecallAsUserBlock: (msgs: Message[]) => msgs,
172
+ }));
173
+
174
+ // Per-test compaction result — set before calling forceCompact().
175
+ let mockCompactResult: ContextWindowResult = {
176
+ messages: [],
177
+ compacted: false,
178
+ previousEstimatedInputTokens: 0,
179
+ estimatedInputTokens: 0,
180
+ maxInputTokens: 0,
181
+ thresholdTokens: 0,
182
+ compactedMessages: 0,
183
+ compactedPersistedMessages: 0,
184
+ summaryCalls: 0,
185
+ summaryInputTokens: 0,
186
+ summaryOutputTokens: 0,
187
+ summaryModel: "",
188
+ summaryText: "",
189
+ };
190
+
191
+ mock.module("../context/window-manager.js", () => ({
192
+ ContextWindowManager: class {
193
+ nonPersistedPrefixCount = 0;
194
+ summaryIsInjected = false;
195
+ constructor() {}
196
+ shouldCompact() {
197
+ return { needed: false, estimatedTokens: 0 };
198
+ }
199
+ async maybeCompact(): Promise<ContextWindowResult> {
200
+ return mockCompactResult;
201
+ }
202
+ },
203
+ createContextSummaryMessage: () => ({
204
+ role: "user",
205
+ content: [{ type: "text", text: "summary" }],
206
+ }),
207
+ getSummaryFromContextMessage: () => null,
208
+ }));
209
+
210
+ mock.module("../memory/llm-usage-store.js", () => ({
211
+ recordUsageEvent: () => ({ id: "mock-id", createdAt: Date.now() }),
212
+ listUsageEvents: () => [],
213
+ }));
214
+
215
+ mock.module("../memory/auto-analysis-enqueue.js", () => ({
216
+ enqueueAutoAnalysisOnCompaction: () => {},
217
+ }));
218
+
219
+ mock.module("../agent/loop.js", () => ({
220
+ AgentLoop: class {
221
+ constructor(
222
+ _provider: unknown,
223
+ _systemPrompt: string,
224
+ _config?: Partial<AgentLoopConfig>,
225
+ ) {}
226
+ getToolTokenBudget() {
227
+ return 0;
228
+ }
229
+ getResolvedTools() {
230
+ return [];
231
+ }
232
+ getActiveModel() {
233
+ return undefined;
234
+ }
235
+ async run(
236
+ _messages: Message[],
237
+ _onEvent: (event: AgentEvent) => void,
238
+ _signal?: AbortSignal,
239
+ _requestId?: string,
240
+ _onCheckpoint?: (
241
+ checkpoint: CheckpointInfo,
242
+ ) => CheckpointDecision | Promise<CheckpointDecision>,
243
+ ): Promise<Message[]> {
244
+ return [];
245
+ }
246
+ },
247
+ }));
248
+
249
+ mock.module("../memory/canonical-guardian-store.js", () => ({
250
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
251
+ listCanonicalGuardianRequests: () => [],
252
+ listPendingRequestsByConversationScope: () => [],
253
+ createCanonicalGuardianRequest: () => ({
254
+ id: "mock-cg-id",
255
+ code: "MOCK",
256
+ status: "pending",
257
+ }),
258
+ getCanonicalGuardianRequest: () => null,
259
+ getCanonicalGuardianRequestByCode: () => null,
260
+ updateCanonicalGuardianRequest: () => {},
261
+ resolveCanonicalGuardianRequest: () => {},
262
+ createCanonicalGuardianDelivery: () => ({ id: "mock-cgd-id" }),
263
+ listCanonicalGuardianDeliveries: () => [],
264
+ listPendingCanonicalGuardianRequestsByDestinationChat: () => [],
265
+ updateCanonicalGuardianDelivery: () => {},
266
+ generateCanonicalRequestCode: () => "MOCK-CODE",
267
+ }));
268
+
269
+ // ---------------------------------------------------------------------------
270
+ // Import Conversation AFTER mocks
271
+ // ---------------------------------------------------------------------------
272
+
273
+ import { Conversation } from "../daemon/conversation.js";
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Helpers
277
+ // ---------------------------------------------------------------------------
278
+
279
+ function makeProvider() {
280
+ return {
281
+ name: "mock-provider",
282
+ async sendMessage(): Promise<ProviderResponse> {
283
+ return {
284
+ content: [],
285
+ model: "mock",
286
+ usage: { inputTokens: 0, outputTokens: 0 },
287
+ stopReason: "end_turn",
288
+ };
289
+ },
290
+ };
291
+ }
292
+
293
+ function makeConversation(
294
+ collected: ServerMessage[],
295
+ id = "conv-compact-events",
296
+ ): Conversation {
297
+ return new Conversation(
298
+ id,
299
+ makeProvider(),
300
+ "system prompt",
301
+ 4096,
302
+ (msg) => {
303
+ collected.push(msg);
304
+ },
305
+ "/tmp",
306
+ );
307
+ }
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Tests
311
+ // ---------------------------------------------------------------------------
312
+
313
+ describe("forceCompact event emission", () => {
314
+ test("emits context_compacted and a usage_update without contextWindow when compacted", async () => {
315
+ const collected: ServerMessage[] = [];
316
+ mockCompactResult = {
317
+ messages: [],
318
+ compacted: true,
319
+ previousEstimatedInputTokens: 150_000,
320
+ estimatedInputTokens: 80_000,
321
+ maxInputTokens: 200_000,
322
+ thresholdTokens: 160_000,
323
+ compactedMessages: 10,
324
+ compactedPersistedMessages: 5,
325
+ summaryCalls: 1,
326
+ summaryInputTokens: 500,
327
+ summaryOutputTokens: 200,
328
+ summaryModel: "test-model",
329
+ summaryText: "summary text",
330
+ };
331
+
332
+ const conversation = makeConversation(collected);
333
+ await conversation.forceCompact();
334
+
335
+ const compactedEvents = collected.filter(
336
+ (m) => m.type === "context_compacted",
337
+ );
338
+ expect(compactedEvents.length).toBe(1);
339
+ const compactedEvent = compactedEvents[0] as Extract<
340
+ ServerMessage,
341
+ { type: "context_compacted" }
342
+ >;
343
+ expect(compactedEvent.conversationId).toBe("conv-compact-events");
344
+ expect(compactedEvent.estimatedInputTokens).toBe(80_000);
345
+ expect(compactedEvent.maxInputTokens).toBe(200_000);
346
+ expect(compactedEvent.previousEstimatedInputTokens).toBe(150_000);
347
+ expect(compactedEvent.summaryCalls).toBe(1);
348
+ expect(compactedEvent.summaryInputTokens).toBe(500);
349
+ expect(compactedEvent.summaryOutputTokens).toBe(200);
350
+ expect(compactedEvent.summaryModel).toBe("test-model");
351
+ // Quality signals derived from the summary text itself.
352
+ expect(compactedEvent.summaryCharCount).toBe("summary text".length);
353
+ expect(compactedEvent.summaryHeaderCount).toBe(0);
354
+ expect(compactedEvent.summaryHadMemoryEcho).toBe(false);
355
+
356
+ const usageEvents = collected.filter((m) => m.type === "usage_update");
357
+ expect(usageEvents.length).toBe(1);
358
+ const usageEvent = usageEvents[0] as Extract<
359
+ ServerMessage,
360
+ { type: "usage_update" }
361
+ >;
362
+ // `context_compacted` is now the single source of truth for the UI
363
+ // indicator after compaction; the paired `usage_update` intentionally
364
+ // omits contextWindow to avoid a redundant SwiftUI invalidation.
365
+ expect(usageEvent.contextWindowTokens).toBeUndefined();
366
+ expect(usageEvent.contextWindowMaxTokens).toBeUndefined();
367
+ expect(usageEvent.inputTokens).toBe(500);
368
+ expect(usageEvent.outputTokens).toBe(200);
369
+ expect(usageEvent.model).toBe("test-model");
370
+ });
371
+
372
+ test("emits context_compacted even when summary LLM was skipped (truncation-only path)", async () => {
373
+ const collected: ServerMessage[] = [];
374
+ mockCompactResult = {
375
+ messages: [],
376
+ compacted: true,
377
+ previousEstimatedInputTokens: 150_000,
378
+ estimatedInputTokens: 80_000,
379
+ maxInputTokens: 200_000,
380
+ thresholdTokens: 160_000,
381
+ compactedMessages: 10,
382
+ compactedPersistedMessages: 5,
383
+ summaryCalls: 0,
384
+ summaryInputTokens: 0,
385
+ summaryOutputTokens: 0,
386
+ summaryModel: "",
387
+ summaryText: "",
388
+ };
389
+
390
+ const conversation = makeConversation(collected, "conv-compact-trunc");
391
+ await conversation.forceCompact();
392
+
393
+ // The truncation-only path does not call the summary LLM, so
394
+ // `recordUsage` early-returns on 0/0 tokens and no `usage_update` is
395
+ // emitted. The client instead picks up the fresh context-window tokens
396
+ // from the `context_compacted` event, which carries the post-compaction
397
+ // `estimatedInputTokens` and `maxInputTokens` alongside `conversationId`.
398
+ const compactedEvents = collected.filter(
399
+ (m) => m.type === "context_compacted",
400
+ );
401
+ expect(compactedEvents.length).toBe(1);
402
+ const compactedEvent = compactedEvents[0] as Extract<
403
+ ServerMessage,
404
+ { type: "context_compacted" }
405
+ >;
406
+ expect(compactedEvent.conversationId).toBe("conv-compact-trunc");
407
+ expect(compactedEvent.estimatedInputTokens).toBe(80_000);
408
+ expect(compactedEvent.maxInputTokens).toBe(200_000);
409
+
410
+ // No usage_update synthesis in the truncation-only path (the previous
411
+ // synthetic fallback was removed now that context_compacted carries
412
+ // conversationId and refreshes the indicator on the client).
413
+ expect(collected.filter((m) => m.type === "usage_update").length).toBe(0);
414
+ });
415
+
416
+ test("skips emission when compacted is false", async () => {
417
+ const collected: ServerMessage[] = [];
418
+ mockCompactResult = {
419
+ messages: [],
420
+ compacted: false,
421
+ previousEstimatedInputTokens: 0,
422
+ estimatedInputTokens: 0,
423
+ maxInputTokens: 0,
424
+ thresholdTokens: 0,
425
+ compactedMessages: 0,
426
+ compactedPersistedMessages: 0,
427
+ summaryCalls: 0,
428
+ summaryInputTokens: 0,
429
+ summaryOutputTokens: 0,
430
+ summaryModel: "",
431
+ summaryText: "",
432
+ };
433
+
434
+ const conversation = makeConversation(collected, "conv-compact-noop");
435
+ await conversation.forceCompact();
436
+
437
+ expect(collected.filter((m) => m.type === "context_compacted").length).toBe(
438
+ 0,
439
+ );
440
+ expect(collected.filter((m) => m.type === "usage_update").length).toBe(0);
441
+ });
442
+ });
443
+
444
+ // ---------------------------------------------------------------------------
445
+ // computeSummaryQualitySignals — imported lazily after mocks are installed so
446
+ // the logger stub and other module replacements take effect first.
447
+ // ---------------------------------------------------------------------------
448
+
449
+ import { computeSummaryQualitySignals } from "../daemon/conversation-agent-loop.js";
450
+
451
+ describe("computeSummaryQualitySignals", () => {
452
+ test("counts `## ` headers at the start of lines", () => {
453
+ const summary =
454
+ "Narrative opener.\n\n## What We're Working On\n- x\n\n## Open Threads\n- y";
455
+ const signals = computeSummaryQualitySignals(summary);
456
+ expect(signals.headerCount).toBe(2);
457
+ expect(signals.charCount).toBe(summary.length);
458
+ expect(signals.hadMemoryEcho).toBe(false);
459
+ });
460
+
461
+ test("reports empty signals for an empty summary", () => {
462
+ const signals = computeSummaryQualitySignals("");
463
+ expect(signals.charCount).toBe(0);
464
+ expect(signals.headerCount).toBe(0);
465
+ expect(signals.hadMemoryEcho).toBe(false);
466
+ });
467
+
468
+ test("flags summaries that leaked injection tags", () => {
469
+ const leaked =
470
+ "## Facts\nThe user had a `<memory __injected>` block in their history";
471
+ expect(computeSummaryQualitySignals(leaked).hadMemoryEcho).toBe(true);
472
+
473
+ const turnCtxLeak = "A <turn_context> fragment snuck through";
474
+ expect(computeSummaryQualitySignals(turnCtxLeak).hadMemoryEcho).toBe(true);
475
+
476
+ const nowLeak = "<NOW.md> scratchpad echo";
477
+ expect(computeSummaryQualitySignals(nowLeak).hadMemoryEcho).toBe(true);
478
+ });
479
+
480
+ test("flags tags that sit next to an underscore (word-boundary gap)", () => {
481
+ // These four tags contain underscores, so `\b` only asserts between the
482
+ // full tag name and `>` (not between two word characters like the `e_` in
483
+ // `workspace_top_level`). Each tag must be detected as a memory echo when
484
+ // leaked into a summary.
485
+ const cases = [
486
+ "<workspace_top_level>\nlisting",
487
+ "<active_subagents>\nstuff",
488
+ "<active_workspace>\nstuff",
489
+ "<active_dynamic_page>\nstuff",
490
+ ];
491
+ for (const leaked of cases) {
492
+ expect(computeSummaryQualitySignals(leaked).hadMemoryEcho).toBe(true);
493
+ }
494
+ });
495
+
496
+ test("does not flag ordinary mentions of the word 'memory'", () => {
497
+ const clean =
498
+ "## Facts\nThe user asked about their memory and remembered their dad's recipe.";
499
+ expect(computeSummaryQualitySignals(clean).hadMemoryEcho).toBe(false);
500
+ });
501
+ });
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Tests for the `compaction` plugin pipeline (PR 25).
3
+ *
4
+ * Covers:
5
+ * - Default plugin delegates to the manager's `maybeCompact` and returns the
6
+ * same `ContextWindowResult` object the manager produced.
7
+ * - A custom plugin layered on top can short-circuit before the terminal is
8
+ * reached and return a different summary, demonstrating that the pipeline
9
+ * slot is observable and replaceable without patching the manager.
10
+ *
11
+ * The tests drive `runPipeline` directly rather than going through the full
12
+ * orchestrator — the integration path (conversation-agent-loop) is exercised
13
+ * by `conversation-agent-loop-overflow.test.ts`, which must continue to pass
14
+ * as the acceptance criterion for this PR.
15
+ */
16
+
17
+ import { describe, expect, test } from "bun:test";
18
+
19
+ import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
20
+ import {
21
+ DEFAULT_COMPACTION_PLUGIN_NAME,
22
+ defaultCompactionTerminal,
23
+ } from "../plugins/defaults/compaction.js";
24
+ import { runPipeline } from "../plugins/pipeline.js";
25
+ import {
26
+ type CompactionArgs,
27
+ type CompactionResult,
28
+ type Middleware,
29
+ PluginExecutionError,
30
+ type TurnContext,
31
+ } from "../plugins/types.js";
32
+
33
+ type ContextWindowResultShape = {
34
+ compacted: boolean;
35
+ summaryText: string;
36
+ messages: unknown[];
37
+ previousEstimatedInputTokens: number;
38
+ estimatedInputTokens: number;
39
+ maxInputTokens: number;
40
+ thresholdTokens: number;
41
+ compactedMessages: number;
42
+ compactedPersistedMessages: number;
43
+ summaryCalls: number;
44
+ summaryInputTokens: number;
45
+ summaryOutputTokens: number;
46
+ summaryModel: string;
47
+ reason?: string;
48
+ };
49
+
50
+ const trust: TrustContext = {
51
+ sourceChannel: "vellum",
52
+ trustClass: "guardian",
53
+ };
54
+
55
+ function makeTurnCtx(manager: {
56
+ maybeCompact: (...args: unknown[]) => Promise<unknown>;
57
+ }): TurnContext {
58
+ return {
59
+ requestId: "req-compaction-test",
60
+ conversationId: "conv-compaction-test",
61
+ turnIndex: 0,
62
+ trust,
63
+ // `TurnContext.contextWindowManager` is a typed optional field; the
64
+ // default compaction plugin reads it directly without a cast.
65
+ contextWindowManager:
66
+ manager as unknown as TurnContext["contextWindowManager"],
67
+ };
68
+ }
69
+
70
+ function makeResult(
71
+ overrides: Partial<ContextWindowResultShape> = {},
72
+ ): ContextWindowResultShape {
73
+ return {
74
+ compacted: true,
75
+ summaryText: "default-summary",
76
+ messages: [],
77
+ previousEstimatedInputTokens: 1000,
78
+ estimatedInputTokens: 100,
79
+ maxInputTokens: 100000,
80
+ thresholdTokens: 80000,
81
+ compactedMessages: 3,
82
+ compactedPersistedMessages: 3,
83
+ summaryCalls: 1,
84
+ summaryInputTokens: 500,
85
+ summaryOutputTokens: 120,
86
+ summaryModel: "default-model",
87
+ ...overrides,
88
+ };
89
+ }
90
+
91
+ describe("compaction pipeline", () => {
92
+ test("default plugin delegates to the manager and returns its result unchanged", async () => {
93
+ const observed: {
94
+ messages: unknown;
95
+ signal: unknown;
96
+ options: unknown;
97
+ }[] = [];
98
+ const expected = makeResult({
99
+ summaryText: "manager-summary",
100
+ compactedMessages: 7,
101
+ });
102
+ const manager = {
103
+ maybeCompact: async (
104
+ messages: unknown,
105
+ signal: unknown,
106
+ options: unknown,
107
+ ) => {
108
+ observed.push({ messages, signal, options });
109
+ return expected;
110
+ },
111
+ };
112
+ const turnCtx = makeTurnCtx(manager);
113
+ const args: CompactionArgs = {
114
+ messages: [{ role: "user", content: "hi" }],
115
+ signal: new AbortController().signal,
116
+ options: { lastCompactedAt: 42, precomputedEstimate: 1234 },
117
+ };
118
+
119
+ // No middleware registered — the runner invokes the terminal directly.
120
+ const result = (await runPipeline<CompactionArgs, CompactionResult>(
121
+ "compaction",
122
+ [],
123
+ (innerArgs) => defaultCompactionTerminal(innerArgs, turnCtx),
124
+ args,
125
+ turnCtx,
126
+ 30000,
127
+ )) as ContextWindowResultShape;
128
+
129
+ // Terminal forwarded args verbatim to the manager — except for
130
+ // `signal`, which the pipeline runner replaces with a signal linked
131
+ // to its internal timeout controller. The linked signal must forward
132
+ // caller-originated aborts, which is verified in the dedicated
133
+ // pipeline-runner abort-propagation tests.
134
+ expect(observed).toHaveLength(1);
135
+ expect(observed[0]!.messages).toBe(args.messages);
136
+ expect(observed[0]!.signal).toBeInstanceOf(AbortSignal);
137
+ expect(observed[0]!.options).toBe(args.options);
138
+
139
+ // Returned result is the manager's object, unmodified — no wrapping
140
+ // or shape transformation is allowed in the default path.
141
+ expect(result).toBe(expected);
142
+ expect(result.summaryText).toBe("manager-summary");
143
+ expect(result.compactedMessages).toBe(7);
144
+ });
145
+
146
+ test("custom plugin short-circuits to a different summary without touching the manager", async () => {
147
+ let managerCallCount = 0;
148
+ const manager = {
149
+ maybeCompact: async () => {
150
+ managerCallCount++;
151
+ return makeResult({ summaryText: "should-not-run" });
152
+ },
153
+ };
154
+ const turnCtx = makeTurnCtx(manager);
155
+
156
+ const custom: Middleware<CompactionArgs, CompactionResult> =
157
+ async function customCompaction(_args, _next, _ctx) {
158
+ // Short-circuit — omit the `next` call so the terminal never fires.
159
+ return makeResult({
160
+ summaryText: "custom-plugin-summary",
161
+ compactedMessages: 0,
162
+ summaryCalls: 0,
163
+ reason: "short-circuited by custom plugin",
164
+ });
165
+ };
166
+
167
+ const args: CompactionArgs = {
168
+ messages: [],
169
+ signal: undefined,
170
+ options: undefined,
171
+ };
172
+
173
+ const result = (await runPipeline<CompactionArgs, CompactionResult>(
174
+ "compaction",
175
+ [custom],
176
+ (innerArgs) => defaultCompactionTerminal(innerArgs, turnCtx),
177
+ args,
178
+ turnCtx,
179
+ 30000,
180
+ )) as ContextWindowResultShape;
181
+
182
+ expect(managerCallCount).toBe(0);
183
+ expect(result.summaryText).toBe("custom-plugin-summary");
184
+ expect(result.reason).toBe("short-circuited by custom plugin");
185
+ });
186
+
187
+ test("default terminal surfaces a PluginExecutionError when the manager is missing", async () => {
188
+ // Build a turn context without the extension field so the default
189
+ // terminal's lenient read fails — this guards against a future refactor
190
+ // that removes the handle-attach helper in the orchestrator.
191
+ const turnCtxWithoutManager: TurnContext = {
192
+ requestId: "req-missing",
193
+ conversationId: "conv-missing",
194
+ turnIndex: 0,
195
+ trust,
196
+ };
197
+ const args: CompactionArgs = {
198
+ messages: [],
199
+ signal: undefined,
200
+ options: undefined,
201
+ };
202
+
203
+ await expect(
204
+ defaultCompactionTerminal(args, turnCtxWithoutManager),
205
+ ).rejects.toThrow(PluginExecutionError);
206
+ await expect(
207
+ defaultCompactionTerminal(args, turnCtxWithoutManager),
208
+ ).rejects.toThrow(DEFAULT_COMPACTION_PLUGIN_NAME);
209
+ });
210
+ });