@vellumai/assistant 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -0,0 +1,160 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ // Stub the in-process SSE hub so the writer's publish path is a
7
+ // no-op in these tests.
8
+ const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
9
+
10
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
11
+ assistantEventHub: {
12
+ publish: publishSpy,
13
+ subscribe: () => () => {},
14
+ },
15
+ }));
16
+
17
+ // Stub workspace prompt reads so the heartbeat service doesn't try to
18
+ // read real workspace files.
19
+ mock.module("../../util/platform.js", () => ({
20
+ getWorkspaceDir: () => workspaceDir,
21
+ getWorkspacePromptPath: (name: string) => join(workspaceDir, name),
22
+ vellumRoot: () => workspaceDir,
23
+ getDataDir: () => join(workspaceDir, "data"),
24
+ }));
25
+
26
+ // Stub config so heartbeat is enabled.
27
+ mock.module("../../config/loader.js", () => ({
28
+ getConfig: () => ({
29
+ heartbeat: {
30
+ enabled: true,
31
+ intervalMs: 60_000,
32
+ activeHoursStart: null,
33
+ activeHoursEnd: null,
34
+ },
35
+ }),
36
+ }));
37
+
38
+ // Stub conversation bootstrap.
39
+ const lastConversationId = "conv-heartbeat-test";
40
+ mock.module("../../memory/conversation-bootstrap.js", () => ({
41
+ bootstrapConversation: () => ({ id: lastConversationId }),
42
+ }));
43
+
44
+ // Stub prompt helpers.
45
+ mock.module("../../prompts/persona-resolver.js", () => ({
46
+ GUARDIAN_PERSONA_TEMPLATE: "",
47
+ resolveGuardianPersona: () => null,
48
+ }));
49
+ mock.module("../../prompts/system-prompt.js", () => ({
50
+ isTemplateContent: () => false,
51
+ }));
52
+
53
+ const { getHomeFeedPath } = await import("../../home/feed-writer.js");
54
+ const { HeartbeatService } = await import("../heartbeat-service.js");
55
+
56
+ interface OnDiskItem {
57
+ id: string;
58
+ type: string;
59
+ source?: string;
60
+ title: string;
61
+ summary: string;
62
+ priority: number;
63
+ status: string;
64
+ author: string;
65
+ urgency?: string;
66
+ }
67
+
68
+ function readFeedItems(): OnDiskItem[] {
69
+ const raw = JSON.parse(readFileSync(getHomeFeedPath(), "utf-8"));
70
+ return raw.items as OnDiskItem[];
71
+ }
72
+
73
+ let workspaceDir: string;
74
+ let origWorkspaceDir: string | undefined;
75
+
76
+ beforeEach(() => {
77
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-hb-feed-"));
78
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
79
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
80
+ publishSpy.mockClear();
81
+ });
82
+
83
+ afterEach(() => {
84
+ if (origWorkspaceDir === undefined) {
85
+ delete process.env.VELLUM_WORKSPACE_DIR;
86
+ } else {
87
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
88
+ }
89
+ try {
90
+ rmSync(workspaceDir, { recursive: true, force: true });
91
+ } catch {
92
+ // best-effort
93
+ }
94
+ });
95
+
96
+ describe("heartbeat feed events", () => {
97
+ test("successful heartbeat emits feed event with priority 30 and no urgency", async () => {
98
+ const service = new HeartbeatService({
99
+ processMessage: async () => ({ messageId: "msg-1" }),
100
+ alerter: () => {},
101
+ });
102
+
103
+ await service.runOnce({ force: true });
104
+
105
+ // Give the fire-and-forget emitFeedEvent time to flush.
106
+ await new Promise((r) => setTimeout(r, 100));
107
+
108
+ const items = readFeedItems();
109
+ const heartbeatItem = items.find((i) => i.title === "Heartbeat");
110
+ expect(heartbeatItem).toBeDefined();
111
+ expect(heartbeatItem!.summary).toBe("Heartbeat check completed.");
112
+ expect(heartbeatItem!.priority).toBe(30);
113
+ expect(heartbeatItem!.urgency).toBeUndefined();
114
+ expect(heartbeatItem!.source).toBe("assistant");
115
+ });
116
+
117
+ test("failed heartbeat emits feed event with priority 55 and urgency medium", async () => {
118
+ const service = new HeartbeatService({
119
+ processMessage: async () => {
120
+ throw new Error("LLM call failed");
121
+ },
122
+ alerter: () => {},
123
+ });
124
+
125
+ await service.runOnce({ force: true });
126
+
127
+ // Give the fire-and-forget emitFeedEvent time to flush.
128
+ await new Promise((r) => setTimeout(r, 100));
129
+
130
+ const items = readFeedItems();
131
+ const heartbeatItem = items.find((i) => i.title === "Heartbeat");
132
+ expect(heartbeatItem).toBeDefined();
133
+ expect(heartbeatItem!.summary).toBe(
134
+ "Heartbeat check failed. Check logs for details.",
135
+ );
136
+ expect(heartbeatItem!.priority).toBe(55);
137
+ expect(heartbeatItem!.urgency).toBe("medium");
138
+ expect(heartbeatItem!.source).toBe("assistant");
139
+ });
140
+
141
+ test("dedupKey uses date for daily dedup", async () => {
142
+ const service = new HeartbeatService({
143
+ processMessage: async () => ({ messageId: "msg-1" }),
144
+ alerter: () => {},
145
+ });
146
+
147
+ // Run twice — same day should dedup to one item.
148
+ await service.runOnce({ force: true });
149
+ await new Promise((r) => setTimeout(r, 100));
150
+ await service.runOnce({ force: true });
151
+ await new Promise((r) => setTimeout(r, 100));
152
+
153
+ const items = readFeedItems();
154
+ const heartbeatItems = items.filter((i) => i.title === "Heartbeat");
155
+ expect(heartbeatItems).toHaveLength(1);
156
+
157
+ const today = new Date().toISOString().split("T")[0];
158
+ expect(heartbeatItems[0]!.id).toBe(`emit:assistant:heartbeat:ok:${today}`);
159
+ });
160
+ });
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { getConfig } from "../config/loader.js";
5
5
  import type { LLMCallSite } from "../config/schemas/llm.js";
