@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
@@ -6,6 +6,7 @@ import type {
6
6
  CheckpointInfo,
7
7
  } from "../agent/loop.js";
8
8
  import type { ServerMessage } from "../daemon/message-protocol.js";
9
+ import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
9
10
  import type { ContentBlock, Message } from "../providers/types.js";
10
11
 
11
12
  // ── Module mocks (must precede imports of the module under test) ─────
@@ -57,10 +58,19 @@ mock.module("../config/loader.js", () => ({
57
58
  // ── Overflow recovery mocks ──────────────────────────────────────────
58
59
 
59
60
  // Token estimator returns a small value by default (well within budget)
60
- // so preflight does not trigger unless the test overrides it.
61
+ // so preflight does not trigger unless the test overrides it. Both the
62
+ // calibrated entry point (`estimatePromptTokens`, used in the convergence
63
+ // path) and the raw entry point (`estimatePromptTokensRaw`, used by the
64
+ // default `tokenEstimate` plugin pipeline for preflight/mid-loop) are
65
+ // stubbed so either call site can drive the test.
61
66
  let mockEstimateTokens = 1000;
62
67
  mock.module("../context/token-estimator.js", () => ({
63
68
  estimatePromptTokens: () => mockEstimateTokens,
69
+ estimatePromptTokensRaw: () => mockEstimateTokens,
70
+ // Pass-through: the default plugin computes `toolTokenBudget` via this
71
+ // helper before delegating to the raw estimator. Return 0 so the mocked
72
+ // raw estimate is not perturbed.
73
+ estimateToolsTokens: () => 0,
64
74
  }));
65
75
 
66
76
  // Reducer: by default returns the input untouched and marks exhausted
@@ -103,31 +113,10 @@ mock.module("../daemon/context-overflow-policy.js", () => ({
103
113
  resolveOverflowAction: () => mockOverflowAction,
104
114
  }));
105
115
 
106
- // Approval: default to denied
107
- let mockApprovalResult = { approved: false };
108
- mock.module("../daemon/context-overflow-approval.js", () => ({
109
- requestCompressionApproval: async () => mockApprovalResult,
110
- CONTEXT_OVERFLOW_TOOL_NAME: "context_overflow_compression",
111
- }));
112
-
113
- let hookBlocked = false;
114
- let hookBlockedBy = "";
115
-
116
- mock.module("../hooks/manager.js", () => ({
117
- getHookManager: () => ({
118
- trigger: async (hookName: string) => {
119
- if (hookName === "pre-message" && hookBlocked) {
120
- return { blocked: true, blockedBy: hookBlockedBy };
121
- }
122
- return { blocked: false };
123
- },
124
- }),
125
- }));
126
-
127
116
  const updateMessageMetadataMock = mock(
128
117
  (_id: string, _updates: Record<string, unknown>) => {},
129
118
  );
130
- const clearPkbSystemReminderMetadataForConversationMock = mock(
119
+ const clearStrippedInjectionMetadataForConversationMock = mock(
131
120
  (_conversationId: string) => {},
132
121
  );
133
122
  mock.module("../memory/conversation-crud.js", () => ({
@@ -135,8 +124,8 @@ mock.module("../memory/conversation-crud.js", () => ({
135
124
  setConversationOriginChannelIfUnset: () => {},
136
125
  updateConversationUsage: () => {},
137
126
  updateMessageMetadata: updateMessageMetadataMock,
138
- clearPkbSystemReminderMetadataForConversation:
139
- clearPkbSystemReminderMetadataForConversationMock,
127
+ clearStrippedInjectionMetadataForConversation:
128
+ clearStrippedInjectionMetadataForConversationMock,
140
129
  getMessages: () => [],
141
130
  getConversation: () => ({
142
131
  id: "conv-1",
@@ -374,7 +363,9 @@ type AgentLoopRun = (
374
363
  onEvent: (event: AgentEvent) => void,
375
364
  signal?: AbortSignal,
376
365
  requestId?: string,
377
- onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
366
+ onCheckpoint?: (
367
+ checkpoint: CheckpointInfo,
368
+ ) => CheckpointDecision | Promise<CheckpointDecision>,
378
369
  ) => Promise<Message[]>;
379
370
 
380
371
  function makeCtx(
@@ -404,6 +395,7 @@ function makeCtx(
404
395
  agentLoop: {
405
396
  run: agentLoopRun,
406
397
  getToolTokenBudget: () => 0,
398
+ getResolvedTools: () => [],
407
399
  // Tests here don't exercise calibration; returning undefined makes
408
400
  // the estimator use the per-provider aggregate key.
409
401
  getActiveModel: () => undefined,
@@ -509,12 +501,9 @@ function makeCtx(
509
501
  // ── Tests ────────────────────────────────────────────────────────────
510
502
 
511
503
  beforeEach(() => {
512
- hookBlocked = false;
513
- hookBlockedBy = "";
514
504
  mockEstimateTokens = 1000;
515
505
  mockReducerStepFn = null;
516
506
  mockOverflowAction = "fail_gracefully";
517
- mockApprovalResult = { approved: false };
518
507
  mockInjectionBlocks = {};
519
508
  recordUsageMock.mockClear();
520
509
  recordRequestLogMock.mockClear();
@@ -522,11 +511,17 @@ beforeEach(() => {
522
511
  rebuildConversationDiskViewFromDbStateMock.mockClear();
523
512
  updateMessageMetadataMock.mockClear();
524
513
  updateMessageMetadataMock.mockImplementation(() => {});
525
- clearPkbSystemReminderMetadataForConversationMock.mockClear();
526
- clearPkbSystemReminderMetadataForConversationMock.mockImplementation(
514
+ clearStrippedInjectionMetadataForConversationMock.mockClear();
515
+ clearStrippedInjectionMetadataForConversationMock.mockImplementation(
527
516
  () => {},
528
517
  );
529
518
  applyRuntimeInjectionsMock.mockClear();
519
+ // Orchestrator pipelines (overflowReduce, persistence, …) run through the
520
+ // plugin registry; reset and re-register every default so the pipelines
521
+ // dispatch to middleware backed by the mocked collaborators these tests
522
+ // install (`reduceContextOverflow`, `syncMessageToDisk`, etc.) instead of
523
+ // hitting the bare terminals.
524
+ resetPluginRegistryAndRegisterDefaults();
530
525
  });
531
526
 
532
527
  describe("session-agent-loop", () => {
@@ -540,47 +535,6 @@ describe("session-agent-loop", () => {
540
535
  });
541
536
  });
542
537
 
543
- describe("pre-message hook blocking", () => {
544
- test("emits error and returns early when pre-message hook blocks", async () => {
545
- hookBlocked = true;
546
- hookBlockedBy = "test-hook";
547
- const events: ServerMessage[] = [];
548
- const ctx = makeCtx();
549
-
550
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
551
-
552
- const errorEvent = events.find((e) => e.type === "error");
553
- expect(errorEvent).toBeDefined();
554
- expect((errorEvent as { message: string }).message).toContain(
555
- "test-hook",
556
- );
557
- });
558
-
559
- test("removes user message when hook blocks without skipPreMessageRollback", async () => {
560
- hookBlocked = true;
561
- hookBlockedBy = "guard";
562
- const ctx = makeCtx();
563
- const originalLength = ctx.messages.length;
564
-
565
- await runAgentLoopImpl(ctx, "hello", "msg-1", () => {});
566
-
567
- expect(ctx.messages.length).toBe(originalLength - 1);
568
- });
569
-
570
- test("keeps user message when hook blocks with skipPreMessageRollback", async () => {
571
- hookBlocked = true;
572
- hookBlockedBy = "guard";
573
- const ctx = makeCtx();
574
- const originalLength = ctx.messages.length;
575
-
576
- await runAgentLoopImpl(ctx, "hello", "msg-1", () => {}, {
577
- skipPreMessageRollback: true,
578
- });
579
-
580
- expect(ctx.messages.length).toBe(originalLength);
581
- });
582
- });
583
-
584
538
  describe("tool execution errors via agent loop", () => {
585
539
  test("error events from agent loop are classified and emitted", async () => {
586
540
  const events: ServerMessage[] = [];
@@ -1308,71 +1262,6 @@ describe("session-agent-loop", () => {
1308
1262
  expect(complete).toBeDefined();
1309
1263
  });
1310
1264
 
1311
- test("interactive deny produces graceful assistant response instead of conversation_error", async () => {
1312
- const events: ServerMessage[] = [];
1313
-
1314
- // Reducer exhausts all tiers but context is still too large
1315
- mockReducerStepFn = (msgs: Message[]) => ({
1316
- messages: msgs,
1317
- tier: "injection_downgrade",
1318
- state: {
1319
- appliedTiers: [
1320
- "forced_compaction",
1321
- "tool_result_truncation",
1322
- "media_stubbing",
1323
- "injection_downgrade",
1324
- ],
1325
- injectionMode: "minimal",
1326
- exhausted: true,
1327
- },
1328
- estimatedTokens: 120000,
1329
- });
1330
-
1331
- mockOverflowAction = "request_user_approval";
1332
- mockApprovalResult = { approved: false };
1333
-
1334
- const agentLoopRun: AgentLoopRun = async (messages, onEvent) => {
1335
- onEvent({
1336
- type: "error",
1337
- error: new Error("context_length_exceeded"),
1338
- });
1339
- onEvent({
1340
- type: "usage",
1341
- inputTokens: 100,
1342
- outputTokens: 0,
1343
- model: "test-model",
1344
- providerDurationMs: 50,
1345
- });
1346
- return messages;
1347
- };
1348
-
1349
- const ctx = makeCtx({
1350
- agentLoopRun,
1351
- contextWindowManager: {
1352
- shouldCompact: () => ({ needed: false, estimatedTokens: 0 }),
1353
- maybeCompact: async () => ({ compacted: false }),
1354
- } as unknown as AgentLoopConversationContext["contextWindowManager"],
1355
- });
1356
-
1357
- await runAgentLoopImpl(ctx, "hello", "msg-1", (msg) => events.push(msg));
1358
-
1359
- // Should NOT emit conversation_error
1360
- const conversationError = events.find(
1361
- (e) => e.type === "conversation_error",
1362
- );
1363
- expect(conversationError).toBeUndefined();
1364
-
1365
- // Should emit a graceful assistant text delta instead
1366
- const textDeltas = events.filter(
1367
- (e) => e.type === "assistant_text_delta",
1368
- );
1369
- expect(textDeltas.length).toBeGreaterThanOrEqual(1);
1370
- const lastDelta = textDeltas[textDeltas.length - 1] as {
1371
- text: string;
1372
- };
1373
- expect(lastDelta.text).toContain("compression was declined");
1374
- });
1375
-
1376
1265
  test("non-interactive auto-compress continues without approval prompt", async () => {
1377
1266
  const events: ServerMessage[] = [];
1378
1267
  let callCount = 0;
@@ -1701,7 +1590,7 @@ describe("session-agent-loop", () => {
1701
1590
  providerDurationMs: 100,
1702
1591
  });
1703
1592
  if (onCheckpoint) {
1704
- const decision = onCheckpoint({
1593
+ const decision = await onCheckpoint({
1705
1594
  turnIndex: 0,
1706
1595
  toolCount: 1,
1707
1596
  hasToolUse: true,
@@ -1769,7 +1658,7 @@ describe("session-agent-loop", () => {
1769
1658
  providerDurationMs: 100,
1770
1659
  });
1771
1660
  if (onCheckpoint) {
1772
- onCheckpoint({
1661
+ await onCheckpoint({
1773
1662
  turnIndex: 0,
1774
1663
  toolCount: 1,
1775
1664
  hasToolUse: true,
@@ -2441,14 +2330,14 @@ describe("session-agent-loop", () => {
2441
2330
  // The bulk-clear helper must have been called with the conversation id
2442
2331
  // at least once (one of the three strip sites fired).
2443
2332
  const clearCalls =
2444
- clearPkbSystemReminderMetadataForConversationMock.mock.calls.filter(
2333
+ clearStrippedInjectionMetadataForConversationMock.mock.calls.filter(
2445
2334
  (call) => call[0] === "test-conv",
2446
2335
  );
2447
2336
  expect(clearCalls.length).toBeGreaterThanOrEqual(1);
2448
2337
  });
2449
2338
 
2450
2339
  test("strip-site clear is non-fatal when the helper throws", async () => {
2451
- clearPkbSystemReminderMetadataForConversationMock.mockImplementation(
2340
+ clearStrippedInjectionMetadataForConversationMock.mockImplementation(
2452
2341
  () => {
2453
2342
  throw new Error("db write failed");
2454
2343
  },
@@ -195,6 +195,9 @@ mock.module("../agent/loop.js", () => ({
195
195
  getToolTokenBudget() {
196
196
  return 0;
197
197
  }
198
+ getResolvedTools() {
199
+ return [];
200
+ }
198
201
  getActiveModel() {
199
202
  return undefined;
200
203
  }
@@ -203,7 +206,9 @@ mock.module("../agent/loop.js", () => ({
203
206
  _onEvent: (event: AgentEvent) => void,
204
207
  _signal?: AbortSignal,
205
208
  _requestId?: string,
206
- _onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
209
+ _onCheckpoint?: (
210
+ checkpoint: CheckpointInfo,
211
+ ) => CheckpointDecision | Promise<CheckpointDecision>,
207
212
  ): Promise<Message[]> {
208
213
  return [];
209
214
  }
@@ -72,6 +72,7 @@ mock.module("../memory/qdrant-client.js", () => ({
72
72
  getQdrantClient: () => {
73
73
  throw new Error("Qdrant not initialized");
74
74
  },
75
+ resolveQdrantUrl: () => "http://127.0.0.1:6333",
75
76
  }));
76
77
 
77
78
  // Import after mocking
@@ -202,7 +202,7 @@ mock.module("../memory/conversation-crud.js", () => ({
202
202
  getTurnTimeBounds: () => null,
203
203
  PRIVATE_CONVERSATION_FORK_ERROR: "Cannot fork a private conversation",
204
204
  countConversationsByScheduleJobId: () => 0,
205
- countMessagesWithSlackMeta: () => 0,
205
+ selectSlackMetaCandidateMetadata: () => [],
206
206
  forkConversation: () => null,
207
207
  wipeConversation: () => ({
208
208
  conversations: 0,
@@ -219,7 +219,7 @@ mock.module("../memory/conversation-crud.js", () => ({
219
219
  getLastAssistantTimestampBefore: () => null,
220
220
  archiveConversation: () => false,
221
221
  unarchiveConversation: () => false,
222
- clearPkbSystemReminderMetadataForConversation: () => {},
222
+ clearStrippedInjectionMetadataForConversation: () => {},
223
223
  }));
224
224
 
225
225
  mock.module("../memory/conversation-queries.js", () => ({
@@ -233,13 +233,6 @@ mock.module("../memory/conversation-queries.js", () => ({
233
233
  buildExcerpt: () => "",
234
234
  }));
235
235
 
236
- mock.module("../hooks/manager.js", () => ({
237
- getHookManager: () => ({
238
- trigger: () => Promise.resolve(),
239
- initialize: () => {},
240
- }),
241
- }));
242
-
243
236
  mock.module("../tools/watch/watch-state.js", () => ({
244
237
  watchSessions: new Map(),
245
238
  registerWatchStartNotifier: () => {},
@@ -292,13 +285,6 @@ mock.module("../calls/call-store.js", () => ({
292
285
  finalizeCallbackClaim: () => true,
293
286
  }));
294
287
 
295
- mock.module("../daemon/watch-handler.js", () => ({
296
- lastCommentaryByConversation: new Map(),
297
- lastSummaryByConversation: new Map(),
298
- handleWatchObservation: () => Promise.resolve(),
299
- generateSummary: () => Promise.resolve(),
300
- }));
301
-
302
288
  mock.module("../tools/browser/browser-screencast.js", () => ({
303
289
  registerConversationSender: () => {},
304
290
  unregisterConversationSender: () => {},
@@ -358,8 +358,11 @@ describe("pairDeliveryWithConversation", () => {
358
358
  expect(upsertOutboundBindingMock).toHaveBeenCalledTimes(1);
359
359
  });
360
360
 
361
- test("reuses pre-namespace binding when namespaced binding is absent", async () => {
362
- // Simulate a binding created before the notification: prefix was introduced
361
+ test("reuses pre-namespace binding via inbound path when namespaced binding is absent", async () => {
362
+ // Simulate a binding created before the notification: prefix was introduced.
363
+ // Un-prefixed bindings are resolved by step 1 (inbound path) which skips
364
+ // the source check and does not upsert a notification-prefixed binding —
365
+ // the conversation is still reused.
363
366
  mockExistingConversations["conv-legacy"] = {
364
367
  id: "conv-legacy",
365
368
  source: "notification",
@@ -390,16 +393,11 @@ describe("pairDeliveryWithConversation", () => {
390
393
  expect(result.conversationId).toBe("conv-legacy");
391
394
  expect(result.createdNewConversation).toBe(false);
392
395
  expect(createConversationMock).not.toHaveBeenCalled();
393
- // The upsert should write with the new namespaced sourceChannel
394
- expect(upsertOutboundBindingMock).toHaveBeenCalledTimes(1);
395
- const upsertArgs = upsertOutboundBindingMock.mock.calls[0]![0] as Record<
396
- string,
397
- unknown
398
- >;
399
- expect(upsertArgs.sourceChannel).toBe("notification:telegram");
396
+ // Inbound path does not touch outbound bindings it only reads.
397
+ expect(upsertOutboundBindingMock).not.toHaveBeenCalled();
400
398
  });
401
399
 
402
- test("falls back to new conversation when bound conversation is stale (wrong source)", async () => {
400
+ test("falls back to new conversation when notification-bound conversation is stale (wrong source) and no inbound binding exists", async () => {
403
401
  mockExistingConversations["conv-user-owned"] = {
404
402
  id: "conv-user-owned",
405
403
  source: "user",
@@ -410,6 +408,8 @@ describe("pairDeliveryWithConversation", () => {
410
408
  sourceChannel: "notification:slack",
411
409
  externalChatId: "C0123ABCDEF",
412
410
  };
411
+ // No inbound (un-prefixed) binding — step 1 finds nothing, step 2
412
+ // finds the notification binding but the source check rejects it.
413
413
 
414
414
  const signal = makeSignal();
415
415
  const copy = makeCopy();
@@ -468,6 +468,170 @@ describe("pairDeliveryWithConversation", () => {
468
468
  expect(upsertOutboundBindingMock).toHaveBeenCalledTimes(1);
469
469
  });
470
470
 
471
+ // ── Inbound conversation continuity ──────────────────────────────
472
+
473
+ test("prefers inbound conversation over notification-scoped conversation for reply continuity", async () => {
474
+ // An inbound conversation exists (un-prefixed binding, source: null)
475
+ // AND a notification conversation exists (prefixed binding, source: "notification").
476
+ // The inbound conversation should win so that the user's replies
477
+ // include the notification in their conversation history.
478
+ mockExistingConversations["conv-inbound"] = {
479
+ id: "conv-inbound",
480
+ source: null as unknown as string,
481
+ title: "Slack DM",
482
+ };
483
+ mockBindings["slack:D0ASABGUTQR"] = {
484
+ conversationId: "conv-inbound",
485
+ sourceChannel: "slack",
486
+ externalChatId: "D0ASABGUTQR",
487
+ };
488
+ mockExistingConversations["conv-notification"] = {
489
+ id: "conv-notification",
490
+ source: "notification",
491
+ title: "Notification Thread",
492
+ };
493
+ mockBindings["notification:slack:D0ASABGUTQR"] = {
494
+ conversationId: "conv-notification",
495
+ sourceChannel: "notification:slack",
496
+ externalChatId: "D0ASABGUTQR",
497
+ };
498
+
499
+ const signal = makeSignal();
500
+ const copy = makeCopy({
501
+ conversationSeedMessage: "New tweet from @alice - draft reply attached",
502
+ });
503
+ const bindingContext: DestinationBindingContext = {
504
+ sourceChannel: "slack" as NotificationChannel,
505
+ externalChatId: "D0ASABGUTQR",
506
+ };
507
+
508
+ const result = await pairDeliveryWithConversation(
509
+ signal,
510
+ "slack" as NotificationChannel,
511
+ copy,
512
+ { bindingContext },
513
+ );
514
+
515
+ // Should use the inbound conversation, not the notification one
516
+ expect(result.conversationId).toBe("conv-inbound");
517
+ expect(result.messageId).toBe("msg-001");
518
+ expect(result.createdNewConversation).toBe(false);
519
+ expect(result.conversationFallbackUsed).toBe(false);
520
+ expect(createConversationMock).not.toHaveBeenCalled();
521
+ expect(addMessageMock).toHaveBeenCalledTimes(1);
522
+ expect(addMessageMock.mock.calls[0]![0]).toBe("conv-inbound");
523
+ // Should NOT touch the notification binding — we only read the inbound one
524
+ expect(upsertOutboundBindingMock).not.toHaveBeenCalled();
525
+ });
526
+
527
+ test("uses inbound conversation regardless of source field for reply continuity", async () => {
528
+ // The inbound conversation has source: null (typical for conversations
529
+ // created by the inbound handler). The notification would normally
530
+ // skip this because effectiveSource is "notification". But the inbound
531
+ // path intentionally skips the source check.
532
+ mockExistingConversations["conv-inbound-null-source"] = {
533
+ id: "conv-inbound-null-source",
534
+ source: null as unknown as string,
535
+ title: "Slack DM",
536
+ };
537
+ mockBindings["slack:D0CHATID123"] = {
538
+ conversationId: "conv-inbound-null-source",
539
+ sourceChannel: "slack",
540
+ externalChatId: "D0CHATID123",
541
+ };
542
+
543
+ const signal = makeSignal();
544
+ const copy = makeCopy({
545
+ conversationSeedMessage: "Your daily briefing is ready",
546
+ });
547
+ const bindingContext: DestinationBindingContext = {
548
+ sourceChannel: "slack" as NotificationChannel,
549
+ externalChatId: "D0CHATID123",
550
+ };
551
+
552
+ const result = await pairDeliveryWithConversation(
553
+ signal,
554
+ "slack" as NotificationChannel,
555
+ copy,
556
+ { bindingContext },
557
+ );
558
+
559
+ expect(result.conversationId).toBe("conv-inbound-null-source");
560
+ expect(result.createdNewConversation).toBe(false);
561
+ expect(createConversationMock).not.toHaveBeenCalled();
562
+ expect(addMessageMock.mock.calls[0]![0]).toBe("conv-inbound-null-source");
563
+ });
564
+
565
+ test("falls back to notification binding when inbound binding points to deleted conversation", async () => {
566
+ // Inbound binding exists but conversation was deleted.
567
+ // Should fall through to notification binding.
568
+ mockBindings["slack:D0STALE"] = {
569
+ conversationId: "conv-deleted-inbound",
570
+ sourceChannel: "slack",
571
+ externalChatId: "D0STALE",
572
+ };
573
+ // conv-deleted-inbound is NOT in mockExistingConversations — getConversation returns null
574
+
575
+ mockExistingConversations["conv-notification-fallback"] = {
576
+ id: "conv-notification-fallback",
577
+ source: "notification",
578
+ title: "Notification Thread",
579
+ };
580
+ mockBindings["notification:slack:D0STALE"] = {
581
+ conversationId: "conv-notification-fallback",
582
+ sourceChannel: "notification:slack",
583
+ externalChatId: "D0STALE",
584
+ };
585
+
586
+ const signal = makeSignal();
587
+ const copy = makeCopy();
588
+ const bindingContext: DestinationBindingContext = {
589
+ sourceChannel: "slack" as NotificationChannel,
590
+ externalChatId: "D0STALE",
591
+ };
592
+
593
+ const result = await pairDeliveryWithConversation(
594
+ signal,
595
+ "slack" as NotificationChannel,
596
+ copy,
597
+ { bindingContext },
598
+ );
599
+
600
+ // Inbound conversation is gone — should fall back to notification conversation
601
+ expect(result.conversationId).toBe("conv-notification-fallback");
602
+ expect(result.createdNewConversation).toBe(false);
603
+ expect(createConversationMock).not.toHaveBeenCalled();
604
+ });
605
+
606
+ test("falls through to create new conversation when no inbound and no notification binding exists", async () => {
607
+ // First notification to a channel where user has never messaged.
608
+ // No bindings at all — should create a new conversation.
609
+ const signal = makeSignal();
610
+ const copy = makeCopy();
611
+ const bindingContext: DestinationBindingContext = {
612
+ sourceChannel: "slack" as NotificationChannel,
613
+ externalChatId: "D0BRANDNEW",
614
+ };
615
+
616
+ const result = await pairDeliveryWithConversation(
617
+ signal,
618
+ "slack" as NotificationChannel,
619
+ copy,
620
+ { bindingContext },
621
+ );
622
+
623
+ expect(result.conversationId).toBe("conv-001");
624
+ expect(result.createdNewConversation).toBe(true);
625
+ expect(createConversationMock).toHaveBeenCalledTimes(1);
626
+ expect(upsertOutboundBindingMock).toHaveBeenCalledTimes(1);
627
+ const upsertArgs = upsertOutboundBindingMock.mock.calls[0]![0] as Record<
628
+ string,
629
+ unknown
630
+ >;
631
+ expect(upsertArgs.sourceChannel).toBe("notification:slack");
632
+ expect(upsertArgs.externalChatId).toBe("D0BRANDNEW");
633
+ });
634
+
471
635
  test("creates new conversation and upserts binding when no prior binding exists", async () => {
472
636
  const signal = makeSignal();
473
637
  const copy = makeCopy();
@@ -23,7 +23,7 @@ mock.module("../providers/registry.js", () => ({
23
23
  mock.module("../config/loader.js", () => ({
24
24
  getConfig: () => ({
25
25
  ui: {},
26
-
26
+
27
27
  llm: {
28
28
  default: {
29
29
  provider: "mock-provider",
@@ -141,6 +141,9 @@ mock.module("../agent/loop.js", () => ({
141
141
  getToolTokenBudget() {
142
142
  return 0;
143
143
  }
144
+ getResolvedTools() {
145
+ return [];
146
+ }
144
147
  getActiveModel() {
145
148
  return undefined;
146
149
  }
@@ -170,6 +170,9 @@ mock.module("../agent/loop.js", () => ({
170
170
  getToolTokenBudget() {
171
171
  return 0;
172
172
  }
173
+ getResolvedTools() {
174
+ return [];
175
+ }
173
176
  async run(
174
177
  messages: Message[],
175
178
  onEvent: (event: Record<string, unknown>) => void,
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { AgentEvent } from "../agent/loop.js";
4
4
  import type { UserMessageAttachment } from "../daemon/message-protocol.js";
5
+ import { resetPluginRegistryAndRegisterDefaults } from "../plugins/defaults/index.js";
5
6
  import type { Message, ProviderResponse } from "../providers/types.js";
6
7
  import { ProviderError } from "../util/errors.js";
7
8
 
@@ -26,7 +27,7 @@ mock.module("../config/loader.js", () => ({
26
27
  daemon: {
27
28
  titleGenerationMaxTokens: 30,
28
29
  },
29
-
30
+
30
31
  llm: {
31
32
  default: {
32
33
  provider: "mock-provider",
@@ -76,9 +77,13 @@ mock.module("../config/loader.js", () => ({
76
77
  }));
77
78
 
78
79
  // Token estimator: return a small value (well within budget) so preflight
79
- // does not trigger in existing tests.
80
+ // does not trigger in existing tests. Stub both the calibrated and raw
81
+ // entry points — the latter backs the default `tokenEstimate` plugin
82
+ // pipeline now used by the orchestrator's preflight / mid-loop checkpoints.
80
83
  mock.module("../context/token-estimator.js", () => ({
81
84
  estimatePromptTokens: () => 1000,
85
+ estimatePromptTokensRaw: () => 1000,
86
+ estimateToolsTokens: () => 0,
82
87
  }));
83
88
 
84
89
  // Overflow recovery module mocks — the convergence loop delegates to these
@@ -140,11 +145,6 @@ mock.module("../daemon/context-overflow-policy.js", () => ({
140
145
  resolveOverflowAction: () => "fail_gracefully",
141
146
  }));
142
147
 
143
- mock.module("../daemon/context-overflow-approval.js", () => ({
144
- requestCompressionApproval: async () => ({ approved: false }),
145
- CONTEXT_OVERFLOW_TOOL_NAME: "context_overflow_compression",
146
- }));
147
-
148
148
  mock.module("../prompts/system-prompt.js", () => ({
149
149
  buildSystemPrompt: () => "system prompt",
150
150
  }));
@@ -261,6 +261,9 @@ mock.module("../agent/loop.js", () => ({
261
261
  getToolTokenBudget() {
262
262
  return 0;
263
263
  }
264
+ getResolvedTools() {
265
+ return [];
266
+ }
264
267
  getActiveModel() {
265
268
  return undefined;
266
269
  }
@@ -445,6 +448,12 @@ describe("provider ordering error retry", () => {
445
448
  firstRunErrorMode = "ordering";
446
449
  maybeCompactCalls = [];
447
450
  forceCompactionEnabled = false;
451
+ // Orchestrator pipelines (`overflowReduce`, `persistence`, …) run through
452
+ // the plugin registry; re-register every default so each pipeline has a
453
+ // middleware to dispatch to. The `context-overflow-reducer` module itself
454
+ // (and other collaborators) are mocked above, so the default plugins'
455
+ // delegates go through the mocked implementations.
456
+ resetPluginRegistryAndRegisterDefaults();
448
457
  });
449
458
 
450
459
  test("simulated strict provider error triggers exactly one retry", async () => {