@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
@@ -30,6 +30,118 @@ const COMPACTION_TOOL_RESULT_MAX_CHARS = 6_000;
30
30
  const MIN_COMPACTABLE_PERSISTED_MESSAGES = 2;
31
31
  const INTERNAL_CONTEXT_SUMMARY_MESSAGES = new WeakSet<Message>();
32
32
 
33
+ /**
34
+ * When the existing summary is this fraction or more of the per-summary
35
+ * token budget, inject a "compress older content aggressively" instruction
36
+ * so incremental-update passes don't let the summary grow unboundedly.
37
+ */
38
+ const SUMMARY_COMPRESSION_PRESSURE_RATIO = 0.6;
39
+
40
+ /**
41
+ * Text-block prefixes that persist in live history (for prefix-caching
42
+ * stability and model grounding) but pollute the summarizer's view of the
43
+ * actual conversation. These blocks are system-metadata attached to user
44
+ * turns — memory injections, turn context, workspace hints, etc. They are
45
+ * stripped ONLY from the messages fed to the summarization LLM call. Live
46
+ * history is never mutated, so prefix caching is preserved.
47
+ *
48
+ * This list intentionally overlaps with `RUNTIME_INJECTION_PREFIXES` in
49
+ * `conversation-runtime-assembly.ts`. That list governs in-flight turn
50
+ * assembly via pure prefix matching; this one governs compaction input.
51
+ * Keep the two lists in sync when a new injection type is added.
52
+ *
53
+ * Compaction strip coverage is two-tier: this prefix list catches
54
+ * internal-vocabulary tags and any tag carrying the `__injected`
55
+ * attribute, while `COMPACTION_ONLY_WRAPPED_STRIP_TAGS` below matches
56
+ * ambiguous bare-tag blocks that are shaped like a runtime-emitted
57
+ * open/close wrap. A new ambiguous tag added upstream needs to be
58
+ * evaluated against both tiers — internal-vocabulary names go here,
59
+ * and names whose bare form collides with ordinary English
60
+ * (`<memory>`, `<workspace>`, `<knowledge_base>`, `<pkb>`,
61
+ * `<system_reminder>`) go in the wrapped-strip list so user prose
62
+ * mentioning the tag is preserved.
63
+ */
64
+ const COMPACTION_ONLY_STRIP_PREFIXES = [
65
+ "<memory __injected>",
66
+ "<memory_image __injected>",
67
+ "</memory_image>",
68
+ "<memory_context __injected>",
69
+ "<turn_context>",
70
+ "<channel_turn_context>",
71
+ "<guardian_context>",
72
+ "<inbound_actor_context>",
73
+ "<interface_turn_context>",
74
+ "<workspace_top_level>",
75
+ "<now_scratchpad>",
76
+ "<NOW.md Always keep this up to date",
77
+ "<active_thread>",
78
+ "<active_subagents>",
79
+ "<active_workspace>",
80
+ "<active_dynamic_page>",
81
+ "<channel_capabilities>",
82
+ "<channel_command_context>",
83
+ "<voice_call_control>",
84
+ "<transport_hints>",
85
+ "<system_notice>",
86
+ "<non_interactive_context>",
87
+ "<temporal_context>",
88
+ ];
89
+
90
+ /**
91
+ * Tags whose bare form (`<tag>`) is common English vocabulary or markup a
92
+ * user might legitimately type in prose. For these we only strip a text
93
+ * block if it is shaped exactly like a runtime injection: starts with
94
+ * `<tag>\n` and ends with `</tag>`. This bare-tag wrapped shape
95
+ * (e.g. `<memory>\n...\n</memory>`) appears in persisted history
96
+ * alongside the `__injected`-attributed variants, which the prefix list
97
+ * above already catches via `<memory __injected>`. A user who mentions
98
+ * `<memory>` in a sentence or inlines `<workspace>...</workspace>` within
99
+ * other prose will not match this shape.
100
+ */
101
+ const COMPACTION_ONLY_WRAPPED_STRIP_TAGS = [
102
+ "memory",
103
+ "memory_context",
104
+ "workspace",
105
+ "knowledge_base",
106
+ "pkb",
107
+ "system_reminder",
108
+ ];
109
+
110
+ function isCompactionInjectedBlock(text: string): boolean {
111
+ if (COMPACTION_ONLY_STRIP_PREFIXES.some((p) => text.startsWith(p))) {
112
+ return true;
113
+ }
114
+ return COMPACTION_ONLY_WRAPPED_STRIP_TAGS.some(
115
+ (tag) => text.startsWith(`<${tag}>\n`) && text.endsWith(`</${tag}>`),
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Remove text blocks that look like runtime injections from user messages.
121
+ * Non-text blocks (images, tool_use, tool_result, etc.) are untouched.
122
+ * Empty messages (every block filtered out) are dropped from the output.
123
+ *
124
+ * Used only on the `compactableMessages` slice right before it is
125
+ * serialized for the summarization LLM — the caller's original message
126
+ * array is never mutated.
127
+ */
128
+ export function stripCompactionOnlyInjections(messages: Message[]): Message[] {
129
+ return messages
130
+ .map((message) => {
131
+ if (message.role !== "user") return message;
132
+ const nextContent = message.content.filter((block) => {
133
+ if (block.type !== "text") return true;
134
+ return !isCompactionInjectedBlock(block.text);
135
+ });
136
+ if (nextContent.length === message.content.length) return message;
137
+ if (nextContent.length === 0) return null;
138
+ return { ...message, content: nextContent };
139
+ })
140
+ .filter(
141
+ (message): message is NonNullable<typeof message> => message != null,
142
+ );
143
+ }
144
+
33
145
  /**
34
146
  * Load the compaction summary system prompt from the bundled markdown asset.
35
147
  *
@@ -57,18 +169,27 @@ export function loadCompactPrompt(): string {
57
169
  * rather than failing module import at startup.
58
170
  */
59
171
  const SUMMARY_PROMPT_FALLBACK = [
60
- "You compress long assistant conversations into durable working memory.",
61
- "Focus on actionable state, not prose.",
62
- "Preserve concrete facts: goals, constraints, decisions, pending questions, file paths, commands, errors, and TODOs.",
63
- "Remove repetition and stale details that were superseded.",
64
- 'Thread anchors: when a "Retained Thread References" section is present, each listed reply cites its parent via `→ Mxxxxxx`. If that parent appears in the Transcript, preserve its text verbatim (reactions may be aggregated as "N users reacted"). Omit when the section is absent.',
65
- "Return concise markdown using these section headers exactly:",
66
- "## Goals",
67
- "## Constraints",
68
- "## Decisions",
69
- "## Open Conversations",
70
- "## Key Artifacts",
71
- "## Recent Progress",
172
+ "You are summarizing a long conversation so that the assistant can keep working with it after older messages are dropped. Your summary will REPLACE those messages — the assistant's only access to what was said earlier will be what you write here.",
173
+ "",
174
+ "Be thorough. Capture what happened, why it mattered, what's unresolved, and what was felt. Do not compress away emotional tone, relationship context, or nuance. Keep specific details (names, numbers, file paths, commands, URLs, exact phrasings) when they might matter later.",
175
+ "",
176
+ "Target length: aim for 1500–4000 tokens. Use the upper end when the conversation is rich in decisions, relationships, emotional content, or threads that are still open. Use the lower end for short or simple task execution.",
177
+ "",
178
+ "Open with a 1–2 paragraph narrative describing what the conversation is about and where it currently stands. Then use `## ` section headers. Use these when they apply; skip sections that have nothing to say; add your own headers when something doesn't fit:",
179
+ "- `## What We're Working On`",
180
+ "- `## Decisions & Commitments`",
181
+ "- `## Facts Worth Remembering`",
182
+ "- `## Open Threads`",
183
+ "- `## Emotional Arc / Relationship Notes` (include when relevant)",
184
+ "- `## Artifacts & References`",
185
+ "",
186
+ "If an existing summary is provided, update it: merge new information in, prefer the most recent and explicit detail on conflicts, and preserve anything still unresolved or still true. Do not restart from scratch.",
187
+ "",
188
+ "Never include in the summary: content inside `<memory __injected>`, `<memory>`, `<turn_context>`, `<workspace>`, `<knowledge_base>`, `<system_reminder>`, `<now_scratchpad>`, `<NOW.md …>`, `<active_thread>`, `<channel_capabilities>`, `<transport_hints>`, `<system_notice>`, or any other angle-bracket-tagged system blocks. Tool-call boilerplate (retries, failed attempts the assistant recovered from, routine status updates) — summarize the outcome instead. Repetitive chit-chat that adds nothing.",
189
+ "",
190
+ 'Thread anchors (Slack only): if the input includes a "Retained Thread References" section, each listed reply cites its parent via `→ Mxxxxxx`. If that parent appears in the Transcript, preserve its text verbatim. Omit when absent.',
191
+ "",
192
+ "Return only the summary itself in markdown — no preamble, no meta-commentary.",
72
193
  ].join("\n");
73
194
 
74
195
  /**
@@ -535,8 +656,14 @@ export class ContextWindowManager {
535
656
  const retainedThreadRefs = collectRetainedThreadReferences(
536
657
  messages.slice(keepPlan.keepFromIndex),
537
658
  );
659
+ // Strip runtime injections (memory, turn context, workspace hints, etc.)
660
+ // from the messages fed to the summarizer. These blocks are system
661
+ // metadata; leaving them in causes the summary to echo rotating memory
662
+ // content instead of the actual conversation. The caller's live message
663
+ // array is untouched so prefix caching stays intact.
664
+ const transcriptSource = stripCompactionOnlyInjections(compactableMessages);
538
665
  const transcriptBlocks = this.capTranscriptBlocksToTokenBudget(
539
- serializeMessagesToContentBlocks(compactableMessages),
666
+ serializeMessagesToContentBlocks(transcriptSource),
540
667
  existingSummary ?? "No previous summary.",
541
668
  retainedThreadRefs,
542
669
  );
@@ -882,10 +1009,18 @@ export class ContextWindowManager {
882
1009
  */
883
1010
  failed: boolean;
884
1011
  }> {
1012
+ // When the existing summary is already consuming most of its budget,
1013
+ // nudge the model to compress older durable content aggressively so
1014
+ // incremental-update passes don't let the summary grow unboundedly.
1015
+ const existingSummaryTokens = estimateTextTokens(currentSummary);
1016
+ const compressionPressure =
1017
+ existingSummaryTokens >=
1018
+ this.summaryMaxTokens * SUMMARY_COMPRESSION_PRESSURE_RATIO;
885
1019
  const contentBlocks = buildSummaryContentBlocks(
886
1020
  currentSummary,
887
1021
  transcriptBlocks,
888
1022
  retainedThreadRefs,
1023
+ { compressionPressure },
889
1024
  );
890
1025
  const summaryMessage: Message = { role: "user", content: contentBlocks };
891
1026
  let failed = false;
@@ -895,7 +1030,10 @@ export class ContextWindowManager {
895
1030
  undefined,
896
1031
  SUMMARY_SYSTEM_PROMPT,
897
1032
  {
898
- config: { max_tokens: this.summaryMaxTokens },
1033
+ config: {
1034
+ callSite: "conversationSummarization" as const,
1035
+ max_tokens: this.summaryMaxTokens,
1036
+ },
899
1037
  signal,
900
1038
  },
901
1039
  );
@@ -942,10 +1080,38 @@ export class ContextWindowManager {
942
1080
  // Budget in tokens → approximate char limit (4 chars ≈ 1 token).
943
1081
  const maxChars = this.summaryMaxTokens * 4;
944
1082
  if (summary.length <= maxChars) return summary;
945
- return `${safeStringSlice(summary, 0, maxChars)}...`;
1083
+ return clampSummaryAtSectionBoundary(summary, maxChars);
946
1084
  }
947
1085
  }
948
1086
 
1087
+ /**
1088
+ * Truncate a markdown summary that exceeds `maxChars`, preferring a
1089
+ * section boundary (`\n## `) so we never cut a heading mid-text. Falls
1090
+ * back to a hard character slice when no boundary exists in the safe
1091
+ * region (first half of the budget).
1092
+ */
1093
+ export function clampSummaryAtSectionBoundary(
1094
+ summary: string,
1095
+ maxChars: number,
1096
+ ): string {
1097
+ if (summary.length <= maxChars) return summary;
1098
+ const ELLIPSIS = "...";
1099
+ // Hard limit we must stay under, leaving room for the ellipsis suffix.
1100
+ const cutoff = maxChars - ELLIPSIS.length;
1101
+ if (cutoff <= 0) return ELLIPSIS;
1102
+ const head = safeStringSlice(summary, 0, cutoff);
1103
+ // Find the last `## ` heading at a line start. Require it to be past the
1104
+ // midpoint of the allowed region so we don't drop most of the summary
1105
+ // just to hit a boundary — better to cut mid-section late than to keep
1106
+ // almost nothing.
1107
+ const halfway = Math.floor(cutoff / 2);
1108
+ const boundary = head.lastIndexOf("\n## ");
1109
+ if (boundary >= halfway) {
1110
+ return `${head.slice(0, boundary).trimEnd()}\n${ELLIPSIS}`;
1111
+ }
1112
+ return `${head}${ELLIPSIS}`;
1113
+ }
1114
+
949
1115
  function collectUserTurnStartIndexes(messages: Message[]): number[] {
950
1116
  const starts: number[] = [];
951
1117
  for (let i = 0; i < messages.length; i++) {
@@ -1092,17 +1258,25 @@ function buildSummaryContentBlocks(
1092
1258
  currentSummary: string,
1093
1259
  transcriptBlocks: ContentBlock[],
1094
1260
  retainedThreadRefs: string[],
1261
+ options: { compressionPressure: boolean } = { compressionPressure: false },
1095
1262
  ): ContentBlock[] {
1096
1263
  const lines = [
1097
1264
  "Update the summary with new transcript data.",
1098
1265
  "If new information conflicts with older notes, keep the most recent and explicit detail.",
1099
1266
  "Keep all unresolved asks and next steps.",
1100
1267
  "For any images included below, describe their visual content in the summary so the information is preserved after compaction.",
1268
+ ];
1269
+ if (options.compressionPressure) {
1270
+ lines.push(
1271
+ "The existing summary is approaching its token budget. Compress older durable content aggressively (drop detail that is no longer load-bearing, merge bullets, tighten prose) while preserving the most recent turns' nuance.",
1272
+ );
1273
+ }
1274
+ lines.push(
1101
1275
  "",
1102
1276
  "### Existing Summary",
1103
1277
  currentSummary.trim().length > 0 ? currentSummary.trim() : "None.",
1104
1278
  "",
1105
- ];
1279
+ );
1106
1280
  if (retainedThreadRefs.length > 0) {
1107
1281
  lines.push(
1108
1282
  "### Retained Thread References",
@@ -16,6 +16,7 @@ import {
16
16
  oauthConnectionAccessTokenPath,
17
17
  } from "@vellumai/credential-storage";
18
18
 
19
+ import { manualTokenAccessCredentialKey } from "../oauth/manual-token-connection.js";
19
20
  import {
20
21
  getProvider,
21
22
  listActiveConnectionsByProvider,
@@ -100,7 +101,9 @@ async function pingProvider(
100
101
 
101
102
  const body =
102
103
  method !== "GET" && pingBody
103
- ? (typeof pingBody === "string" ? pingBody : JSON.stringify(pingBody))
104
+ ? typeof pingBody === "string"
105
+ ? pingBody
106
+ : JSON.stringify(pingBody)
104
107
  : undefined;
105
108
 
106
109
  try {
@@ -155,12 +158,22 @@ async function checkConnection(
155
158
  pingBody,
156
159
  } = opts;
157
160
 
158
- const base = { connectionId, provider, accountInfo, missingScopes: [] as string[] };
161
+ const base = {
162
+ connectionId,
163
+ provider,
164
+ accountInfo,
165
+ missingScopes: [] as string[],
166
+ };
159
167
 
160
- // 1. Check token presence
161
- const token = await getSecureKeyAsync(
162
- oauthConnectionAccessTokenPath(connectionId),
163
- );
168
+ // 1. Check token presence. Manual-token providers (e.g. slack_channel,
169
+ // telegram) store their primary token under credential/<provider>/<field>
170
+ // rather than oauth_connection/<id>/access_token, so resolve the correct
171
+ // path before looking up the token — otherwise the lookup always returns
172
+ // null and marks these providers as "missing_token".
173
+ const tokenPath =
174
+ manualTokenAccessCredentialKey(provider) ??
175
+ oauthConnectionAccessTokenPath(connectionId);
176
+ const token = await getSecureKeyAsync(tokenPath);
164
177
  if (!token) {
165
178
  return {
166
179
  ...base,
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Tests for the background conversation feed event emission logic in
3
+ * conversation-agent-loop.ts.
4
+ *
5
+ * Rather than running the full agent loop, these tests exercise the
6
+ * extraction logic in isolation by simulating the conditions under which
7
+ * emitFeedEvent is called: conversation type, message content, and title.
8
+ */
9
+
10
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Module-level mocks — must be in place before importing the module under test
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const emitFeedEventSpy = mock<
17
+ (params: {
18
+ source: string;
19
+ title: string;
20
+ summary: string;
21
+ dedupKey?: string;
22
+ }) => Promise<unknown>
23
+ >(async () => ({}));
24
+
25
+ mock.module("../../home/emit-feed-event.js", () => ({
26
+ emitFeedEvent: emitFeedEventSpy,
27
+ }));
28
+
29
+ const getConversationSpy = mock<
30
+ (id: string) => {
31
+ conversationType: string;
32
+ title: string | null;
33
+ } | null
34
+ >(() => null);
35
+
36
+ const getMessageByIdSpy = mock<
37
+ (
38
+ messageId: string,
39
+ conversationId?: string,
40
+ ) => { id: string; content: string } | null
41
+ >(() => null);
42
+
43
+ // We need to stub enough of conversation-crud to avoid DB initialization.
44
+ // The actual agent loop imports getConversation and getMessageById from
45
+ // conversation-crud — we intercept those here for test assertions.
46
+ mock.module("../../memory/conversation-crud.js", () => ({
47
+ getConversation: getConversationSpy,
48
+ getMessageById: getMessageByIdSpy,
49
+ // Stubs for other exports that may be transitively referenced:
50
+ addMessage: () => {},
51
+ clearStrippedInjectionMetadataForConversation: () => {},
52
+ deleteMessageById: () => {},
53
+ getConversationOriginChannel: () => null,
54
+ getConversationOriginInterface: () => null,
55
+ getLastUserTimestampBefore: () => null,
56
+ provenanceFromTrustContext: () => ({}),
57
+ updateConversationContextWindow: () => {},
58
+ updateConversationTitle: () => {},
59
+ updateMessageMetadata: () => {},
60
+ }));
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Helpers
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /**
67
+ * Simulates the feed-event emission logic from conversation-agent-loop.ts.
68
+ *
69
+ * This mirrors the block inserted after `message_complete` so we can test it
70
+ * in isolation without spinning up the full agent loop infrastructure.
71
+ */
72
+ /**
73
+ * Tracks the last warning logged by simulateFeedEventEmission, so tests can
74
+ * assert that malformed content is swallowed gracefully.
75
+ */
76
+ let lastFeedEventWarning: unknown = undefined;
77
+
78
+ function simulateFeedEventEmission(
79
+ conversationId: string,
80
+ lastAssistantMessageId: string | undefined,
81
+ ): void {
82
+ lastFeedEventWarning = undefined;
83
+ try {
84
+ const conv = getConversationSpy(conversationId);
85
+ if (
86
+ conv &&
87
+ (conv.conversationType === "background" ||
88
+ conv.conversationType === "scheduled")
89
+ ) {
90
+ const lastMsg = lastAssistantMessageId
91
+ ? getMessageByIdSpy(lastAssistantMessageId, conversationId)
92
+ : undefined;
93
+ let summary: string;
94
+ if (lastMsg) {
95
+ const parsed: unknown = JSON.parse(lastMsg.content);
96
+ if (typeof parsed === "string") {
97
+ summary = parsed.slice(0, 200);
98
+ } else if (Array.isArray(parsed)) {
99
+ const textBlock = (
100
+ parsed as Array<{ type?: string; text?: string }>
101
+ ).find((b) => b.type === "text");
102
+ summary =
103
+ typeof textBlock?.text === "string"
104
+ ? textBlock.text.slice(0, 200)
105
+ : (conv.title ?? "Background task completed.");
106
+ } else {
107
+ summary = conv.title ?? "Background task completed.";
108
+ }
109
+ } else {
110
+ summary = conv.title ?? "Background task completed.";
111
+ }
112
+ void emitFeedEventSpy({
113
+ source: "assistant",
114
+ title: conv.title ?? "Background Task",
115
+ summary,
116
+ dedupKey: `bg-conv:${conversationId}`,
117
+ });
118
+ }
119
+ } catch (feedErr) {
120
+ lastFeedEventWarning = feedErr;
121
+ }
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Tests
126
+ // ---------------------------------------------------------------------------
127
+
128
+ describe("background conversation feed event", () => {
129
+ beforeEach(() => {
130
+ emitFeedEventSpy.mockClear();
131
+ getConversationSpy.mockClear();
132
+ getMessageByIdSpy.mockClear();
133
+ });
134
+
135
+ afterEach(() => {
136
+ emitFeedEventSpy.mockReset();
137
+ getConversationSpy.mockReset();
138
+ getMessageByIdSpy.mockReset();
139
+ });
140
+
141
+ test("emits feed event for background conversation with text content", () => {
142
+ const convId = "conv-bg-123";
143
+ const msgId = "msg-456";
144
+
145
+ getConversationSpy.mockReturnValue({
146
+ conversationType: "background",
147
+ title: "Nightly Cleanup",
148
+ });
149
+ getMessageByIdSpy.mockReturnValue({
150
+ id: msgId,
151
+ content: JSON.stringify([
152
+ { type: "text", text: "Cleaned up 42 files successfully." },
153
+ ]),
154
+ });
155
+
156
+ simulateFeedEventEmission(convId, msgId);
157
+
158
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
159
+ expect(emitFeedEventSpy).toHaveBeenCalledWith({
160
+ source: "assistant",
161
+ title: "Nightly Cleanup",
162
+ summary: "Cleaned up 42 files successfully.",
163
+ dedupKey: `bg-conv:${convId}`,
164
+ });
165
+ });
166
+
167
+ test("emits feed event for scheduled conversation", () => {
168
+ const convId = "conv-sched-789";
169
+ const msgId = "msg-101";
170
+
171
+ getConversationSpy.mockReturnValue({
172
+ conversationType: "scheduled",
173
+ title: "Weekly Report",
174
+ });
175
+ getMessageByIdSpy.mockReturnValue({
176
+ id: msgId,
177
+ content: JSON.stringify([{ type: "text", text: "Report generated." }]),
178
+ });
179
+
180
+ simulateFeedEventEmission(convId, msgId);
181
+
182
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
183
+ expect(emitFeedEventSpy).toHaveBeenCalledWith({
184
+ source: "assistant",
185
+ title: "Weekly Report",
186
+ summary: "Report generated.",
187
+ dedupKey: `bg-conv:${convId}`,
188
+ });
189
+ });
190
+
191
+ test("truncates summary to 200 characters", () => {
192
+ const convId = "conv-bg-long";
193
+ const msgId = "msg-long";
194
+ const longText = "A".repeat(300);
195
+
196
+ getConversationSpy.mockReturnValue({
197
+ conversationType: "background",
198
+ title: "Long Task",
199
+ });
200
+ getMessageByIdSpy.mockReturnValue({
201
+ id: msgId,
202
+ content: JSON.stringify([{ type: "text", text: longText }]),
203
+ });
204
+
205
+ simulateFeedEventEmission(convId, msgId);
206
+
207
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
208
+ const call = emitFeedEventSpy.mock.calls[0]![0];
209
+ expect(call.summary).toHaveLength(200);
210
+ expect(call.summary).toBe(longText.slice(0, 200));
211
+ });
212
+
213
+ test("falls back to conversation title when no text block in content", () => {
214
+ const convId = "conv-bg-notext";
215
+ const msgId = "msg-notext";
216
+
217
+ getConversationSpy.mockReturnValue({
218
+ conversationType: "background",
219
+ title: "Image Task",
220
+ });
221
+ getMessageByIdSpy.mockReturnValue({
222
+ id: msgId,
223
+ content: JSON.stringify([{ type: "image", source: { data: "..." } }]),
224
+ });
225
+
226
+ simulateFeedEventEmission(convId, msgId);
227
+
228
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
229
+ expect(emitFeedEventSpy.mock.calls[0]![0].summary).toBe("Image Task");
230
+ });
231
+
232
+ test("falls back to default summary when no message and no title", () => {
233
+ const convId = "conv-bg-notitle";
234
+
235
+ getConversationSpy.mockReturnValue({
236
+ conversationType: "background",
237
+ title: null,
238
+ });
239
+
240
+ simulateFeedEventEmission(convId, undefined);
241
+
242
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(1);
243
+ const call = emitFeedEventSpy.mock.calls[0]![0];
244
+ expect(call.title).toBe("Background Task");
245
+ expect(call.summary).toBe("Background task completed.");
246
+ });
247
+
248
+ test("does NOT emit feed event for standard (foreground) conversations", () => {
249
+ const convId = "conv-standard-1";
250
+
251
+ getConversationSpy.mockReturnValue({
252
+ conversationType: "standard",
253
+ title: "Regular Chat",
254
+ });
255
+
256
+ simulateFeedEventEmission(convId, undefined);
257
+
258
+ expect(emitFeedEventSpy).not.toHaveBeenCalled();
259
+ });
260
+
261
+ test("does NOT emit feed event for private conversations", () => {
262
+ const convId = "conv-private-1";
263
+
264
+ getConversationSpy.mockReturnValue({
265
+ conversationType: "private",
266
+ title: "Secret Chat",
267
+ });
268
+
269
+ simulateFeedEventEmission(convId, undefined);
270
+
271
+ expect(emitFeedEventSpy).not.toHaveBeenCalled();
272
+ });
273
+
274
+ test("uses dedupKey per conversation for in-place updates", () => {
275
+ const convId = "conv-bg-dedup";
276
+
277
+ getConversationSpy.mockReturnValue({
278
+ conversationType: "background",
279
+ title: "Recurring Job",
280
+ });
281
+
282
+ // First run
283
+ simulateFeedEventEmission(convId, undefined);
284
+ // Second run (re-run of same conversation)
285
+ simulateFeedEventEmission(convId, undefined);
286
+
287
+ expect(emitFeedEventSpy).toHaveBeenCalledTimes(2);
288
+ // Both calls use the same dedupKey
289
+ expect(emitFeedEventSpy.mock.calls[0]![0].dedupKey).toBe(
290
+ `bg-conv:${convId}`,
291
+ );
292
+ expect(emitFeedEventSpy.mock.calls[1]![0].dedupKey).toBe(
293
+ `bg-conv:${convId}`,
294
+ );
295
+ });
296
+
297
+ test("swallows malformed JSON content without throwing", () => {
298
+ const convId = "conv-bg-badjson";
299
+ const msgId = "msg-badjson";
300
+
301
+ getConversationSpy.mockReturnValue({
302
+ conversationType: "background",
303
+ title: "Bad JSON Task",
304
+ });
305
+ getMessageByIdSpy.mockReturnValue({
306
+ id: msgId,
307
+ content: "this is not valid JSON {{{",
308
+ });
309
+
310
+ // Should not throw
311
+ expect(() => simulateFeedEventEmission(convId, msgId)).not.toThrow();
312
+ // The error was caught and logged as a warning
313
+ expect(lastFeedEventWarning).toBeInstanceOf(SyntaxError);
314
+ // emitFeedEvent should NOT have been called
315
+ expect(emitFeedEventSpy).not.toHaveBeenCalled();
316
+ });
317
+ });
@@ -7,9 +7,9 @@
7
7
  * `auto-analyze` feature flag and source-type guard).
8
8
  *
9
9
  * We stub the downstream enqueue helpers and the side-effecting lifecycle
10
- * deps (hook manager, notifier/skill cleanup, browser-screencast) so the test
11
- * can invoke `disposeConversation` with a minimal `DisposeContext` and assert
12
- * on the enqueue bookkeeping alone.
10
+ * deps (notifier/skill cleanup, browser-screencast) so the test can invoke
11
+ * `disposeConversation` with a minimal `DisposeContext` and assert on the
12
+ * enqueue bookkeeping alone.
13
13
  *
14
14
  * Two recursion guards apply when the source conversation is itself an
15
15
  * auto-analysis conversation:
@@ -83,19 +83,12 @@ mock.module("../../memory/auto-analysis-enqueue.js", () => ({
83
83
 
84
84
  // Stub all side-effecting cleanup helpers that disposeConversation chains
85
85
  // into after the enqueue block. We assert on enqueue behavior only.
86
- mock.module("../../hooks/manager.js", () => ({
87
- getHookManager: () => ({
88
- trigger: () => undefined,
89
- }),
90
- }));
91
-
92
86
  mock.module("../../tools/browser/browser-screencast.js", () => ({
93
87
  unregisterConversationSender: () => {},
94
88
  }));
95
89
 
96
90
  mock.module("../conversation-notifiers.js", () => ({
97
91
  unregisterCallNotifiers: () => {},
98
- unregisterWatchNotifiers: () => {},
99
92
  }));
100
93
 
101
94
  mock.module("../conversation-skill-tools.js", () => ({
@@ -105,8 +98,7 @@ mock.module("../conversation-skill-tools.js", () => ({
105
98
  // Dynamic import after mock.module calls so stubs take effect.
106
99
  const { disposeConversation } = await import("../conversation-lifecycle.js");
107
100
  type DisposeContext = import("../conversation-lifecycle.js").DisposeContext;
108
- type TrustClass =
109
- import("../../runtime/actor-trust-resolver.js").TrustClass;
101
+ type TrustClass = import("../../runtime/actor-trust-resolver.js").TrustClass;
110
102
 
111
103
  // ---------------------------------------------------------------------------
112
104
  // Fixture builder — minimal DisposeContext satisfying the interface shape.