@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -9,8 +9,6 @@ import {
9
9
  shouldLogDiskPressureBackgroundSkip,
10
10
  } from "../daemon/disk-pressure-background-gate.js";
11
11
  import type { HeartbeatAlert } from "../daemon/message-protocol.js";
12
- import { getConversation, getMessages } from "../memory/conversation-crud.js";
13
- import { GENERATING_TITLE } from "../memory/conversation-title-service.js";
14
12
  import { emitNotificationSignal } from "../notifications/emit-signal.js";
15
13
  import {
16
14
  GUARDIAN_PERSONA_TEMPLATE,
@@ -47,9 +45,6 @@ const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still ac
47
45
  const EARLY_HEARTBEAT_THRESHOLD = 3;
48
46
  const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
49
47
  const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
50
- const HEARTBEAT_ALERT_MARKER = "HEARTBEAT_ALERT";
51
- const HEARTBEAT_OK_MARKER = "HEARTBEAT_OK";
52
- const HEARTBEAT_ALERT_SUMMARY_MAX_CHARS = 700;
53
48
 
54
49
  // Stripped-comment form of the guardian persona scaffold. Computed
55
50
  // once at module load because stripping comment lines is deterministic
@@ -102,69 +97,6 @@ function recordReengagementTimestamp(): void {
102
97
  }
103
98
  }
104
99
 
