@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
@@ -55,7 +55,7 @@ mock.module("../providers/registry.js", () => ({
55
55
  mock.module("../config/loader.js", () => ({
56
56
  getConfig: () => ({
57
57
  ui: {},
58
-
58
+
59
59
  llm: {
60
60
  default: {
61
61
  provider: "mock-provider",
@@ -330,7 +330,9 @@ interface PendingRun {
330
330
  reject: (err: Error) => void;
331
331
  messages: Message[];
332
332
  onEvent: (event: AgentEvent) => void;
333
- onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision;
333
+ onCheckpoint?: (
334
+ checkpoint: CheckpointInfo,
335
+ ) => CheckpointDecision | Promise<CheckpointDecision>;
334
336
  }
335
337
 
336
338
  let pendingRuns: PendingRun[] = [];
@@ -341,6 +343,9 @@ mock.module("../agent/loop.js", () => ({
341
343
  getToolTokenBudget() {
342
344
  return 0;
343
345
  }
346
+ getResolvedTools() {
347
+ return [];
348
+ }
344
349
  getActiveModel() {
345
350
  return undefined;
346
351
  }
@@ -349,7 +354,9 @@ mock.module("../agent/loop.js", () => ({
349
354
  onEvent: (event: AgentEvent) => void,
350
355
  _signal?: AbortSignal,
351
356
  _requestId?: string,
352
- onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
357
+ onCheckpoint?: (
358
+ checkpoint: CheckpointInfo,
359
+ ) => CheckpointDecision | Promise<CheckpointDecision>,
353
360
  ): Promise<Message[]> {
354
361
  return new Promise<Message[]>((resolve, reject) => {
355
362
  pendingRuns.push({ resolve, reject, messages, onEvent, onCheckpoint });
@@ -1728,7 +1735,7 @@ describe("Conversation checkpoint handoff", () => {
1728
1735
  // Simulate the agent loop calling it at a turn boundary.
1729
1736
  const run = pendingRuns[0];
1730
1737
  expect(run.onCheckpoint).toBeDefined();
1731
- const decision = run.onCheckpoint!({
1738
+ const decision = await run.onCheckpoint!({
1732
1739
  turnIndex: 0,
1733
1740
  toolCount: 1,
1734
1741
  hasToolUse: true,
@@ -1771,7 +1778,7 @@ describe("Conversation checkpoint handoff", () => {
1771
1778
  // The pending run should have an onCheckpoint callback
1772
1779
  const run = pendingRuns[0];
1773
1780
  expect(run.onCheckpoint).toBeDefined();
1774
- const decision = run.onCheckpoint!({
1781
+ const decision = await run.onCheckpoint!({
1775
1782
  turnIndex: 0,
1776
1783
  toolCount: 1,
1777
1784
  hasToolUse: true,
@@ -1813,7 +1820,7 @@ describe("Conversation checkpoint handoff", () => {
1813
1820
  // Simulate the agent loop yielding at the checkpoint (first run is mid-tool-use)
1814
1821
  const run0 = pendingRuns[0];
1815
1822
  expect(run0.onCheckpoint).toBeDefined();
1816
- const decision = run0.onCheckpoint!({
1823
+ const decision = await run0.onCheckpoint!({
1817
1824
  turnIndex: 0,
1818
1825
  toolCount: 1,
1819
1826
  hasToolUse: true,
@@ -1871,7 +1878,7 @@ describe("Conversation checkpoint handoff", () => {
1871
1878
 
1872
1879
  // Simulate multiple tool-use turns before the checkpoint fires
1873
1880
  // Turn 0 — checkpoint yields because msg-2 is waiting
1874
- const decision = run.onCheckpoint!({
1881
+ const decision = await run.onCheckpoint!({
1875
1882
  turnIndex: 0,
1876
1883
  toolCount: 1,
1877
1884
  hasToolUse: true,
@@ -1966,7 +1973,7 @@ describe("Conversation checkpoint handoff", () => {
1966
1973
  const runA = pendingRuns[0];
1967
1974
  expect(runA.onCheckpoint).toBeDefined();
1968
1975
  expect(
1969
- runA.onCheckpoint!({
1976
+ await runA.onCheckpoint!({
1970
1977
  turnIndex: 0,
1971
1978
  toolCount: 1,
1972
1979
  hasToolUse: true,
@@ -1983,7 +1990,7 @@ describe("Conversation checkpoint handoff", () => {
1983
1990
  const runB = pendingRuns[1];
1984
1991
  expect(runB.onCheckpoint).toBeDefined();
1985
1992
  expect(
1986
- runB.onCheckpoint!({
1993
+ await runB.onCheckpoint!({
1987
1994
  turnIndex: 0,
1988
1995
  toolCount: 1,
1989
1996
  hasToolUse: true,
@@ -1998,7 +2005,7 @@ describe("Conversation checkpoint handoff", () => {
1998
2005
  expect(runC.onCheckpoint).toBeDefined();
1999
2006
  // Only D remains, still should yield
2000
2007
  expect(
2001
- runC.onCheckpoint!({
2008
+ await runC.onCheckpoint!({
2002
2009
  turnIndex: 0,
2003
2010
  toolCount: 1,
2004
2011
  hasToolUse: true,
@@ -2012,7 +2019,7 @@ describe("Conversation checkpoint handoff", () => {
2012
2019
  const runD = pendingRuns[3];
2013
2020
  expect(runD.onCheckpoint).toBeDefined();
2014
2021
  expect(
2015
- runD.onCheckpoint!({
2022
+ await runD.onCheckpoint!({
2016
2023
  turnIndex: 0,
2017
2024
  toolCount: 1,
2018
2025
  hasToolUse: true,
@@ -2411,7 +2418,12 @@ describe("Conversation attachment event payloads", () => {
2411
2418
  model: "mock",
2412
2419
  providerDurationMs: 100,
2413
2420
  });
2414
- run.onEvent({ type: "message_complete", message: assistantMsg });
2421
+ // Await the message_complete dispatch so the async persistence pipeline
2422
+ // (which sets `state.lastAssistantMessageId`) finishes before the mock
2423
+ // resolves `agentLoop.run()` and downstream post-processing runs. The
2424
+ // real agent loop in `agent/loop.ts` awaits onEvent before returning,
2425
+ // so awaiting here keeps the mock faithful to production semantics.
2426
+ await run.onEvent({ type: "message_complete", message: assistantMsg });
2415
2427
  run.resolve([...run.messages, assistantMsg]);
2416
2428
 
2417
2429
  await p1;
@@ -2450,7 +2462,7 @@ describe("Conversation attachment event payloads", () => {
2450
2462
  const run = pendingRuns[0];
2451
2463
  expect(run.onCheckpoint).toBeDefined();
2452
2464
  expect(
2453
- run.onCheckpoint!({
2465
+ await run.onCheckpoint!({
2454
2466
  turnIndex: 0,
2455
2467
  toolCount: 1,
2456
2468
  hasToolUse: true,
@@ -2585,7 +2597,10 @@ describe("Regression: cancel semantics and error channel split", () => {
2585
2597
  model: "mock",
2586
2598
  providerDurationMs: 100,
2587
2599
  });
2588
- run.onEvent({ type: "message_complete", message: assistantMsg });
2600
+ // Await the message_complete dispatch so the async persistence pipeline
2601
+ // finishes before the mock resolves `agentLoop.run()`. See the matching
2602
+ // comment in "message_complete includes assistant attachments".
2603
+ await run.onEvent({ type: "message_complete", message: assistantMsg });
2589
2604
  run.resolve([...run.messages, assistantMsg]);
2590
2605
  await p1;
2591
2606
 
@@ -432,15 +432,16 @@ describe("macOS browser backend fallback (no extension, no cdp-inspect)", () =>
432
432
  expect(interfaceCtx!.userMessageInterface).toBe("macos");
433
433
  expect(interfaceCtx!.assistantMessageInterface).toBe("macos");
434
434
 
435
- // With no extension in the ChromeExtensionRegistry, the conversation's
436
- // hostBrowserProxy should NOT be provisioned through the registry.
437
- // The absence of a hostBrowserProxy means the CDP factory will skip
438
- // the extension candidate entirely and fall through:
439
- // cdp-inspect (desktop-auto) local.
435
+ // macOS now natively supports host_browser, so the conversation gets a
436
+ // hostBrowserProxy provisioned via the SSE sender path even without an
437
+ // extension connection. The proxy routes host_browser_request frames to
438
+ // the macOS desktop client over SSE. The hostBrowserSenderOverride
439
+ // remains undefined because no registry-routed extension connection is
440
+ // present — the proxy uses the default SSE sender.
440
441
  expect(
441
442
  (capturedConversation as unknown as Record<string, unknown>)
442
443
  .hostBrowserProxy,
443
- ).toBeUndefined();
444
+ ).toBeDefined();
444
445
  expect(
445
446
  (capturedConversation as unknown as Record<string, unknown>)
446
447
  .hostBrowserSenderOverride,
@@ -1,4 +1,4 @@
1
- import { describe, expect, mock, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  // PKB search is mocked so the reminder-hints tests can assert behavior
4
4
  // without standing up Qdrant. The mock returns whatever is staged in
@@ -31,8 +31,6 @@ import {
31
31
  findLastInjectedNowContent,
32
32
  injectChannelCapabilityContext,
33
33
  injectChannelCommandContext,
34
- injectNowScratchpad,
35
- injectSubagentStatus,
36
34
  isGroupChatType,
37
35
  isSlackChannelConversation,
38
36
  loadSlackActiveThreadFocusBlock,
@@ -49,9 +47,24 @@ import {
49
47
  writeSlackMetadata,
50
48
  } from "../messaging/providers/slack/message-metadata.js";
51
49
  import { parentAlias } from "../messaging/providers/slack/render-transcript.js";
50
+ import { defaultInjectorsPlugin } from "../plugins/defaults/injectors.js";
51
+ import {
52
+ registerPlugin,
53
+ resetPluginRegistryForTests,
54
+ } from "../plugins/registry.js";
52
55
  import type { Message } from "../providers/types.js";
53
56
  import type { SubagentState } from "../subagent/types.js";
54
57
 
58
+ // `applyRuntimeInjections` is now driven by the default injector chain
59
+ // (PR G2.1). The default-injectors plugin must be registered for the chain
60
+ // to emit workspace, PKB, NOW.md, subagent, Slack, and thread-focus blocks.
61
+ // Each test gets a clean registry so a test that registers its own plugin
62
+ // doesn't leak into the next one.
63
+ beforeEach(() => {
64
+ resetPluginRegistryForTests();
65
+ registerPlugin(defaultInjectorsPlugin);
66
+ });
67
+
55
68
  // ---------------------------------------------------------------------------
56
69
  // resolveChannelCapabilities
57
70
  // ---------------------------------------------------------------------------
@@ -797,86 +810,12 @@ describe("applyRuntimeInjections — injection mode", () => {
797
810
  });
798
811
  });
799
812
 
800
- // ---------------------------------------------------------------------------
801
- // injectNowScratchpad
802
- // ---------------------------------------------------------------------------
803
-
804
- describe("injectNowScratchpad", () => {
805
- const baseUserMessage: Message = {
806
- role: "user",
807
- content: [{ type: "text", text: "What should I work on?" }],
808
- };
809
-
810
- test("inserts NOW.md before user content", () => {
811
- const result = injectNowScratchpad(
812
- baseUserMessage,
813
- "Current focus: shipping PR 3",
814
- );
815
- expect(result.content.length).toBe(2);
816
- // Scratchpad comes first (before user content)
817
- const injected = result.content[0];
818
- expect(injected.type).toBe("text");
819
- const text = (injected as { type: "text"; text: string }).text;
820
- expect(text).toBe(
821
- "<NOW.md Always keep this up to date; keep under 10 lines>\nCurrent focus: shipping PR 3\n</NOW.md>",
822
- );
823
- // Original content comes last
824
- expect((result.content[1] as { type: "text"; text: string }).text).toBe(
825
- "What should I work on?",
826
- );
827
- });
828
-
829
- test("inserts after memory_context but before user content", () => {
830
- const messageWithMemory: Message = {
831
- role: "user",
832
- content: [
833
- {
834
- type: "text",
835
- text: "<memory_context __injected>\nrecalled notes\n</memory_context>",
836
- },
837
- { type: "text", text: "What should I work on?" },
838
- ],
839
- };
840
-
841
- const result = injectNowScratchpad(messageWithMemory, "scratchpad notes");
842
- expect(result.content.length).toBe(3);
843
- // Memory context stays first
844
- expect(
845
- (result.content[0] as { type: "text"; text: string }).text,
846
- ).toContain("<memory_context");
847
- // Scratchpad inserted after memory
848
- expect(
849
- (result.content[1] as { type: "text"; text: string }).text,
850
- ).toContain("<NOW.md");
851
- // User content is last
852
- expect((result.content[2] as { type: "text"; text: string }).text).toBe(
853
- "What should I work on?",
854
- );
855
- });
856
-
857
- test("preserves existing multi-block content with scratchpad before it", () => {
858
- const multiBlockMessage: Message = {
859
- role: "user",
860
- content: [
861
- { type: "text", text: "First block" },
862
- { type: "text", text: "Second block" },
863
- ],
864
- };
865
-
866
- const result = injectNowScratchpad(multiBlockMessage, "scratchpad notes");
867
- expect(result.content.length).toBe(3);
868
- // Scratchpad is first (no memory_context to skip)
869
- expect(
870
- (result.content[0] as { type: "text"; text: string }).text,
871
- ).toContain("<NOW.md");
872
- expect((result.content[1] as { type: "text"; text: string }).text).toBe(
873
- "First block",
874
- );
875
- expect((result.content[2] as { type: "text"; text: string }).text).toBe(
876
- "Second block",
877
- );
878
- });
879
- });
813
+ // The standalone `injectNowScratchpad` helper was removed in G2.1. The
814
+ // now-md default injector (registered by `defaultInjectorsPlugin`) emits
815
+ // the `<NOW.md>` block as an `after-memory-prefix` placement during
816
+ // `applyRuntimeInjections`. The suites below (`applyRuntimeInjections with
817
+ // nowScratchpad` and the injection-mode tests) cover that behaviour
818
+ // end-to-end.
880
819
 
881
820
  // ---------------------------------------------------------------------------
882
821
  // stripNowScratchpad
@@ -1837,22 +1776,9 @@ describe("buildSubagentStatusBlock", () => {
1837
1776
  });
1838
1777
  });
1839
1778
 
1840
- describe("injectSubagentStatus", () => {
1841
- test("appends status block to user message", () => {
1842
- const msg: Message = {
1843
- role: "user",
1844
- content: [{ type: "text", text: "hello" }],
1845
- };
1846
- const result = injectSubagentStatus(
1847
- msg,
1848
- "<active_subagents>\ntest\n</active_subagents>",
1849
- );
1850
- expect(result.content).toHaveLength(2);
1851
- expect(
1852
- (result.content[1] as { type: string; text: string }).text,
1853
- ).toContain("<active_subagents>");
1854
- });
1855
- });
1779
+ // `injectSubagentStatus` was removed in G2.1 — coverage of the append
1780
+ // placement lives in the `applyRuntimeInjections subagent status` suite
1781
+ // below, which exercises the subagent-status default injector end-to-end.
1856
1782
 
1857
1783
  describe("applyRuntimeInjections — subagent status", () => {
1858
1784
  const userMsg: Message = {
@@ -2702,7 +2628,7 @@ describe("Slack channel chronological rendering — multi-thread", () => {
2702
2628
  },
2703
2629
  {
2704
2630
  role: "assistant",
2705
- content: [{ type: "text", text: "[11/14/23 14:26]: prior reply" }],
2631
+ content: [{ type: "text", text: "prior reply" }],
2706
2632
  },
2707
2633
  ],
2708
2634
  },
@@ -3666,6 +3592,125 @@ describe("assembleSlackActiveThreadFocusBlock", () => {
3666
3592
  expect(result!).toContain("@user");
3667
3593
  });
3668
3594
 
3595
+ test("assistant reactions are not double-attributed (`@assistant: [... @assistant reacted ...]`)", () => {
3596
+ // `renderReaction` bakes `@assistant` into the reaction tag line
3597
+ // (`[11/14/23 14:28 @assistant reacted 👍 to Mxxxxxx]`). The
3598
+ // post-render step that prepends `@assistant: ` to assistant content
3599
+ // lines must skip reaction lines, otherwise the flattened block
3600
+ // produces `@assistant: [... @assistant reacted ...]` — two
3601
+ // attributions for one event.
3602
+ const rows: SlackTranscriptInputRow[] = [
3603
+ buildRow(
3604
+ "user",
3605
+ "Parent",
3606
+ 1_000,
3607
+ buildMeta({ channelTs: PARENT_TS, displayName: "@alice" }),
3608
+ ),
3609
+ // Assistant reply in the thread — gets an `@assistant:` prefix.
3610
+ buildRow(
3611
+ "assistant",
3612
+ "Assistant reply",
3613
+ 2_000,
3614
+ buildMeta({
3615
+ channelTs: "1700000005.000001",
3616
+ threadTs: PARENT_TS,
3617
+ }),
3618
+ ),
3619
+ // Assistant reaction on the parent — must NOT get a second prefix.
3620
+ buildRow(
3621
+ "assistant",
3622
+ "[reaction]",
3623
+ 3_000,
3624
+ buildMeta({
3625
+ channelTs: "1700000008.000002",
3626
+ eventKind: "reaction",
3627
+ reaction: {
3628
+ emoji: "👍",
3629
+ targetChannelTs: PARENT_TS,
3630
+ op: "added",
3631
+ },
3632
+ }),
3633
+ ),
3634
+ // Latest user row in the thread — required for `detectActiveThreadTs`
3635
+ // to lock onto PARENT_TS (the latest user turn is the anchor).
3636
+ buildRow(
3637
+ "user",
3638
+ "User follow-up",
3639
+ 4_000,
3640
+ buildMeta({
3641
+ channelTs: REPLY_TS,
3642
+ threadTs: PARENT_TS,
3643
+ displayName: "@alice",
3644
+ }),
3645
+ ),
3646
+ ];
3647
+ const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
3648
+ expect(result).not.toBeNull();
3649
+ // Double-attribution anti-pattern must NOT appear anywhere.
3650
+ expect(result!).not.toContain("@assistant: [");
3651
+ // Both the reaction attribution and the reply prefix are still present.
3652
+ expect(result!).toContain("@assistant reacted 👍");
3653
+ expect(result!).toContain("@assistant: Assistant reply");
3654
+ });
3655
+
3656
+ test("assistant reaction overflow trailer is not double-attributed", () => {
3657
+ // When assistant reactions overflow the per-target cap, `renderSlackTranscript`
3658
+ // emits a trailer line (`[…and N more reactions to Mxxxxxx]`) whose role
3659
+ // is inherited from the first overflowing reaction — i.e. `assistant`. The
3660
+ // trailer embeds no actor attribution but ends with the parent alias and
3661
+ // shares the same `M<hex>]` signature as a real reaction line, so it must
3662
+ // be detected by `isReactionTagLine` and skipped by the prefix step.
3663
+ const PARENT_ALIAS_TS = PARENT_TS;
3664
+ const buildAssistantReaction = (ts: string, emoji: string) =>
3665
+ buildRow(
3666
+ "assistant",
3667
+ "[reaction]",
3668
+ Number.parseFloat(ts) * 1000,
3669
+ buildMeta({
3670
+ channelTs: ts,
3671
+ eventKind: "reaction",
3672
+ reaction: {
3673
+ emoji,
3674
+ targetChannelTs: PARENT_ALIAS_TS,
3675
+ op: "added",
3676
+ },
3677
+ }),
3678
+ );
3679
+ const rows: SlackTranscriptInputRow[] = [
3680
+ buildRow(
3681
+ "user",
3682
+ "Parent",
3683
+ 1_000,
3684
+ buildMeta({ channelTs: PARENT_TS, displayName: "@alice" }),
3685
+ ),
3686
+ // Overflow the default per-target cap (5) with 7 reactions so the
3687
+ // trailer line is emitted with 2 excess.
3688
+ buildAssistantReaction("1700000100.000001", "👍"),
3689
+ buildAssistantReaction("1700000100.000002", "🎉"),
3690
+ buildAssistantReaction("1700000100.000003", "🔥"),
3691
+ buildAssistantReaction("1700000100.000004", "💯"),
3692
+ buildAssistantReaction("1700000100.000005", "👏"),
3693
+ buildAssistantReaction("1700000100.000006", "👀"),
3694
+ buildAssistantReaction("1700000100.000007", "🚀"),
3695
+ // Latest user row in the thread — required for `detectActiveThreadTs`.
3696
+ buildRow(
3697
+ "user",
3698
+ "Follow-up",
3699
+ 2_000_000,
3700
+ buildMeta({
3701
+ channelTs: REPLY_TS,
3702
+ threadTs: PARENT_TS,
3703
+ displayName: "@alice",
3704
+ }),
3705
+ ),
3706
+ ];
3707
+ const result = assembleSlackActiveThreadFocusBlock(rows, SLACK_CAPS);
3708
+ expect(result).not.toBeNull();
3709
+ expect(result!).toContain("more reactions");
3710
+ // The trailer line must not be double-attributed.
3711
+ expect(result!).not.toMatch(/@assistant: \[…and \d+ more reaction/);
3712
+ });
3713
+
3669
3714
  test("emits a block even when the parent has not been backfilled yet", () => {
3670
3715
  // The inbound reply detects an `activeThreadTs` from its own
3671
3716
  // `threadTs`, but the parent (`channelTs === activeThreadTs`) has not
@@ -3826,7 +3871,7 @@ describe("assembleSlackChronologicalMessages", () => {
3826
3871
  },
3827
3872
  {
3828
3873
  role: "assistant",
3829
- content: [{ type: "text", text: "[11/14/23 14:26]: hi back!" }],
3874
+ content: [{ type: "text", text: "hi back!" }],
3830
3875
  },
3831
3876
  {
3832
3877
  role: "user",
@@ -3877,9 +3922,9 @@ describe("assembleSlackChronologicalMessages", () => {
3877
3922
  expect(result!.map((m) => (m.content[0] as { text: string }).text)).toEqual(
3878
3923
  [
3879
3924
  "[11/14/23 14:25]: old hi",
3880
- "[11/14/23 14:26]: old reply",
3925
+ "old reply",
3881
3926
  "[11/14/23 14:28 @alice]: fresh hi",
3882
- "[11/14/23 14:30]: fresh reply",
3927
+ "fresh reply",
3883
3928
  ],
3884
3929
  );
3885
3930
  expect(result!.map((m) => m.role)).toEqual([
@@ -4035,7 +4080,7 @@ describe("assembleSlackChronologicalMessages", () => {
4035
4080
  expect(rendered[1]!).toEqual({
4036
4081
  role: "assistant",
4037
4082
  content: [
4038
- { type: "text", text: "[11/14/23 14:26]: looking it up" },
4083
+ { type: "text", text: "looking it up" },
4039
4084
  {
4040
4085
  type: "tool_use",
4041
4086
  id: "tu_1",
@@ -4357,11 +4402,11 @@ describe("assembleSlackChronologicalMessages", () => {
4357
4402
  role: "user",
4358
4403
  content: [{ type: "text", text: "[11/14/23 23:03 @alice]: hi" }],
4359
4404
  });
4360
- // Row 2: assistant tag line + tool_use(abc).
4405
+ // Row 2: assistant content + tool_use(abc) — no tag line.
4361
4406
  expect(result![1]).toEqual({
4362
4407
  role: "assistant",
4363
4408
  content: [
4364
- { type: "text", text: "[11/14/23 23:03]: checking..." },
4409
+ { type: "text", text: "checking..." },
4365
4410
  {
4366
4411
  type: "tool_use",
4367
4412
  id: "tu_abc",
@@ -4377,11 +4422,11 @@ describe("assembleSlackChronologicalMessages", () => {
4377
4422
  { type: "tool_result", tool_use_id: "tu_abc", content: "result 1" },
4378
4423
  ],
4379
4424
  });
4380
- // Row 4: assistant tag line + tool_use(def).
4425
+ // Row 4: assistant content + tool_use(def) — no tag line.
4381
4426
  expect(result![3]).toEqual({
4382
4427
  role: "assistant",
4383
4428
  content: [
4384
- { type: "text", text: "[11/14/23 23:03]: one more lookup..." },
4429
+ { type: "text", text: "one more lookup..." },
4385
4430
  {
4386
4431
  type: "tool_use",
4387
4432
  id: "tu_def",
@@ -4397,10 +4442,10 @@ describe("assembleSlackChronologicalMessages", () => {
4397
4442
  { type: "tool_result", tool_use_id: "tu_def", content: "result 2" },
4398
4443
  ],
4399
4444
  });
4400
- // Row 6: assistant final text-only answer, rendered as tag line only.
4445
+ // Row 6: assistant final text-only answer, content-only (no tag line).
4401
4446
  expect(result![5]).toEqual({
4402
4447
  role: "assistant",
4403
- content: [{ type: "text", text: "[11/14/23 23:03]: all done" }],
4448
+ content: [{ type: "text", text: "all done" }],
4404
4449
  });
4405
4450
  // Row 7: user follow-up tag line.
4406
4451
  expect(result![6]).toEqual({
@@ -1,9 +1,11 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { beforeEach, describe, expect, test } from "bun:test";
2
2
 
3
+ import { applyRuntimeInjections } from "../daemon/conversation-runtime-assembly.js";
4
+ import { defaultInjectorsPlugin } from "../plugins/defaults/injectors.js";
3
5
  import {
4
- applyRuntimeInjections,
5
- injectWorkspaceTopLevelContext,
6
- } from "../daemon/conversation-runtime-assembly.js";
6
+ registerPlugin,
7
+ resetPluginRegistryForTests,
8
+ } from "../plugins/registry.js";
7
9
  import type { Message } from "../providers/types.js";
8
10
 
9
11
  // ---------------------------------------------------------------------------
@@ -21,43 +23,21 @@ function userMsg(text: string): Message {
21
23
  const sampleContext =
22
24
  "<workspace>\nRoot: /sandbox\nDirectories: src, lib, tests\n</workspace>";
23
25
 
24
- describe("Workspace top-level context injection", () => {
25
- test("prepends workspace block to user message content", () => {
26
- const original = userMsg("Hello");
27
- const injected = injectWorkspaceTopLevelContext(original, sampleContext);
26
+ // The standalone `injectWorkspaceTopLevelContext` helper was removed in
27
+ // G2.1. The workspace-context default injector (registered by
28
+ // `defaultInjectorsPlugin`) now emits the workspace block as a
29
+ // `prepend-user-tail` placement during `applyRuntimeInjections`. The suite
30
+ // below exercises that end-to-end path instead.
28
31
 
29
- expect(injected.content).toHaveLength(2);
30
- expect(injected.content[0]).toEqual({ type: "text", text: sampleContext });
31
- expect(injected.content[1]).toEqual({ type: "text", text: "Hello" });
32
- });
33
-
34
- test("preserves multi-block user content after prepend", () => {
35
- const original: Message = {
36
- role: "user",
37
- content: [
38
- { type: "text", text: "First" },
39
- { type: "text", text: "Second" },
40
- ],
41
- };
42
- const injected = injectWorkspaceTopLevelContext(original, sampleContext);
43
-
44
- expect(injected.content).toHaveLength(3);
45
- expect(injected.content[0].type).toBe("text");
46
- expect((injected.content[0] as { text: string }).text).toBe(sampleContext);
47
- expect((injected.content[1] as { text: string }).text).toBe("First");
48
- expect((injected.content[2] as { text: string }).text).toBe("Second");
49
- });
50
-
51
- test("does not mutate original message", () => {
52
- const original = userMsg("Hello");
53
- const originalContentLength = original.content.length;
54
- injectWorkspaceTopLevelContext(original, sampleContext);
55
-
56
- expect(original.content).toHaveLength(originalContentLength);
32
+ describe("applyRuntimeInjections — workspace top-level context", () => {
33
+ beforeEach(() => {
34
+ // Post-G2.1: workspace injection is driven by the `workspace-context`
35
+ // default injector, so the plugin must be registered for the chain to
36
+ // produce a block. Each test gets a clean registry.
37
+ resetPluginRegistryForTests();
38
+ registerPlugin(defaultInjectorsPlugin);
57
39
  });
58
- });
59
40
 
60
- describe("applyRuntimeInjections — workspace top-level context", () => {
61
41
  test("injects workspace context when provided", async () => {
62
42
  const messages: Message[] = [userMsg("Hello")];
63
43
  const { messages: result } = await applyRuntimeInjections(messages, {
@@ -100,6 +80,11 @@ describe("applyRuntimeInjections — workspace top-level context", () => {
100
80
  });
101
81
 
102
82
  describe("applyRuntimeInjections — minimal mode skips workspace blocks", () => {
83
+ beforeEach(() => {
84
+ resetPluginRegistryForTests();
85
+ registerPlugin(defaultInjectorsPlugin);
86
+ });
87
+
103
88
  test("minimal mode skips workspace top-level context", async () => {
104
89
  const messages: Message[] = [userMsg("Hello")];
105
90
  const { messages: result } = await applyRuntimeInjections(messages, {
@@ -411,14 +411,14 @@ describe("composeConversationSeed", () => {
411
411
  });
412
412
 
413
413
  test('falls back to event name when title is "Notification" and body is empty', () => {
414
- const signal = makeSignal({ sourceEventName: "schedule.complete" });
414
+ const signal = makeSignal({ sourceEventName: "watcher.notification" });
415
415
  const copy = makeCopy({ title: "Notification", body: "" });
416
416
  const seed = composeConversationSeed(
417
417
  signal,
418
418
  "vellum" as NotificationChannel,
419
419
  copy,
420
420
  );
421
- expect(seed).toBe("Schedule complete");
421
+ expect(seed).toBe("Watcher notification");
422
422
  });
423
423
 
424
424
  test('uses context payload "message" field in fallback when available', () => {
@@ -30,7 +30,7 @@ mock.module("../providers/registry.js", () => ({
30
30
  mock.module("../config/loader.js", () => ({
31
31
  getConfig: () => ({
32
32
  ui: {},
33
-
33
+
34
34
  llm: {
35
35
  default: {
36
36
  provider: "mock-provider",
@@ -207,6 +207,9 @@ mock.module("../agent/loop.js", () => ({
207
207
  getToolTokenBudget() {
208
208
  return 0;
209
209
  }
210
+ getResolvedTools() {
211
+ return [];
212
+ }
210
213
  getActiveModel() {
211
214
  return undefined;
212
215
  }
@@ -215,7 +218,9 @@ mock.module("../agent/loop.js", () => ({
215
218
  onEvent: (event: AgentEvent) => void,
216
219
  _signal?: AbortSignal,
217
220
  _requestId?: string,
218
- _onCheckpoint?: (checkpoint: CheckpointInfo) => CheckpointDecision,
221
+ _onCheckpoint?: (
222
+ checkpoint: CheckpointInfo,
223
+ ) => CheckpointDecision | Promise<CheckpointDecision>,
219
224
  ): Promise<Message[]> {
220
225
  return new Promise<Message[]>((resolve) => {
221
226
  pendingRuns.push({ resolve, messages, onEvent });