@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,309 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { Conversation } from "../../../../daemon/conversation.js";
4
+ import type { RouteDefinition } from "../../../http-router.js";
5
+ import type { PlaygroundRouteDeps } from "../deps.js";
6
+ import { PLAYGROUND_TITLE_PREFIX } from "../seed-conversation.js";
7
+ import { seededConversationsRouteDefinitions } from "../seeded-conversations.js";
8
+
9
+ interface StubOpts {
10
+ enabled?: boolean;
11
+ listRows?: Array<{
12
+ id: string;
13
+ title: string;
14
+ messageCount: number;
15
+ createdAt: number;
16
+ }>;
17
+ getConversationById?: (id: string) => Promise<Conversation | undefined>;
18
+ deleteReturn?: boolean | ((id: string) => boolean);
19
+ }
20
+
21
+ interface Stub {
22
+ deps: PlaygroundRouteDeps;
23
+ listCalls: string[];
24
+ deleteCalls: string[];
25
+ }
26
+
27
+ function makeStub(opts: StubOpts = {}): Stub {
28
+ const listCalls: string[] = [];
29
+ const deleteCalls: string[] = [];
30
+ const deleteReturn = opts.deleteReturn ?? true;
31
+ const deps: PlaygroundRouteDeps = {
32
+ isPlaygroundEnabled: () => opts.enabled ?? true,
33
+ getConversationById: opts.getConversationById ?? (async () => undefined),
34
+ listConversationsByTitlePrefix: (prefix) => {
35
+ listCalls.push(prefix);
36
+ return opts.listRows ?? [];
37
+ },
38
+ deleteConversationById: (id) => {
39
+ deleteCalls.push(id);
40
+ return typeof deleteReturn === "function"
41
+ ? deleteReturn(id)
42
+ : deleteReturn;
43
+ },
44
+ createConversation: async () => ({ id: "conv-test" }),
45
+ addMessage: async () => ({ id: "msg-test" }),
46
+ };
47
+ return { deps, listCalls, deleteCalls };
48
+ }
49
+
50
+ function findRoute(
51
+ routes: RouteDefinition[],
52
+ method: string,
53
+ endpoint: string,
54
+ ): RouteDefinition {
55
+ const match = routes.find(
56
+ (r) => r.method === method && r.endpoint === endpoint,
57
+ );
58
+ if (!match) {
59
+ throw new Error(`Expected route ${method} ${endpoint} not found`);
60
+ }
61
+ return match;
62
+ }
63
+
64
+ // Minimal stand-in for the handler context — the seeded-conversation
65
+ // handlers only read `params`, so we only populate that.
66
+ function ctx(params: Record<string, string> = {}) {
67
+ return { params } as unknown as Parameters<RouteDefinition["handler"]>[0];
68
+ }
69
+
70
+ describe("seededConversationsRouteDefinitions — flag disabled", () => {
71
+ test("GET list returns 404 with playground_disabled code when the playground flag is off", async () => {
72
+ const { deps, listCalls } = makeStub({ enabled: false });
73
+ const routes = seededConversationsRouteDefinitions(deps);
74
+ const route = findRoute(routes, "GET", "playground/seeded-conversations");
75
+
76
+ const res = await route.handler(ctx());
77
+
78
+ expect(res.status).toBe(404);
79
+ const body = (await res.json()) as { error: { code: string } };
80
+ // Distinct from `conversation_not_found` so the Swift client can
81
+ // surface the right toast text without sniffing the URL path.
82
+ expect(body.error.code).toBe("playground_disabled");
83
+ expect(listCalls).toEqual([]);
84
+ });
85
+
86
+ test("DELETE single returns 404 with playground_disabled code when the playground flag is off", async () => {
87
+ const { deps, deleteCalls } = makeStub({ enabled: false });
88
+ const routes = seededConversationsRouteDefinitions(deps);
89
+ const route = findRoute(
90
+ routes,
91
+ "DELETE",
92
+ "playground/seeded-conversations/:id",
93
+ );
94
+
95
+ const res = await route.handler(ctx({ id: "conv-1" }));
96
+
97
+ expect(res.status).toBe(404);
98
+ const body = (await res.json()) as { error: { code: string } };
99
+ expect(body.error.code).toBe("playground_disabled");
100
+ expect(deleteCalls).toEqual([]);
101
+ });
102
+
103
+ test("DELETE bulk returns 404 with playground_disabled code when the playground flag is off", async () => {
104
+ const { deps, listCalls, deleteCalls } = makeStub({ enabled: false });
105
+ const routes = seededConversationsRouteDefinitions(deps);
106
+ const route = findRoute(
107
+ routes,
108
+ "DELETE",
109
+ "playground/seeded-conversations",
110
+ );
111
+
112
+ const res = await route.handler(ctx());
113
+
114
+ expect(res.status).toBe(404);
115
+ const body = (await res.json()) as { error: { code: string } };
116
+ expect(body.error.code).toBe("playground_disabled");
117
+ expect(listCalls).toEqual([]);
118
+ expect(deleteCalls).toEqual([]);
119
+ });
120
+ });
121
+
122
+ describe("GET playground/seeded-conversations", () => {
123
+ test("forwards the prefix to the deps helper and returns the rows verbatim", async () => {
124
+ const rows = [
125
+ {
126
+ id: "conv-1",
127
+ title: `${PLAYGROUND_TITLE_PREFIX}First`,
128
+ messageCount: 4,
129
+ createdAt: 2000,
130
+ },
131
+ {
132
+ id: "conv-2",
133
+ title: `${PLAYGROUND_TITLE_PREFIX}Second`,
134
+ messageCount: 2,
135
+ createdAt: 1000,
136
+ },
137
+ ];
138
+ const { deps, listCalls } = makeStub({ listRows: rows });
139
+ const routes = seededConversationsRouteDefinitions(deps);
140
+ const route = findRoute(routes, "GET", "playground/seeded-conversations");
141
+
142
+ const res = await route.handler(ctx());
143
+ expect(res.status).toBe(200);
144
+
145
+ const body = (await res.json()) as {
146
+ conversations: typeof rows;
147
+ };
148
+ expect(body.conversations).toEqual(rows);
149
+ expect(listCalls).toEqual([PLAYGROUND_TITLE_PREFIX]);
150
+ });
151
+ });
152
+
153
+ describe("DELETE playground/seeded-conversations/:id", () => {
154
+ test("returns 403 when the conversation is not in the prefix-filtered set", async () => {
155
+ // The list call (authoritative prefix check) returns nothing for this id.
156
+ const { deps, deleteCalls } = makeStub({
157
+ listRows: [
158
+ {
159
+ id: "other-playground-id",
160
+ title: `${PLAYGROUND_TITLE_PREFIX}Kept`,
161
+ messageCount: 1,
162
+ createdAt: 1,
163
+ },
164
+ ],
165
+ });
166
+ const routes = seededConversationsRouteDefinitions(deps);
167
+ const route = findRoute(
168
+ routes,
169
+ "DELETE",
170
+ "playground/seeded-conversations/:id",
171
+ );
172
+
173
+ const res = await route.handler(ctx({ id: "non-playground-conv" }));
174
+
175
+ expect(res.status).toBe(403);
176
+ const body = (await res.json()) as {
177
+ error: { code: string; message: string };
178
+ };
179
+ expect(body.error.code).toBe("FORBIDDEN");
180
+ expect(body.error.message).toBe("Not a playground conversation");
181
+ expect(deleteCalls).toEqual([]);
182
+ });
183
+
184
+ test("returns 200 and deletes when the id is a prefix-matching conversation", async () => {
185
+ const { deps, deleteCalls } = makeStub({
186
+ listRows: [
187
+ {
188
+ id: "conv-seeded",
189
+ title: `${PLAYGROUND_TITLE_PREFIX}Seeded`,
190
+ messageCount: 3,
191
+ createdAt: 5,
192
+ },
193
+ ],
194
+ });
195
+ const routes = seededConversationsRouteDefinitions(deps);
196
+ const route = findRoute(
197
+ routes,
198
+ "DELETE",
199
+ "playground/seeded-conversations/:id",
200
+ );
201
+
202
+ const res = await route.handler(ctx({ id: "conv-seeded" }));
203
+
204
+ expect(res.status).toBe(200);
205
+ const body = (await res.json()) as { deletedCount: number };
206
+ expect(body.deletedCount).toBe(1);
207
+ expect(deleteCalls).toEqual(["conv-seeded"]);
208
+ });
209
+
210
+ test("returns deletedCount: 0 when deleteConversationById reports a miss", async () => {
211
+ const { deps, deleteCalls } = makeStub({
212
+ listRows: [
213
+ {
214
+ id: "conv-seeded",
215
+ title: `${PLAYGROUND_TITLE_PREFIX}Seeded`,
216
+ messageCount: 0,
217
+ createdAt: 5,
218
+ },
219
+ ],
220
+ deleteReturn: false,
221
+ });
222
+ const routes = seededConversationsRouteDefinitions(deps);
223
+ const route = findRoute(
224
+ routes,
225
+ "DELETE",
226
+ "playground/seeded-conversations/:id",
227
+ );
228
+
229
+ const res = await route.handler(ctx({ id: "conv-seeded" }));
230
+
231
+ expect(res.status).toBe(200);
232
+ const body = (await res.json()) as { deletedCount: number };
233
+ expect(body.deletedCount).toBe(0);
234
+ expect(deleteCalls).toEqual(["conv-seeded"]);
235
+ });
236
+ });
237
+
238
+ describe("DELETE playground/seeded-conversations (bulk)", () => {
239
+ test("enumerates only prefix-matching rows and calls delete for each", async () => {
240
+ const rows = [
241
+ {
242
+ id: "conv-a",
243
+ title: `${PLAYGROUND_TITLE_PREFIX}A`,
244
+ messageCount: 0,
245
+ createdAt: 3,
246
+ },
247
+ {
248
+ id: "conv-b",
249
+ title: `${PLAYGROUND_TITLE_PREFIX}B`,
250
+ messageCount: 2,
251
+ createdAt: 2,
252
+ },
253
+ {
254
+ id: "conv-c",
255
+ title: `${PLAYGROUND_TITLE_PREFIX}C`,
256
+ messageCount: 5,
257
+ createdAt: 1,
258
+ },
259
+ ];
260
+ const { deps, listCalls, deleteCalls } = makeStub({ listRows: rows });
261
+ const routes = seededConversationsRouteDefinitions(deps);
262
+ const route = findRoute(
263
+ routes,
264
+ "DELETE",
265
+ "playground/seeded-conversations",
266
+ );
267
+
268
+ const res = await route.handler(ctx());
269
+
270
+ expect(res.status).toBe(200);
271
+ const body = (await res.json()) as { deletedCount: number };
272
+ expect(body.deletedCount).toBe(3);
273
+ expect(listCalls).toEqual([PLAYGROUND_TITLE_PREFIX]);
274
+ expect(deleteCalls).toEqual(["conv-a", "conv-b", "conv-c"]);
275
+ });
276
+
277
+ test("deletedCount reflects only rows where the underlying delete succeeded", async () => {
278
+ const rows = [
279
+ {
280
+ id: "conv-ok",
281
+ title: `${PLAYGROUND_TITLE_PREFIX}Ok`,
282
+ messageCount: 1,
283
+ createdAt: 2,
284
+ },
285
+ {
286
+ id: "conv-missing",
287
+ title: `${PLAYGROUND_TITLE_PREFIX}Missing`,
288
+ messageCount: 0,
289
+ createdAt: 1,
290
+ },
291
+ ];
292
+ const { deps, deleteCalls } = makeStub({
293
+ listRows: rows,
294
+ deleteReturn: (id) => id !== "conv-missing",
295
+ });
296
+ const routes = seededConversationsRouteDefinitions(deps);
297
+ const route = findRoute(
298
+ routes,
299
+ "DELETE",
300
+ "playground/seeded-conversations",
301
+ );
302
+
303
+ const res = await route.handler(ctx());
304
+ expect(res.status).toBe(200);
305
+ const body = (await res.json()) as { deletedCount: number };
306
+ expect(body.deletedCount).toBe(1);
307
+ expect(deleteCalls).toEqual(["conv-ok", "conv-missing"]);
308
+ });
309
+ });
@@ -0,0 +1,224 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../../../../config/loader.js", () => ({
4
+ getConfig: () => ({
5
+ llm: {
6
+ default: {
7
+ contextWindow: {
8
+ enabled: true,
9
+ maxInputTokens: 200_000,
10
+ compactThreshold: 0.8,
11
+ },
12
+ },
13
+ },
14
+ }),
15
+ }));
16
+
17
+ // estimatePromptTokens has no external dependencies beyond its `messages`
18
+ // argument, but we mock it so the assertions here do not depend on the
19
+ // estimator's internal tuning.
20
+ mock.module("../../../../context/token-estimator.js", () => ({
21
+ estimatePromptTokens: (messages: unknown[]): number => messages.length * 10,
22
+ }));
23
+
24
+ import type { Conversation } from "../../../../daemon/conversation.js";
25
+ import type { PlaygroundRouteDeps } from "../deps.js";
26
+ import { playgroundRouteDefinitions } from "../index.js";
27
+ import { buildCompactionStateResponse } from "../state.js";
28
+
29
+ function makeDeps(
30
+ overrides: Partial<PlaygroundRouteDeps> = {},
31
+ ): PlaygroundRouteDeps {
32
+ return {
33
+ getConversationById: async () => undefined,
34
+ isPlaygroundEnabled: () => true,
35
+ listConversationsByTitlePrefix: () => [],
36
+ deleteConversationById: () => false,
37
+ createConversation: async () => ({ id: "conv-test" }),
38
+ addMessage: async () => ({ id: "msg-test" }),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ interface FakeConversationOverrides {
44
+ messages?: unknown[];
45
+ contextCompactedMessageCount?: number;
46
+ contextCompactedAt?: number | null;
47
+ consecutiveCompactionFailures?: number;
48
+ compactionCircuitOpenUntil?: number | null;
49
+ }
50
+
51
+ function makeFakeConversation(
52
+ overrides: FakeConversationOverrides = {},
53
+ ): Conversation {
54
+ const messages = overrides.messages ?? [];
55
+ return {
56
+ getMessages: () => messages,
57
+ contextCompactedMessageCount: overrides.contextCompactedMessageCount ?? 0,
58
+ contextCompactedAt: overrides.contextCompactedAt ?? null,
59
+ consecutiveCompactionFailures: overrides.consecutiveCompactionFailures ?? 0,
60
+ compactionCircuitOpenUntil: overrides.compactionCircuitOpenUntil ?? null,
61
+ } as unknown as Conversation;
62
+ }
63
+
64
+ function findStateRoute() {
65
+ const routes = playgroundRouteDefinitions(makeDeps());
66
+ const route = routes.find(
67
+ (r) =>
68
+ r.endpoint === "conversations/:id/playground/compaction-state" &&
69
+ r.method === "GET",
70
+ );
71
+ if (!route) throw new Error("compaction-state route not registered");
72
+ return route;
73
+ }
74
+
75
+ async function invokeRoute(
76
+ deps: PlaygroundRouteDeps,
77
+ id = "conv-abc",
78
+ ): Promise<Response> {
79
+ const routes = playgroundRouteDefinitions(deps);
80
+ const route = routes.find(
81
+ (r) =>
82
+ r.endpoint === "conversations/:id/playground/compaction-state" &&
83
+ r.method === "GET",
84
+ );
85
+ if (!route) throw new Error("compaction-state route not registered");
86
+ // The handler only reads `params` from RouteContext — cast a minimal stub.
87
+ return Promise.resolve(
88
+ route.handler({
89
+ params: { id },
90
+ } as unknown as Parameters<typeof route.handler>[0]),
91
+ );
92
+ }
93
+
94
+ describe("GET conversations/:id/playground/compaction-state", () => {
95
+ test("registers the expected route definition", () => {
96
+ const route = findStateRoute();
97
+ expect(route.policyKey).toBe("conversations/playground/state");
98
+ expect(route.tags).toEqual(["playground"]);
99
+ });
100
+
101
+ test("returns 404 with playground_disabled code when the playground flag is disabled", async () => {
102
+ const deps = makeDeps({ isPlaygroundEnabled: () => false });
103
+ const res = await invokeRoute(deps);
104
+ expect(res.status).toBe(404);
105
+ const body = (await res.json()) as { error: { code: string } };
106
+ // Distinct from `conversation_not_found` so the Swift client can
107
+ // surface the right toast text without sniffing the URL path.
108
+ expect(body.error.code).toBe("playground_disabled");
109
+ });
110
+
111
+ test("returns 404 with conversation_not_found code when the conversation does not exist", async () => {
112
+ const deps = makeDeps({
113
+ getConversationById: async () => undefined,
114
+ });
115
+ const res = await invokeRoute(deps, "missing-id");
116
+ expect(res.status).toBe(404);
117
+ const body = (await res.json()) as {
118
+ error: { code: string; message: string };
119
+ };
120
+ expect(body.error.code).toBe("conversation_not_found");
121
+ expect(body.error.message).toContain("missing-id");
122
+ });
123
+
124
+ test("fresh conversation with no messages returns a baseline payload", async () => {
125
+ const conversation = makeFakeConversation();
126
+ const deps = makeDeps({
127
+ getConversationById: async () => conversation,
128
+ });
129
+ const res = await invokeRoute(deps);
130
+ expect(res.status).toBe(200);
131
+ const body = (await res.json()) as ReturnType<
132
+ typeof buildCompactionStateResponse
133
+ >;
134
+ expect(body.messageCount).toBe(0);
135
+ expect(body.estimatedInputTokens).toBe(0);
136
+ expect(body.maxInputTokens).toBe(200_000);
137
+ expect(body.compactThresholdRatio).toBe(0.8);
138
+ expect(body.thresholdTokens).toBe(160_000);
139
+ expect(body.contextCompactedMessageCount).toBe(0);
140
+ expect(body.contextCompactedAt).toBeNull();
141
+ expect(body.consecutiveCompactionFailures).toBe(0);
142
+ expect(body.compactionCircuitOpenUntil).toBeNull();
143
+ expect(body.isCircuitOpen).toBe(false);
144
+ expect(body.isCompactionEnabled).toBe(true);
145
+ });
146
+
147
+ test("open circuit breaker sets isCircuitOpen: true", async () => {
148
+ const future = Date.now() + 5_000;
149
+ const conversation = makeFakeConversation({
150
+ compactionCircuitOpenUntil: future,
151
+ consecutiveCompactionFailures: 3,
152
+ });
153
+ const deps = makeDeps({
154
+ getConversationById: async () => conversation,
155
+ });
156
+ const res = await invokeRoute(deps);
157
+ expect(res.status).toBe(200);
158
+ const body = (await res.json()) as ReturnType<
159
+ typeof buildCompactionStateResponse
160
+ >;
161
+ expect(body.compactionCircuitOpenUntil).toBe(future);
162
+ expect(body.consecutiveCompactionFailures).toBe(3);
163
+ expect(body.isCircuitOpen).toBe(true);
164
+ });
165
+
166
+ test("elapsed circuit-breaker deadline leaves isCircuitOpen: false", async () => {
167
+ const past = Date.now() - 1_000;
168
+ const conversation = makeFakeConversation({
169
+ compactionCircuitOpenUntil: past,
170
+ });
171
+ const deps = makeDeps({
172
+ getConversationById: async () => conversation,
173
+ });
174
+ const res = await invokeRoute(deps);
175
+ const body = (await res.json()) as ReturnType<
176
+ typeof buildCompactionStateResponse
177
+ >;
178
+ expect(body.compactionCircuitOpenUntil).toBe(past);
179
+ expect(body.isCircuitOpen).toBe(false);
180
+ });
181
+
182
+ test("full response shape matches the canonical CompactionStateResponse keys", async () => {
183
+ const conversation = makeFakeConversation({
184
+ messages: [{ role: "user" }, { role: "assistant" }],
185
+ contextCompactedMessageCount: 2,
186
+ contextCompactedAt: 1_700_000_000_000,
187
+ consecutiveCompactionFailures: 1,
188
+ compactionCircuitOpenUntil: null,
189
+ });
190
+ const deps = makeDeps({
191
+ getConversationById: async () => conversation,
192
+ });
193
+ const res = await invokeRoute(deps);
194
+ const body = (await res.json()) as Record<string, unknown>;
195
+ expect(Object.keys(body).sort()).toEqual(
196
+ [
197
+ "estimatedInputTokens",
198
+ "maxInputTokens",
199
+ "compactThresholdRatio",
200
+ "thresholdTokens",
201
+ "messageCount",
202
+ "contextCompactedMessageCount",
203
+ "contextCompactedAt",
204
+ "consecutiveCompactionFailures",
205
+ "compactionCircuitOpenUntil",
206
+ "isCircuitOpen",
207
+ "isCompactionEnabled",
208
+ ].sort(),
209
+ );
210
+ expect(body.messageCount).toBe(2);
211
+ expect(body.estimatedInputTokens).toBe(20);
212
+ expect(body.contextCompactedAt).toBe(1_700_000_000_000);
213
+ expect(body.contextCompactedMessageCount).toBe(2);
214
+ });
215
+ });
216
+
217
+ describe("buildCompactionStateResponse", () => {
218
+ test("is exported for reuse by PR 7 / PR 8 consolidations", () => {
219
+ const conversation = makeFakeConversation();
220
+ const snapshot = buildCompactionStateResponse(conversation);
221
+ expect(typeof snapshot.estimatedInputTokens).toBe("number");
222
+ expect(typeof snapshot.isCircuitOpen).toBe("boolean");
223
+ });
224
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Body code for "conversation lookup returned no row" 404s on
3
+ * conversation-scoped playground routes. Distinct from the generic
4
+ * `NOT_FOUND` code (and from `playground_disabled`, see `guard.ts`) so
5
+ * the Swift `CompactionPlaygroundClient` can pick `.notFound` over
6
+ * `.notAvailable` from the response body rather than from a URL-path
7
+ * heuristic. Without this distinction the `assertPlaygroundEnabled`
8
+ * guard's flag-off 404 (which fires *before* the conversation lookup on
9
+ * every conv-scoped route) would be indistinguishable from a real
10
+ * missing-conversation 404.
11
+ */
12
+ export const CONVERSATION_NOT_FOUND_CODE = "conversation_not_found";
13
+
14
+ /**
15
+ * Build a 404 response for a missing conversation on a playground route.
16
+ * Uses a distinguishing body `code` so the Swift client can route this
17
+ * to `.notFound` (rather than `.notAvailable`).
18
+ */
19
+ export function conversationNotFoundResponse(conversationId: string): Response {
20
+ return Response.json(
21
+ {
22
+ error: {
23
+ code: CONVERSATION_NOT_FOUND_CODE,
24
+ message: `Conversation ${conversationId} not found`,
25
+ },
26
+ },
27
+ { status: 404 },
28
+ );
29
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Shared dependency contract for every `/v1/.../playground/*` route.
3
+ *
4
+ * Each playground route file accepts a `PlaygroundRouteDeps` and calls
5
+ * `assertPlaygroundEnabled(deps)` before doing any real work, so the route
6
+ * group is invisible in production regardless of UI gating.
7
+ *
8
+ * Later PRs in the compaction-playground plan (PR 6, PR 16, ...) extend this
9
+ * interface with additional capabilities. The scaffold keeps the surface
10
+ * intentionally minimal.
11
+ */
12
+
13
+ import type { Conversation } from "../../../daemon/conversation.js";
14
+
15
+ export interface PlaygroundRouteDeps {
16
+ /**
17
+ * Resolve a conversation by ID for conv-scoped playground routes
18
+ * (`forceCompact`, `injectFailures`, `resetCircuit`, `getState`). Async
19
+ * because production wiring may need to hydrate a `Conversation` from the
20
+ * DB on demand — freshly-seeded rows live only in the DB until the daemon
21
+ * loads them. Implementations must return `undefined` for IDs that do not
22
+ * exist in the DB at all so route handlers can preserve their 404 path.
23
+ */
24
+ readonly getConversationById: (
25
+ id: string,
26
+ ) => Promise<Conversation | undefined>;
27
+ readonly isPlaygroundEnabled: () => boolean;
28
+ /**
29
+ * List non-archived conversations whose title starts with `prefix`. Used by
30
+ * the seeded-conversation endpoints (GET list + bulk DELETE) to enumerate
31
+ * the playground-owned set without exposing every conversation.
32
+ */
33
+ readonly listConversationsByTitlePrefix: (prefix: string) => Array<{
34
+ id: string;
35
+ title: string;
36
+ messageCount: number;
37
+ createdAt: number;
38
+ }>;
39
+ /**
40
+ * Delete a conversation by ID. Returns `true` when a row was deleted, or
41
+ * `false` if no conversation with that ID exists. Kept narrow (no
42
+ * memory/vector cleanup surface) so route handlers don't accidentally
43
+ * skip the async cleanup the daemon handles elsewhere; the playground
44
+ * delete path is intentionally best-effort for freshly-seeded rows.
45
+ */
46
+ readonly deleteConversationById: (id: string) => boolean;
47
+ readonly createConversation: (title: string) => Promise<{ id: string }>;
48
+ readonly addMessage: (
49
+ conversationId: string,
50
+ role: "user" | "assistant",
51
+ contentJson: string,
52
+ options?: { skipIndexing?: boolean },
53
+ ) => Promise<{ id: string }>;
54
+ // Later PRs will extend this interface with additional capabilities.
55
+ // Keep this list minimal.
56
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * POST /v1/conversations/:id/playground/compact
3
+ *
4
+ * Force-compact a conversation (dev-only playground). Wraps
5
+ * `Conversation.forceCompact()` and returns the pre/post prompt-token
6
+ * estimates plus the summary metadata so the playground UI can display
7
+ * the delta.
8
+ *
9
+ * Guarded by `assertPlaygroundEnabled` — returns 404 when the
10
+ * `compaction-playground` feature flag is off.
11
+ */
12
+
13
+ import { estimatePromptTokens } from "../../../context/token-estimator.js";
14
+ import { httpError } from "../../http-errors.js";
15
+ import type { RouteDefinition } from "../../http-router.js";
16
+ import { conversationNotFoundResponse } from "./conversation-not-found.js";
17
+ import { assertPlaygroundEnabled, type PlaygroundRouteDeps } from "./index.js";
18
+
19
+ export function forceCompactRouteDefinitions(
20
+ deps: PlaygroundRouteDeps,
21
+ ): RouteDefinition[] {
22
+ return [
23
+ {
24
+ endpoint: "conversations/:id/playground/compact",
25
+ method: "POST",
26
+ policyKey: "conversations/playground/compact",
27
+ summary: "Force compaction on a conversation (dev-only playground)",
28
+ tags: ["playground"],
29
+ handler: async ({ params }) => {
30
+ const gate = assertPlaygroundEnabled(deps);
31
+ if (gate) return gate;
32
+
33
+ const conversation = await deps.getConversationById(params.id);
34
+ if (!conversation) {
35
+ return conversationNotFoundResponse(params.id);
36
+ }
37
+
38
+ // Per-conversation in-flight guard. `Conversation.processing` is set
39
+ // to `true` whenever an agent turn or a slash-`/compact` is mid-flight
40
+ // (see `conversation-routes.ts` and `Conversation.persistUserMessage`).
41
+ // If we ran a second `forceCompact()` against the same conversation
42
+ // while one was already in progress, we would race and double up
43
+ // `contextCompactedMessageCount`, emit duplicate `context_compacted`
44
+ // SSE events, and double-record usage. Easy to trigger by
45
+ // double-clicking the playground "Force Compact" button. Fail fast
46
+ // with 409 — the playground is a debug tool and clobbering legitimate
47
+ // in-flight processing is worse than a brief retryable error.
48
+ if (conversation.processing) {
49
+ return httpError(
50
+ "CONFLICT",
51
+ "Compaction already in progress for this conversation",
52
+ 409,
53
+ );
54
+ }
55
+
56
+ const messagesBefore = conversation.getMessages();
57
+ const previousTokens = estimatePromptTokens(messagesBefore);
58
+ const result = await conversation.forceCompact();
59
+ const messagesAfter = conversation.getMessages();
60
+ const newTokens = estimatePromptTokens(messagesAfter);
61
+
62
+ return Response.json({
63
+ compacted: result.compacted,
64
+ previousTokens,
65
+ newTokens,
66
+ summaryText: result.summaryText ?? null,
67
+ messagesRemoved: result.compactedPersistedMessages ?? 0,
68
+ summaryFailed: result.summaryFailed ?? null,
69
+ });
70
+ },
71
+ },
72
+ ];
73
+ }