6
6
  import type { HeartbeatAlert } from "../daemon/message-protocol.js";
7
+ import { emitFeedEvent } from "../home/emit-feed-event.js";
7
8
  import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
8
9
  import {
9
10
  GUARDIAN_PERSONA_TEMPLATE,
@@ -396,6 +397,19 @@ export class HeartbeatService {
396
397
  }
397
398
 
398
399
  log.info({ conversationId: conversation.id }, "Heartbeat completed");
400
+
401
+ void emitFeedEvent({
402
+ source: "assistant",
403
+ title: "Heartbeat",
404
+ summary: "Heartbeat check completed.",
405
+ dedupKey: `heartbeat:ok:${new Date().toISOString().split("T")[0]}`,
406
+ priority: 30,
407
+ }).catch((err) => {
408
+ log.warn(
409
+ { err, conversationId: conversation.id },
410
+ "Failed to emit heartbeat feed event",
411
+ );
412
+ });
399
413
  } catch (err) {
400
414
  log.error({ err }, "Heartbeat failed");
401
415
  try {
@@ -407,6 +421,15 @@ export class HeartbeatService {
407
421
  } catch (alertErr) {
408
422
  log.error({ alertErr }, "Failed to broadcast heartbeat alert");
409
423
  }
424
+
425
+ void emitFeedEvent({
426
+ source: "assistant",
427
+ title: "Heartbeat",
428
+ summary: "Heartbeat check failed. Check logs for details.",
429
+ dedupKey: `heartbeat:fail:${new Date().toISOString().split("T")[0]}`,
430
+ priority: 55,
431
+ urgency: "medium",
432
+ }).catch(() => {});
410
433
  }
411
434
  }