105
- type HeartbeatDisposition = "alert" | "ok" | "unknown";
106
-
107
- function parseHeartbeatDisposition(text: string | null): HeartbeatDisposition {
108
- if (!text) return "unknown";
109
- const lines = text
110
- .trim()
111
- .split(/\r?\n/)
112
- .map((line) => line.trim())
113
- .filter((line) => line.length > 0);
114
- const lastLine = lines.at(-1);
115
- if (lastLine === HEARTBEAT_ALERT_MARKER) return "alert";
116
- if (lastLine === HEARTBEAT_OK_MARKER) return "ok";
117
- return "unknown";
118
- }
119
-
120
- function stripHeartbeatDispositionMarkers(text: string): string {
121
- return text
122
- .replace(
123
- new RegExp(
124
- `(?:\\r?\\n)?\\s*(?:${HEARTBEAT_ALERT_MARKER}|${HEARTBEAT_OK_MARKER})\\s*$`,
125
- ),
126
- "",
127
- )
128
- .trim();
129
- }
130
-
131
- function truncateSummary(text: string, maxChars: number): string {
132
- if (text.length <= maxChars) return text;
133
- return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
134
- }
135
-
136
- function buildHeartbeatAlertSummary(text: string | null): string {
137
- const summary = text ? stripHeartbeatDispositionMarkers(text) : "";
138
- return truncateSummary(
139
- summary || "Your assistant found something worth your attention.",
140
- HEARTBEAT_ALERT_SUMMARY_MAX_CHARS,
141
- );
142
- }
143
-
144
- function extractVisibleTextFromStoredMessageContent(raw: string): string {
145
- try {
146
- const parsed = JSON.parse(raw) as unknown;
147
- if (typeof parsed === "string") return parsed;
148
- if (!Array.isArray(parsed)) return "";
149
- const texts: string[] = [];
150
- for (const block of parsed) {
151
- if (
152
- block != null &&
153
- typeof block === "object" &&
154
- "type" in block &&
155
- block.type === "text" &&
156
- "text" in block &&
157
- typeof block.text === "string"
158
- ) {
159
- texts.push(block.text);
160
- }
161
- }
162
- return texts.join("\n").trim();
163
- } catch {
164
- return raw;
165
- }
166
- }
167
-
168
100
  export interface HeartbeatDeps {
169
101
  alerter: (alert: HeartbeatAlert) => void;
170
102
  onConversationCreated?: (info: {
@@ -202,6 +134,9 @@ export class HeartbeatService {
202
134
  // Reset by resetTimer (guardian message), reconfigure, and stop. Force runs
203
135
  // bypass the cap and do not increment.
204
136
  private _consecutiveRuns = 0;
137
+ // Bumped every time the counter is reset so an in-flight run that finishes
138
+ // after a guardian message can detect the reset and skip its increment.
139
+ private _resetGeneration = 0;
205
140
 
206
141
  constructor(deps: HeartbeatDeps) {
207
142
  this.deps = deps;
@@ -360,6 +295,7 @@ export class HeartbeatService {
360
295
  /** Restart the timer with the latest config (e.g. after settings change). */
361
296
  reconfigure(): void {
362
297
  this._consecutiveRuns = 0;
298
+ this._resetGeneration++;
363
299
  this.configEpoch++;
364
300
  if (this._pendingRunId) {
365
301
  supersedePendingRun(this._pendingRunId);
@@ -384,6 +320,7 @@ export class HeartbeatService {
384
320
  // Counter resets even when the timer is null so a guardian message during
385
321
  // a stopped window still clears the count.
386
322
  this._consecutiveRuns = 0;
323
+ this._resetGeneration++;
387
324
  if (!this.timer) return;
388
325
  if (this.cronMode) {
389
326
  clearTimeout(this.timer as ReturnType<typeof setTimeout>);
@@ -404,6 +341,7 @@ export class HeartbeatService {
404
341
 
405
342
  async stop(): Promise<void> {
406
343
  this._consecutiveRuns = 0;
344
+ this._resetGeneration++;
407
345
  this.stopped = true;
408
346
  if (this.timer) {
409
347
  clearTimeout(this.timer as ReturnType<typeof setTimeout>);
@@ -552,6 +490,11 @@ export class HeartbeatService {
552
490
  }
553
491
  const run = this.executeRun(runId, scheduledFor);
554
492
  this.activeRun = run;
493
+ // Snapshot the reset generation so we can detect whether a reset (guardian
494
+ // message, reconfigure, stop) happened while this run was in flight. If it
495
+ // did, the counter was already zeroed and we must not undo that reset by
496
+ // incrementing in `finally`.
497
+ const startGeneration = this._resetGeneration;
555
498
  try {
556
499
  await run;
557
500
  } catch (err) {
@@ -561,7 +504,7 @@ export class HeartbeatService {
561
504
  this.activeRun = null;
562
505
  }
563
506
  this._lastRunAt = Date.now();
564
- if (!force) {
507
+ if (!force && this._resetGeneration === startGeneration) {
565
508
  this._consecutiveRuns++;
566
509
  }
567
510
  if (!this.cronMode) {
@@ -699,66 +642,6 @@ export class HeartbeatService {
699
642
  }
700
643
  }
701
644
 
702
- private getLatestAssistantMessage(
703
- conversationId: string,
704
- ): { id: string; text: string } | null {
705
- try {
706
- const messages = getMessages(conversationId);
707
- for (let i = messages.length - 1; i >= 0; i--) {
708
- const message = messages[i]!;
709
- if (message.role !== "assistant") continue;
710
- return {
711
- id: message.id,
712
- text: extractVisibleTextFromStoredMessageContent(message.content),
713
- };
714
- }
715
- } catch (err) {
716
- log.warn(
717
- { err, conversationId },
718
- "Failed to read heartbeat assistant message",
719
- );
720
- }
721
- return null;
722
- }
723
-
724
- private async emitHeartbeatAlertNotification(params: {
725
- runId: string;
726
- conversationId: string;
727
- messageId?: string;
728
- conversationTitle: string;
729
- summary: string;
730
- }): Promise<void> {
731
- const { emitNotificationSignal } =
732
- await import("../notifications/emit-signal.js");
733
-
734
- await emitNotificationSignal({
735
- sourceEventName: "heartbeat.alert",
736
- sourceChannel: "watcher",
737
- sourceContextId: params.runId,
738
- dedupeKey: `heartbeat:alert:${params.runId}`,
739
- attentionHints: {
740
- requiresAction: true,
741
- urgency: "medium",
742
- isAsyncBackground: true,
743
- visibleInSourceNow: false,
744
- },
745
- contextPayload: {
746
- title: "Heartbeat Alert",
747
- summary: params.summary,
748
- conversationTitle: params.conversationTitle,
749
- conversationId: params.conversationId,
750
- messageId: params.messageId,
751
- },
752
- routingIntent: "single_channel",
753
- conversationAffinityHint: { vellum: params.conversationId },
754
- conversationMetadata: {
755
- source: "heartbeat",
756
- groupId: "system:background",
757
- conversationType: "background",
758
- },
759
- });
760
- }
761
-
762
645
  private async executeRun(runId: string, scheduledFor: number): Promise<void> {
763
646
  log.info("Running heartbeat");
764
647
 
@@ -787,9 +670,9 @@ export class HeartbeatService {
787
670
  // The runner fires `onConversationCreated` synchronously after
788
671
  // bootstrap so the macOS sidebar gets the new conversation
789
672
  // immediately rather than waiting up to HEARTBEAT_TIMEOUT_MS for
790
- // the LLM turn to finish. We forward to `deps.onConversationCreated`
791
- // for every run; "silent OK" is enforced by NOT emitting any
792
- // notification signal further down, not by hiding the conversation.
673
+ // the LLM turn to finish. If the model judges the run worth
674
+ // surfacing to the guardian, it calls the `notifications` skill
675
+ // directly no in-band marker.
793
676
  let conversationId: string | undefined;
794
677
  const result = await runBackgroundJob({
795
678
  jobName: "heartbeat",
@@ -803,6 +686,7 @@ export class HeartbeatService {
803
686
  callSite: "heartbeatAgent",
804
687
  timeoutMs: HEARTBEAT_TIMEOUT_MS,
805
688
  origin: "heartbeat",
689
+ deferNotifications: true,
806
690
  onConversationCreated: (newConversationId) => {
807
691
  conversationId = newConversationId;
808
692
  this.deps.onConversationCreated?.({
@@ -821,62 +705,26 @@ export class HeartbeatService {
821
705
  "Heartbeat completed",
822
706
  );
823
707
 
824
- // Mark the run record as ok and surface any disposition-driven
825
- // alert the assistant decided to raise. The runner owns failure
826
- // emission via `activity.failed`; success-side surfacing (alerts,
827
- // late warnings) lives here so it can read the actual conversation
828
- // contents.
708
+ // Mark the run record as ok. The runner owns failure emission via
709
+ // `activity.failed`; any user-facing alert the model decided to
710
+ // raise was emitted in-band via the `notifications` skill during
711
+ // the turn itself.
829
712
  const transitioned = completeHeartbeatRun(runId, {
830
713
  status: "ok",
831
714
  conversationId: result.conversationId,
832
715
  });
833
716
 
834
- if (transitioned) {
835
- let title = "Heartbeat";
836
- try {
837
- const row = getConversation(result.conversationId);
838
- if (row?.title && row.title !== GENERATING_TITLE) {
839
- title = row.title;
840
- }
841
- } catch {
842
- // Best-effort; fall back to generic title.
843
- }
844
-
845
- const assistantMessage = this.getLatestAssistantMessage(
846
- result.conversationId,
847
- );
848
- const disposition = parseHeartbeatDisposition(
849
- assistantMessage?.text ?? null,
850
- );
851
- if (disposition === "alert") {
852
- // Conversation was already surfaced via the runner's bootstrap
853
- // callback above; alert just needs to emit the notification.
854
- void this.emitHeartbeatAlertNotification({
717
+ if (transitioned && latenessMs > LATE_THRESHOLD_MS) {
718
+ const lateMinutes = Math.round(latenessMs / 60_000);
719
+ log.warn(
720
+ {
721
+ latenessMs,
722
+ lateMinutes,
723
+ scheduledFor,
855
724
  runId,
856
- conversationId: result.conversationId,
857
- messageId: assistantMessage?.id,
858
- conversationTitle: title,
859
- summary: buildHeartbeatAlertSummary(assistantMessage?.text ?? null),
860
- }).catch((err) => {
861
- log.warn(
862
- { err, conversationId: result.conversationId },
863
- "Failed to emit heartbeat alert notification",
864
- );
865
- });
866
- }
867
-
868
- if (latenessMs > LATE_THRESHOLD_MS) {
869
- const lateMinutes = Math.round(latenessMs / 60_000);
870
- log.warn(
871
- {
872
- latenessMs,
873
- lateMinutes,
874
- scheduledFor,
875
- runId,
876
- },
877
- "Heartbeat ran late",
878
- );
879
- }
725
+ },
726
+ "Heartbeat ran late",
727
+ );
880
728
  }
881
729
  return;
882
730
  }
@@ -941,18 +789,14 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
941
789
  </credential-status>`;
942
790
  }
943
791
 
944
- prompt += `\n\n<heartbeat-disposition>
945
- This heartbeat runs frequently. Do not manufacture a report just because it ran.
946
- If there is nothing genuinely useful, actionable, or interesting to surface, keep the response brief and end with HEARTBEAT_OK.
947
- If there is something worth interrupting the guardian for, write a concise guardian-facing note first: what happened, why it matters, and the recommended next step. Address the guardian directly as "you"; do not write instructions to yourself or another intermediary. Then end with HEARTBEAT_ALERT. That note may be used as notification copy.
948
- After completing your review, end your response with one of:
949
- - HEARTBEAT_OK — if everything looks good, no action needed
950
- - HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
951
- </heartbeat-disposition>`;
792
+ const disposition = getConfig().heartbeat.disposition;
793
+ if (disposition) {
794
+ prompt += `\n\n<heartbeat-disposition>\n${disposition}\n</heartbeat-disposition>`;
795
+ }
952
796
 
953
797
  if (completedRunCount < EARLY_HEARTBEAT_THRESHOLD) {
954
798
  prompt += `\n\n<early-heartbeat>
955
- This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward HEARTBEAT_ALERT this time. First impressions matter.
799
+ This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward surfacing it via the notifications skill this time. First impressions matter.
956
800
  </early-heartbeat>`;
957
801
  }
958
802
 
@@ -116,6 +116,28 @@ describe("feedItemSchema — valid minimal items", () => {
116
116
  expect(parsed.category).toBeUndefined();
117
117
  expect(parsed.metadata).toBeUndefined();
118
118
  });
119
+
120
+ test("noteworthy field passes through when present", () => {
121
+ const parsed = feedItemSchema.parse({
122
+ ...minimalNotification(),
123
+ noteworthy: true,
124
+ });
125
+ expect(parsed.noteworthy).toBe(true);
126
+ });
127
+
128
+ test("items without noteworthy field still parse (backward compat)", () => {
129
+ const parsed = feedItemSchema.parse(minimalNotification());
130
+ expect(parsed.noteworthy).toBeUndefined();
131
+ });
132
+
133
+ test("title is optional and may be omitted", () => {
134
+ const { title: _omitted, ...rest } = minimalNotification();
135
+ const parsed = feedItemSchema.parse(rest);
136
+ expect(parsed.title).toBeUndefined();
137
+ expect(parsed.summary).toBe(
138
+ "You mentioned wanting to review the onboarding designs.",
139
+ );
140
+ });
119
141
  });
120
142
 
121
143
  // ---------------------------------------------------------------------------
@@ -244,4 +266,22 @@ describe("parseFeedFile", () => {
244
266
  }),
245
267
  ).toThrow();
246
268
  });
269
+
270
+ test("accepts a file with a noteworthy item", () => {
271
+ const parsed = parseFeedFile({
272
+ version: 2,
273
+ items: [{ ...minimalNotification(), noteworthy: true }],
274
+ updatedAt: NOW_ISO,
275
+ });
276
+ expect(parsed.items[0]?.noteworthy).toBe(true);
277
+ });
278
+
279
+ test("accepts a file whose items omit noteworthy (backward compat)", () => {
280
+ const parsed = parseFeedFile({
281
+ version: 2,
282
+ items: [minimalNotification()],
283
+ updatedAt: NOW_ISO,
284
+ });
285
+ expect(parsed.items[0]?.noteworthy).toBeUndefined();
286
+ });
247
287
  });
@@ -5,8 +5,6 @@ import { describe, expect, mock, test } from "bun:test";
5
5
  let mockConnectedProviders = new Set<string>();
6
6
 
7
7
  mock.module("../../oauth/oauth-store.js", () => ({
8
- isProviderConnected: async (provider: string) =>
9
- mockConnectedProviders.has(provider),
10
8
  listProviders: () => [
11
9
  { provider: "google" },
12
10
  { provider: "slack" },
@@ -16,6 +14,11 @@ mock.module("../../oauth/oauth-store.js", () => ({
16
14
  ],
17
15
  }));
18
16
 
17
+ mock.module("../../schedule/integration-status.js", () => ({
18
+ isOAuthProviderConnected: async (provider: string) =>
19
+ mockConnectedProviders.has(provider),
20
+ }));
21
+
19
22
  mock.module("../../util/logger.js", () => ({
20
23
  getLogger: () =>
21
24
  new Proxy({} as Record<string, unknown>, {
@@ -23,6 +26,34 @@ mock.module("../../util/logger.js", () => ({
23
26
  }),
24
27
  }));
25
28
 
29
+ mock.module("../../config/loader.js", () => ({
30
+ getConfig: () => ({ llm: {} }),
31
+ }));
32
+
33
+ mock.module("../../config/llm-resolver.js", () => ({
34
+ resolveCallSiteConfig: () => ({ provider: "mock" }),
35
+ }));
36
+
37
+ mock.module("../../providers/provider-send-message.js", () => ({
38
+ getConfiguredProvider: async () => ({}),
39
+ }));
40
+
41
+ mock.module("../../prompts/persona-resolver.js", () => ({
42
+ resolvePersonaContext: () => ({
43
+ userPersona: null,
44
+ userSlug: null,
45
+ channelPersona: null,
46
+ }),
47
+ }));
48
+
49
+ mock.module("../../prompts/system-prompt.js", () => ({
50
+ buildSystemPrompt: () => "mock system prompt",
51
+ }));
52
+
53
+ mock.module("../../runtime/btw-sidechain.js", () => ({
54
+ runBtwSidechain: async () => ({ text: "" }),
55
+ }));
56
+
26
57
  const { getSuggestedPrompts } = await import("../suggested-prompts.js");
27
58
 
28
59
  // ─── Tests ─────────────────────────────────────────────────────────────
@@ -89,7 +89,13 @@ export interface FeedItem {
89
89
  type: FeedItemType;
90
90
  /** Integer in [0, 100]; higher values sort earlier. */
91
91
  priority: number;
92
- title: string;
92
+ /**
93
+ * Optional short header. Omit when the source did not supply one — the
94
+ * notification pipeline never manufactures a title from rendered copy
95
+ * (LLM-echoed bodies stutter against `summary`). Clients fall back to
96
+ * `summary` when rendering a row.
97
+ */
98
+ title?: string;
93
99
  summary: string;
94
100
  /** Event time (ISO-8601). */
95
101
  timestamp: string;
@@ -100,12 +106,21 @@ export interface FeedItem {
100
106
  actions?: FeedAction[];
101
107
  /** Visual urgency treatment — controls badge color independently of sort priority. */
102
108
  urgency?: FeedItemUrgency;
103
- /** Optional conversation this feed item is associated with. */
109
+ /**
110
+ * Source conversation that emitted this notification, when known. Used by
111
+ * the "Go to Thread" affordance in the detail panel. Omitted when the
112
+ * source context is not a navigable conversation (scheduler job ids,
113
+ * watcher event ids, CLI tool-call ids).
114
+ */
104
115
  conversationId?: string;
105
116
  /** Server-driven detail panel descriptor; when present, the client opens this panel kind. */
106
117
  detailPanel?: FeedItemDetailPanel;
107
118
  /** Broad category for grouping and filtering feed items. */
108
119
  category?: FeedItemCategory;
120
+ /** True when this item represents an assistant-initiated share or a high-importance system event. Used by clients to split inbox vs activity surfaces. */
121
+ noteworthy?: boolean;
122
+ /** True when the assistant herself emitted this item (e.g. via the `notifications send` skill). Drives clients to swap the row's leading icon for the persona avatar; system-generated items keep the category icon. */
123
+ fromAssistant?: boolean;
109
124
  /** Arbitrary structured data the detail panel or other consumers can use. */
110
125
  metadata?: Record<string, unknown>;
111
126
  /** Internal: ISO-8601 writer-record time, used for ordering + TTL. */
@@ -198,7 +213,7 @@ export const feedItemSchema = z.object({
198
213
  id: z.string(),
199
214
  type: feedItemTypeSchema,
200
215
  priority: z.number().int().min(0).max(100),
201
- title: z.string(),
216
+ title: z.string().optional(),
202
217
  summary: z.string(),
203
218
  timestamp: z.string(),
204
219
  status: feedItemStatusSchema.default("new"),
@@ -208,6 +223,8 @@ export const feedItemSchema = z.object({
208
223
  conversationId: z.string().optional(),
209
224
  detailPanel: feedItemDetailPanelSchema.optional(),
210
225
  category: feedItemCategorySchema.optional(),
226
+ noteworthy: z.boolean().optional(),
227
+ fromAssistant: z.boolean().optional(),
211
228
  metadata: z.record(z.string(), z.unknown()).optional(),
212
229
  createdAt: z.string(),
213
230
  });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Background refresh timer for LLM-generated home page content.
3
+ *
4
+ * Keeps the personalized greeting and assistant-generated suggestion
5
+ * prompts warm in their caches so the GET handler never triggers LLM
6
+ * calls or database writes (see `src/runtime/AGENTS.md` — GET handler
7
+ * idempotency rule).
8
+ *
9
+ * Call `startHomeContentRefresh()` once during daemon startup; the
10
+ * timer handles periodic re-generation automatically.
11
+ */
12
+
13
+ import { getLogger } from "../util/logger.js";
14
+ import { refreshPersonalizedGreeting } from "./home-greeting.js";
15
+ import { refreshAssistantSuggestedPrompts } from "./suggested-prompts.js";
16
+
17
+ const log = getLogger("home-content-refresh");
18
+
19
+ const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
20
+
21
+ let refreshTimer: ReturnType<typeof setInterval> | null = null;
22
+
23
+ async function refreshAll(): Promise<void> {
24
+ await Promise.all([
25
+ refreshPersonalizedGreeting(),
26
+ refreshAssistantSuggestedPrompts(),
27
+ ]);
28
+ }
29
+
30
+ /**
31
+ * Start periodic background refresh of home page LLM content.
32
+ * Runs an initial generation immediately (fire-and-forget) and
33
+ * schedules re-generation every 30 minutes.
34
+ */
35
+ export function startHomeContentRefresh(): void {
36
+ void refreshAll().catch((err) =>
37
+ log.warn({ err }, "Initial home content refresh failed"),
38
+ );
39
+
40
+ refreshTimer = setInterval(() => {
41
+ void refreshAll().catch((err) =>
42
+ log.warn({ err }, "Periodic home content refresh failed"),
43
+ );
44
+ }, REFRESH_INTERVAL_MS);
45
+ }
46
+
47
+ export function stopHomeContentRefresh(): void {
48
+ if (refreshTimer) {
49
+ clearInterval(refreshTimer);
50
+ refreshTimer = null;
51
+ }
52
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Caching layer for the LLM-generated personalized home greeting.
3
+ *
4
+ * The greeting (a short persona-flavored "here's what's been going on" line)
5
+ * is generated via `runBtwSidechain` and displayed at the top of the Home
6
+ * page. To avoid redundant LLM calls on every feed fetch, we cache the
7
+ * result for 4 hours with content-hash-based invalidation: when IDENTITY.md,
8
+ * SOUL.md, or the guardian persona change, the cache is busted.
9
+ *
10
+ * Storage uses the existing `memory_checkpoints` table (simple key-value store).
11
+ */
12
+
13
+ import {
14
+ getMemoryCheckpoint,
15
+ setMemoryCheckpoint,
16
+ } from "../memory/checkpoints.js";
17
+ import { computeIdentityContentHash } from "../runtime/routes/identity-intro-cache.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Constants
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const CACHE_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
24
+
25
+ const CHECKPOINT_KEY_TEXT = "home:greeting:text";
26
+ const CHECKPOINT_KEY_HASH = "home:greeting:content_hash";
27
+ const CHECKPOINT_KEY_TIMESTAMP = "home:greeting:cached_at";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Public API
31
+ // ---------------------------------------------------------------------------
32
+
33
+ export function getCachedHomeGreeting(): string | null {
34
+ try {
35
+ const text = getMemoryCheckpoint(CHECKPOINT_KEY_TEXT);
36
+ const hash = getMemoryCheckpoint(CHECKPOINT_KEY_HASH);
37
+ const timestampStr = getMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP);
38
+
39
+ if (!text || !hash || !timestampStr) {
40
+ return null;
41
+ }
42
+
43
+ const cachedAt = Number(timestampStr);
44
+ if (isNaN(cachedAt) || Date.now() - cachedAt > CACHE_TTL_MS) {
45
+ return null;
46
+ }
47
+
48
+ const currentHash = computeIdentityContentHash();
49
+ if (currentHash !== hash) {
50
+ return null;
51
+ }
52
+
53
+ return text;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function setCachedHomeGreeting(text: string): void {
60
+ try {
61
+ const hash = computeIdentityContentHash();
62
+ const now = String(Date.now());
63
+ setMemoryCheckpoint(CHECKPOINT_KEY_TEXT, text);
64
+ setMemoryCheckpoint(CHECKPOINT_KEY_HASH, hash);
65
+ setMemoryCheckpoint(CHECKPOINT_KEY_TIMESTAMP, now);
66
+ } catch {
67
+ // Cache write failure is non-fatal — next request will regenerate.
68
+ }
69
+ }