412
435
 
@@ -431,7 +454,7 @@ ${checklist}
431
454
  if (unhealthyProviders.length > 0) {
432
455
  const providers = unhealthyProviders.join(", ");
433
456
  prompt += `\n\n<credential-status>
434
- The following providers have broken or expired OAuth credentials: ${providers}.
457
+ The following providers have broken or expired credentials: ${providers}.
435
458
  Do NOT attempt to use tools for these providers — they will fail. Skip any checklist items that depend on them and note the outage in your summary.
436
459
  </credential-status>`;
437
460
  }
@@ -0,0 +1,312 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
5
+
6
+ // Stub the in-process SSE hub so the writer's publish path is a
7
+ // no-op in these tests. Must be in place before the writer module is
8
+ // imported (directly or transitively) so the dynamic import below
9
+ // picks it up.
10
+ const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
11
+
12
+ mock.module("../../runtime/assistant-event-hub.js", () => ({
13
+ assistantEventHub: {
14
+ publish: publishSpy,
15
+ subscribe: () => () => {},
16
+ },
17
+ }));
18
+
19
+ const { emitFeedEvent } = await import("../emit-feed-event.js");
20
+ const { readHomeFeed, MAX_ACTIONS_PER_SOURCE } =
21
+ await import("../feed-writer.js");
22
+
23
+ type FeedItemSource = "gmail" | "slack" | "calendar" | "assistant";
24
+
25
+ const ALL_SOURCES: FeedItemSource[] = [
26
+ "gmail",
27
+ "slack",
28
+ "calendar",
29
+ "assistant",
30
+ ];
31
+
32
+ let workspaceDir: string;
33
+ let origWorkspaceDir: string | undefined;
34
+
35
+ beforeEach(() => {
36
+ workspaceDir = mkdtempSync(join(tmpdir(), "vellum-feed-integ-"));
37
+ origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
38
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
39
+ publishSpy.mockClear();
40
+ });
41
+
42
+ afterEach(() => {
43
+ if (origWorkspaceDir === undefined) {
44
+ delete process.env.VELLUM_WORKSPACE_DIR;
45
+ } else {
46
+ process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
47
+ }
48
+ try {
49
+ rmSync(workspaceDir, { recursive: true, force: true });
50
+ } catch {
51
+ // best-effort
52
+ }
53
+ });
54
+
55
+ describe("feed population integration", () => {
56
+ test("items from all 4 sources coexist and are sorted by priority", async () => {
57
+ // Emit one item per source with distinct priorities.
58
+ await emitFeedEvent({
59
+ source: "gmail",
60
+ title: "Gmail action",
61
+ summary: "Replied to a thread.",
62
+ priority: 30,
63
+ dedupKey: "gmail-1",
64
+ });
65
+ await emitFeedEvent({
66
+ source: "slack",
67
+ title: "Slack action",
68
+ summary: "Sent a message in #general.",
69
+ priority: 70,
70
+ dedupKey: "slack-1",
71
+ });
72
+ await emitFeedEvent({
73
+ source: "calendar",
74
+ title: "Calendar action",
75
+ summary: "Meeting prep reminder.",
76
+ priority: 50,
77
+ dedupKey: "cal-1",
78
+ });
79
+ await emitFeedEvent({
80
+ source: "assistant",
81
+ title: "Assistant action",
82
+ summary: "Ran weekly review.",
83
+ priority: 90,
84
+ dedupKey: "asst-1",
85
+ });
86
+
87
+ const feed = readHomeFeed();
88
+ expect(feed.items).toHaveLength(4);
89
+
90
+ // Verify all four sources are present.
91
+ const sources = new Set(feed.items.map((i) => i.source));
92
+ for (const s of ALL_SOURCES) {
93
+ expect(sources.has(s)).toBe(true);
94
+ }
95
+
96
+ // Items should be sorted by priority DESC.
97
+ const priorities = feed.items.map((i) => i.priority);
98
+ for (let i = 1; i < priorities.length; i++) {
99
+ expect(priorities[i - 1]!).toBeGreaterThanOrEqual(priorities[i]!);
100
+ }
101
+
102
+ // Spot-check the ordering: assistant (90) first, gmail (30) last.
103
+ expect(feed.items[0]!.source).toBe("assistant");
104
+ expect(feed.items[feed.items.length - 1]!.source).toBe("gmail");
105
+ });
106
+
107
+ test("per-source cap holds across mixed sources", async () => {
108
+ // Emit MAX + 5 items for gmail and MAX + 5 for slack, plus a
109
+ // handful from calendar and assistant. Only the capped sources
110
+ // should be pruned.
111
+ const overflow = MAX_ACTIONS_PER_SOURCE + 5;
112
+
113
+ for (let i = 0; i < overflow; i++) {
114
+ await emitFeedEvent({
115
+ source: "gmail",
116
+ title: `Gmail item ${i}`,
117
+ summary: `Gmail summary ${i}`,
118
+ priority: 50,
119
+ });
120
+ }
121
+
122
+ for (let i = 0; i < overflow; i++) {
123
+ await emitFeedEvent({
124
+ source: "slack",
125
+ title: `Slack item ${i}`,
126
+ summary: `Slack summary ${i}`,
127
+ priority: 50,
128
+ });
129
+ }
130
+
131
+ // A few items from the other two sources — should be untouched.
132
+ for (let i = 0; i < 3; i++) {
133
+ await emitFeedEvent({
134
+ source: "calendar",
135
+ title: `Calendar item ${i}`,
136
+ summary: `Calendar summary ${i}`,
137
+ priority: 50,
138
+ });
139
+ }
140
+ for (let i = 0; i < 2; i++) {
141
+ await emitFeedEvent({
142
+ source: "assistant",
143
+ title: `Assistant item ${i}`,
144
+ summary: `Assistant summary ${i}`,
145
+ priority: 50,
146
+ });
147
+ }
148
+
149
+ const feed = readHomeFeed();
150
+
151
+ const gmailItems = feed.items.filter((i) => i.source === "gmail");
152
+ const slackItems = feed.items.filter((i) => i.source === "slack");
153
+ const calendarItems = feed.items.filter((i) => i.source === "calendar");
154
+ const assistantItems = feed.items.filter((i) => i.source === "assistant");
155
+
156
+ expect(gmailItems).toHaveLength(MAX_ACTIONS_PER_SOURCE);
157
+ expect(slackItems).toHaveLength(MAX_ACTIONS_PER_SOURCE);
158
+ expect(calendarItems).toHaveLength(3);
159
+ expect(assistantItems).toHaveLength(2);
160
+ });
161
+
162
+ test("expired items are filtered out at read time", async () => {
163
+ // One item that expired in the past, one that is still valid.
164
+ const pastDate = new Date(Date.now() - 60_000).toISOString();
165
+ const futureDate = new Date(Date.now() + 3_600_000).toISOString();
166
+
167
+ await emitFeedEvent({
168
+ source: "gmail",
169
+ title: "Expired item",
170
+ summary: "Should not appear.",
171
+ expiresAt: pastDate,
172
+ dedupKey: "expired-1",
173
+ });
174
+ await emitFeedEvent({
175
+ source: "slack",
176
+ title: "Still valid",
177
+ summary: "Should appear.",
178
+ expiresAt: futureDate,
179
+ dedupKey: "valid-1",
180
+ });
181
+ await emitFeedEvent({
182
+ source: "calendar",
183
+ title: "No expiry",
184
+ summary: "Should also appear.",
185
+ dedupKey: "no-expiry-1",
186
+ });
187
+
188
+ const feed = readHomeFeed();
189
+ expect(feed.items).toHaveLength(2);
190
+
191
+ const titles = feed.items.map((i) => i.title);
192
+ expect(titles).toContain("Still valid");
193
+ expect(titles).toContain("No expiry");
194
+ expect(titles).not.toContain("Expired item");
195
+ });
196
+
197
+ test("dedup: two items with the same dedupKey produce only one entry", async () => {
198
+ await emitFeedEvent({
199
+ source: "gmail",
200
+ title: "First version",
201
+ summary: "Original summary.",
202
+ dedupKey: "shared-key",
203
+ });
204
+ await emitFeedEvent({
205
+ source: "gmail",
206
+ title: "Second version",
207
+ summary: "Updated summary.",
208
+ dedupKey: "shared-key",
209
+ });
210
+
211
+ const feed = readHomeFeed();
212
+ const matching = feed.items.filter((i) => i.id === "emit:gmail:shared-key");
213
+ expect(matching).toHaveLength(1);
214
+ expect(matching[0]!.title).toBe("Second version");
215
+ expect(matching[0]!.summary).toBe("Updated summary.");
216
+ });
217
+
218
+ test("dedup works across reads — no phantom duplicates", async () => {
219
+ await emitFeedEvent({
220
+ source: "assistant",
221
+ title: "Version 1",
222
+ summary: "First emit.",
223
+ dedupKey: "cross-read",
224
+ });
225
+
226
+ // Read once to confirm the item is there.
227
+ const feed1 = readHomeFeed();
228
+ expect(
229
+ feed1.items.filter((i) => i.id === "emit:assistant:cross-read"),
230
+ ).toHaveLength(1);
231
+
232
+ // Emit again with the same dedupKey.
233
+ await emitFeedEvent({
234
+ source: "assistant",
235
+ title: "Version 2",
236
+ summary: "Second emit.",
237
+ dedupKey: "cross-read",
238
+ });
239
+
240
+ const feed2 = readHomeFeed();
241
+ const matching = feed2.items.filter(
242
+ (i) => i.id === "emit:assistant:cross-read",
243
+ );
244
+ expect(matching).toHaveLength(1);
245
+ expect(matching[0]!.title).toBe("Version 2");
246
+ });
247
+
248
+ test("mixed priorities and urgencies sort correctly", async () => {
249
+ const events: Array<{
250
+ source: FeedItemSource;
251
+ title: string;
252
+ priority: number;
253
+ dedupKey: string;
254
+ }> = [
255
+ {
256
+ source: "gmail",
257
+ title: "Low priority gmail",
258
+ priority: 10,
259
+ dedupKey: "g-low",
260
+ },
261
+ {
262
+ source: "slack",
263
+ title: "High priority slack",
264
+ priority: 95,
265
+ dedupKey: "s-high",
266
+ },
267
+ {
268
+ source: "calendar",
269
+ title: "Mid priority calendar",
270
+ priority: 50,
271
+ dedupKey: "c-mid",
272
+ },
273
+ {
274
+ source: "assistant",
275
+ title: "High priority assistant",
276
+ priority: 95,
277
+ dedupKey: "a-high",
278
+ },
279
+ {
280
+ source: "gmail",
281
+ title: "Mid priority gmail",
282
+ priority: 50,
283
+ dedupKey: "g-mid",
284
+ },
285
+ {
286
+ source: "slack",
287
+ title: "Low priority slack",
288
+ priority: 20,
289
+ dedupKey: "s-low",
290
+ },
291
+ ];
292
+
293
+ for (const e of events) {
294
+ await emitFeedEvent({
295
+ source: e.source,
296
+ title: e.title,
297
+ summary: `Summary for ${e.title}`,
298
+ priority: e.priority,
299
+ dedupKey: e.dedupKey,
300
+ });
301
+ }
302
+
303
+ const feed = readHomeFeed();
304
+ expect(feed.items).toHaveLength(6);
305
+
306
+ // Verify descending priority order.
307
+ const priorities = feed.items.map((i) => i.priority);
308
+ for (let i = 1; i < priorities.length; i++) {
309
+ expect(priorities[i - 1]!).toBeGreaterThanOrEqual(priorities[i]!);
310
+ }
311
+ });
312
+ });
@@ -41,6 +41,7 @@ import { randomUUID } from "node:crypto";
41
41
  import {
42
42
  type FeedAction,
43
43
  type FeedItem,
44
+ type FeedItemDetailPanel,
44
45
  feedItemSchema,
45
46
  type FeedItemSource,
46
47
  type FeedItemUrgency,
@@ -96,6 +97,10 @@ export interface EmitFeedEventParams {
96
97
  expiresAt?: string;
97
98
  /** Visual urgency treatment — controls badge color independently of sort priority. */
98
99
  urgency?: FeedItemUrgency;
100
+ /** Optional conversation this feed item is associated with. */
101
+ conversationId?: string;
102
+ /** Server-driven detail panel descriptor; when present, the client opens this panel kind. */
103
+ detailPanel?: FeedItemDetailPanel;
99
104
  }
100
105
 
101
106
  /**
@@ -148,6 +153,8 @@ export async function emitFeedEvent(
148
153
  createdAt: now,
149
154
  actions: params.actions,
150
155
  urgency: params.urgency,
156
+ conversationId: params.conversationId,
157
+ detailPanel: params.detailPanel,
151
158
  minTimeAway: params.minTimeAway,
152
159
  expiresAt: params.expiresAt,
153
160
  };
@@ -34,7 +34,12 @@ export type FeedItemStatus = "new" | "seen" | "acted_on" | "dismissed";
34
34
  * stays exhaustive. Future sources will be added explicitly rather
35
35
  * than letting arbitrary strings slip through.
36
36
  */
37
- export type FeedItemSource = "gmail" | "slack" | "calendar" | "assistant";
37
+ export type FeedItemSource =
38
+ | "gmail"
39
+ | "slack"
40
+ | "calendar"
41
+ | "assistant"
42
+ | "telegram";
38
43
 
39
44
  /**
40
45
  * Internal field used by the hybrid authoring resolver (PR 5 writer).
@@ -62,6 +67,20 @@ export interface FeedAction {
62
67
  prompt: string;
63
68
  }
64
69
 
70
+ /** Which detail panel the macOS client should open for this feed item. */
71
+ export type FeedItemDetailPanelKind =
72
+ | "emailDraft"
73
+ | "documentPreview"
74
+ | "permissionChat"
75
+ | "paymentAuth"
76
+ | "toolPermission"
77
+ | "updatesList";
78
+
79
+ /** Server-driven detail panel descriptor attached to a feed item. */
80
+ export interface FeedItemDetailPanel {
81
+ kind: FeedItemDetailPanelKind;
82
+ }
83
+
65
84
  /**
66
85
  * A single item rendered in the Home feed.
67
86
  *
@@ -81,7 +100,7 @@ export interface FeedItem {
81
100
  priority: number;
82
101
  title: string;
83
102
  summary: string;
84
- /** Optional; when present must be one of the four v1 sources. */
103
+ /** Optional; when present must be one of the v1 sources. */
85
104
  source?: FeedItemSource;
86
105
  /** Event time (ISO-8601). */
87
106
  timestamp: string;
@@ -94,6 +113,10 @@ export interface FeedItem {
94
113
  actions?: FeedAction[];
95
114
  /** Visual urgency treatment — controls badge color independently of sort priority. */
96
115
  urgency?: FeedItemUrgency;
116
+ /** Optional conversation this feed item is associated with. */
117
+ conversationId?: string;
118
+ /** Server-driven detail panel descriptor; when present, the client opens this panel kind. */
119
+ detailPanel?: FeedItemDetailPanel;
97
120
  /** Internal: who authored this item. */
98
121
  author: FeedItemAuthor;
99
122
  /** Internal: ISO-8601 writer-record time, used for ordering + TTL. */
@@ -157,6 +180,7 @@ const feedItemSourceSchema = z.enum([
157
180
  "slack",
158
181
  "calendar",
159
182
  "assistant",
183
+ "telegram",
160
184
  ]);
161
185
 
162
186
  const feedItemAuthorSchema = z.enum(["assistant", "platform"]);
@@ -169,6 +193,19 @@ const feedActionSchema = z.object({
169
193
  prompt: z.string(),
170
194
  });
171
195
 
196
+ const feedItemDetailPanelKindSchema = z.enum([
197
+ "emailDraft",
198
+ "documentPreview",
199
+ "permissionChat",
200
+ "paymentAuth",
201
+ "toolPermission",
202
+ "updatesList",
203
+ ]);
204
+
205
+ const feedItemDetailPanelSchema = z.object({
206
+ kind: feedItemDetailPanelKindSchema,
207
+ });
208
+
172
209
  /**
173
210
  * Schema for a single `FeedItem`.
174
211
  *
@@ -196,6 +233,8 @@ export const feedItemSchema = z.object({
196
233
  minTimeAway: z.number().int().min(0).optional(),
197
234
  actions: z.array(feedActionSchema).optional(),
198
235
  urgency: feedItemUrgencySchema.optional(),
236
+ conversationId: z.string().optional(),
237
+ detailPanel: feedItemDetailPanelSchema.optional(),
199
238
  author: feedItemAuthorSchema,
200
239
  createdAt: z.string(),
201
240
  });
@@ -0,0 +1,66 @@
1
+ import { getConfiguredProvider } from "../providers/provider-send-message.js";
2
+ import { getLogger } from "../util/logger.js";
3
+
4
+ const log = getLogger("command-preview-rewriter");
5
+ const REWRITE_TIMEOUT_MS = 3000;
6
+ const REWRITE_MAX_TOKENS = 100;
7
+
8
+ const SYSTEM_PROMPT = `You rewrite technical computer commands into simple, human-readable descriptions.
9
+ Output ONLY the rewritten description — no quotes, no explanation, no preamble.
10
+ Keep it under 15 words. Use plain language a non-technical person would understand.
11
+ Examples:
12
+ - "ls -la ~/Desktop" → "View files on the desktop"
13
+ - "cat ~/.bashrc" → "Read shell configuration file"
14
+ - "rm -rf /tmp/cache" → "Delete temporary cache files"
15
+ - "grep -r 'password' ." → "Search files for the word 'password'"
16
+ - "curl https://api.example.com/users" → "Fetch user data from an API"`;
17
+
18
+ export async function rewriteCommandPreview(
19
+ toolName: string,
20
+ commandPreview: string,
21
+ ): Promise<string | null> {
22
+ try {
23
+ const provider = await getConfiguredProvider("feedEventCopy");
24
+ if (!provider) return null;
25
+
26
+ const response = await provider.sendMessage(
27
+ [
28
+ {
29
+ role: "user",
30
+ content: [
31
+ {
32
+ type: "text",
33
+ text: `Tool: ${toolName}\nCommand: ${commandPreview}`,
34
+ },
35
+ ],
36
+ },
37
+ ],
38
+ [],
39
+ SYSTEM_PROMPT,
40
+ {
41
+ config: {
42
+ max_tokens: REWRITE_MAX_TOKENS,
43
+ callSite: "feedEventCopy",
44
+ },
45
+ signal: AbortSignal.timeout(REWRITE_TIMEOUT_MS),
46
+ },
47
+ );
48
+
49
+ const block = response.content.find((entry) => entry.type === "text");
50
+ const text =
51
+ block && "text" in block ? (block as { text: string }).text.trim() : "";
52
+ if (!text) return null;
53
+ return (
54
+ text
55
+ .replace(/^["'`]+/, "")
56
+ .replace(/["'`]+$/, "")
57
+ .trim() || null
58
+ );
59
+ } catch (err) {
60
+ log.warn(
61
+ { err, toolName, commandPreview },
62
+ "Command preview rewrite failed",
63
+ );
64
+ return null;
65
+ }
66
+ }