@vellumai/assistant 0.7.1 → 0.7.3

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 (739) hide show
  1. package/ARCHITECTURE.md +48 -50
  2. package/Dockerfile +1 -0
  3. package/README.md +1 -2
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +9 -3
  5. package/bun.lock +26 -26
  6. package/docs/architecture/memory.md +5 -2
  7. package/docs/architecture/security.md +20 -0
  8. package/docs/plugins.md +7 -9
  9. package/knip.json +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  11. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +52 -5
  12. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  13. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  15. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  16. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  17. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  18. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  19. package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
  20. package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
  21. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  22. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  23. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  24. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  25. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  26. package/openapi.yaml +1020 -40
  27. package/package.json +6 -3
  28. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  29. package/src/__tests__/app-bundler.test.ts +170 -1
  30. package/src/__tests__/app-control-flow.test.ts +384 -0
  31. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  32. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  33. package/src/__tests__/app-executors.test.ts +30 -43
  34. package/src/__tests__/approval-routes-http.test.ts +23 -6
  35. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  36. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  37. package/src/__tests__/assistant-event-hub.test.ts +157 -2
  38. package/src/__tests__/assistant-feature-flags-integration.test.ts +29 -7
  39. package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
  40. package/src/__tests__/background-shell-host-bash.test.ts +14 -15
  41. package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
  42. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  43. package/src/__tests__/btw-routes.test.ts +13 -4
  44. package/src/__tests__/call-controller.test.ts +49 -1
  45. package/src/__tests__/call-conversation-messages.test.ts +8 -2
  46. package/src/__tests__/call-domain.test.ts +0 -2
  47. package/src/__tests__/call-routes-http.test.ts +0 -2
  48. package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
  49. package/src/__tests__/channel-readiness-service.test.ts +62 -2
  50. package/src/__tests__/checker.test.ts +3 -4
  51. package/src/__tests__/config-loader-backfill.test.ts +461 -147
  52. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  53. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  54. package/src/__tests__/config-schema.test.ts +1 -0
  55. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  56. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +20 -11
  57. package/src/__tests__/config-watcher.test.ts +142 -71
  58. package/src/__tests__/context-search-agent-runner.test.ts +61 -3
  59. package/src/__tests__/context-search-conversations-source.test.ts +0 -24
  60. package/src/__tests__/context-search-fanout.test.ts +0 -1
  61. package/src/__tests__/context-search-memory-source.test.ts +3 -7
  62. package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
  63. package/src/__tests__/context-search-pkb-source.test.ts +0 -1
  64. package/src/__tests__/context-search-workspace-source.test.ts +0 -1
  65. package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
  66. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
  67. package/src/__tests__/conversation-agent-loop.test.ts +454 -5
  68. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  69. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  70. package/src/__tests__/conversation-error.test.ts +150 -3
  71. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  72. package/src/__tests__/conversation-lifecycle.test.ts +36 -0
  73. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  74. package/src/__tests__/conversation-process-callsite.test.ts +43 -0
  75. package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
  76. package/src/__tests__/conversation-routes-disk-view.test.ts +6 -0
  77. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -72
  78. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  79. package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
  80. package/src/__tests__/conversation-slash-commands.test.ts +0 -4
  81. package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
  82. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  83. package/src/__tests__/conversation-store.test.ts +0 -18
  84. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  85. package/src/__tests__/conversation-surfaces-app-control.test.ts +328 -0
  86. package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
  87. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
  88. package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
  89. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
  90. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  91. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  92. package/src/__tests__/credentials-cli.test.ts +12 -12
  93. package/src/__tests__/cu-unified-flow.test.ts +351 -23
  94. package/src/__tests__/daemon-credential-client.test.ts +101 -19
  95. package/src/__tests__/date-context.test.ts +164 -2
  96. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  97. package/src/__tests__/disk-pressure-guard.test.ts +262 -0
  98. package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
  99. package/src/__tests__/disk-pressure-policy.test.ts +241 -0
  100. package/src/__tests__/disk-pressure-routes.test.ts +379 -0
  101. package/src/__tests__/disk-pressure-tools.test.ts +277 -0
  102. package/src/__tests__/disk-usage.test.ts +150 -0
  103. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  104. package/src/__tests__/events-client-registration.test.ts +52 -0
  105. package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
  106. package/src/__tests__/file-write-tool.test.ts +4 -10
  107. package/src/__tests__/filing-service.test.ts +3 -4
  108. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  109. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  110. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +0 -2
  111. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -1
  112. package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
  113. package/src/__tests__/heartbeat-service.test.ts +968 -2
  114. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  115. package/src/__tests__/host-app-control-proxy.test.ts +772 -0
  116. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +439 -47
  118. package/src/__tests__/host-bash-routes.test.ts +459 -0
  119. package/src/__tests__/host-browser-proxy.test.ts +24 -22
  120. package/src/__tests__/host-browser-routes.test.ts +39 -13
  121. package/src/__tests__/host-cu-proxy.test.ts +248 -52
  122. package/src/__tests__/host-cu-routes-targeted.test.ts +429 -0
  123. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  124. package/src/__tests__/host-file-proxy-targeted.test.ts +378 -0
  125. package/src/__tests__/host-file-proxy.test.ts +301 -45
  126. package/src/__tests__/host-file-read-tool.test.ts +17 -0
  127. package/src/__tests__/host-file-routes-targeted.test.ts +420 -0
  128. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  129. package/src/__tests__/host-proxy-base.test.ts +312 -0
  130. package/src/__tests__/host-shell-tool.test.ts +22 -4
  131. package/src/__tests__/host-transfer-proxy-targeted.test.ts +932 -0
  132. package/src/__tests__/host-transfer-proxy.test.ts +121 -22
  133. package/src/__tests__/host-transfer-routes-targeted.test.ts +662 -0
  134. package/src/__tests__/http-user-message-parity.test.ts +108 -1
  135. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  136. package/src/__tests__/identity-routes.test.ts +103 -1
  137. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  138. package/src/__tests__/injector-chain.test.ts +18 -6
  139. package/src/__tests__/injector-disk-pressure.test.ts +224 -0
  140. package/src/__tests__/inline-command-runner.test.ts +0 -1
  141. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -11
  142. package/src/__tests__/integration-status.test.ts +85 -5
  143. package/src/__tests__/intent-routing.test.ts +0 -1
  144. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  145. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  146. package/src/__tests__/managed-profile-guard.test.ts +18 -0
  147. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -1
  148. package/src/__tests__/mcp-abort-signal.test.ts +130 -0
  149. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  150. package/src/__tests__/mcp-cli.test.ts +338 -2
  151. package/src/__tests__/memory-admin-recall.test.ts +3 -11
  152. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  153. package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
  154. package/src/__tests__/migration-import-commit-http.test.ts +108 -2
  155. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  156. package/src/__tests__/normalize-onboarding.test.ts +180 -0
  157. package/src/__tests__/oauth-cli.test.ts +0 -2
  158. package/src/__tests__/oauth-connect-routes.test.ts +316 -0
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
  160. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  161. package/src/__tests__/onboarding-persona-write.test.ts +308 -0
  162. package/src/__tests__/openai-provider.test.ts +45 -8
  163. package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
  164. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  165. package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
  166. package/src/__tests__/platform-callback-registration.test.ts +21 -4
  167. package/src/__tests__/platform.test.ts +2 -1
  168. package/src/__tests__/playbook-execution.test.ts +0 -43
  169. package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
  170. package/src/__tests__/prechat-onboarding-contract.test.ts +214 -25
  171. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  172. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  173. package/src/__tests__/provider-tool-name.test.ts +23 -0
  174. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  175. package/src/__tests__/relay-server.test.ts +15 -4
  176. package/src/__tests__/require-fresh-approval.test.ts +0 -1
  177. package/src/__tests__/retry-backoff.test.ts +87 -0
  178. package/src/__tests__/runtime-events-sse.test.ts +2 -2
  179. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  180. package/src/__tests__/schedule-retry.test.ts +715 -0
  181. package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
  182. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  183. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  184. package/src/__tests__/send-endpoint-busy.test.ts +3 -0
  185. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -1
  186. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  187. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  188. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  189. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  190. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  191. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -1
  192. package/src/__tests__/slack-channel-config.test.ts +9 -14
  193. package/src/__tests__/suggestion-routes.test.ts +46 -0
  194. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  195. package/src/__tests__/system-prompt.test.ts +0 -1
  196. package/src/__tests__/telegram-config.test.ts +0 -1
  197. package/src/__tests__/test-preload.ts +8 -0
  198. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  199. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  200. package/src/__tests__/tool-execute-pipeline.test.ts +0 -1
  201. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -1
  202. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  203. package/src/__tests__/tool-executor.test.ts +0 -1
  204. package/src/__tests__/twilio-config.test.ts +3 -16
  205. package/src/__tests__/twilio-routes.test.ts +3 -5
  206. package/src/__tests__/twilio-validation.test.ts +93 -0
  207. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -4
  208. package/src/__tests__/verification-control-plane-policy.test.ts +2 -4
  209. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  210. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  211. package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
  212. package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
  213. package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
  214. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  215. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  216. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
  217. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  218. package/src/approvals/guardian-decision-primitive.ts +13 -0
  219. package/src/approvals/guardian-request-resolvers.ts +16 -17
  220. package/src/backup/__tests__/paths.test.ts +0 -22
  221. package/src/backup/__tests__/restore.test.ts +51 -151
  222. package/src/backup/paths.ts +2 -18
  223. package/src/backup/restore.ts +107 -231
  224. package/src/backup/snapshot-lock.ts +2 -27
  225. package/src/bundler/app-bundler.ts +51 -3
  226. package/src/bundler/compiler-tools.ts +3 -2
  227. package/src/calls/call-conversation-messages.ts +46 -10
  228. package/src/calls/relay-server.ts +4 -44
  229. package/src/calls/twilio-config.ts +2 -17
  230. package/src/calls/twilio-rest.ts +33 -105
  231. package/src/calls/twilio-routes.ts +11 -12
  232. package/src/channels/types.ts +8 -7
  233. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  234. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  235. package/src/cli/commands/__tests__/memory-v2.test.ts +4 -0
  236. package/src/cli/commands/__tests__/webhooks.test.ts +0 -5
  237. package/src/cli/commands/backup.ts +6 -331
  238. package/src/cli/commands/bash.ts +35 -108
  239. package/src/cli/commands/clients.ts +36 -37
  240. package/src/cli/commands/contacts.ts +137 -25
  241. package/src/cli/commands/conversations.ts +2 -5
  242. package/src/cli/commands/credentials.ts +71 -7
  243. package/src/cli/commands/domain.ts +66 -15
  244. package/src/cli/commands/gateway.ts +183 -0
  245. package/src/cli/commands/keys.ts +9 -6
  246. package/src/cli/commands/mcp.ts +116 -156
  247. package/src/cli/commands/memory-v2.ts +303 -7
  248. package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
  249. package/src/cli/commands/oauth/connect.ts +127 -1
  250. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -4
  251. package/src/cli/commands/platform/__tests__/connect.test.ts +7 -3
  252. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -3
  253. package/src/cli/commands/platform/__tests__/status.test.ts +116 -21
  254. package/src/cli/commands/platform/disconnect.ts +5 -4
  255. package/src/cli/commands/platform/index.ts +16 -25
  256. package/src/cli/commands/status.ts +57 -0
  257. package/src/cli/lib/daemon-credential-client.ts +110 -28
  258. package/src/cli/program.ts +6 -2
  259. package/src/config/assistant-feature-flags.ts +79 -12
  260. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  261. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  262. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  263. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  264. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  265. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  266. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  267. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  268. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  269. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  270. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  271. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  272. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  273. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  274. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  275. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  276. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  277. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  278. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  279. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  280. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  281. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  282. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  283. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  284. package/src/config/bundled-skills/messaging/TOOLS.json +0 -40
  285. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
  286. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  287. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +25 -4
  288. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  289. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
  290. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
  291. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
  292. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
  293. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  294. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  295. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  296. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  297. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  298. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  299. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  300. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  301. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  302. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  303. package/src/config/bundled-tool-registry.ts +21 -0
  304. package/src/config/env-registry.ts +0 -2
  305. package/src/config/env.ts +19 -20
  306. package/src/config/feature-flag-registry.json +47 -135
  307. package/src/config/loader.ts +197 -104
  308. package/src/config/sanitize-for-transfer.ts +2 -0
  309. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  310. package/src/config/schemas/__tests__/memory-v2.test.ts +17 -9
  311. package/src/config/schemas/call-site-catalog.ts +14 -0
  312. package/src/config/schemas/calls.ts +0 -9
  313. package/src/config/schemas/channels.ts +0 -5
  314. package/src/config/schemas/heartbeat.ts +64 -1
  315. package/src/config/schemas/ingress.ts +10 -6
  316. package/src/config/schemas/llm.ts +7 -10
  317. package/src/config/schemas/memory-lifecycle.ts +90 -24
  318. package/src/config/schemas/memory-v2.ts +121 -13
  319. package/src/config/schemas/platform.ts +49 -3
  320. package/src/config/schemas/services.ts +29 -15
  321. package/src/config/schemas/skills.ts +0 -6
  322. package/src/config/seed-inference-profiles.ts +230 -33
  323. package/src/contacts/contact-store.ts +0 -55
  324. package/src/contacts/contacts-write.ts +0 -27
  325. package/src/context/window-manager.ts +1 -2
  326. package/src/credential-execution/feature-gates.ts +10 -10
  327. package/src/credential-execution/process-manager.ts +12 -41
  328. package/src/daemon/__tests__/conversation-tool-setup.test.ts +187 -5
  329. package/src/daemon/assistant-attachments.ts +4 -4
  330. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  331. package/src/daemon/config-watcher.ts +89 -60
  332. package/src/daemon/conversation-agent-loop-handlers.ts +27 -3
  333. package/src/daemon/conversation-agent-loop.ts +202 -61
  334. package/src/daemon/conversation-error.ts +87 -15
  335. package/src/daemon/conversation-lifecycle.ts +9 -4
  336. package/src/daemon/conversation-process.ts +24 -11
  337. package/src/daemon/conversation-runtime-assembly.ts +28 -2
  338. package/src/daemon/conversation-store.ts +2 -2
  339. package/src/daemon/conversation-surfaces.ts +305 -4
  340. package/src/daemon/conversation-tool-setup.ts +66 -62
  341. package/src/daemon/conversation.ts +38 -24
  342. package/src/daemon/date-context.ts +71 -22
  343. package/src/daemon/disk-pressure-background-gate.ts +73 -0
  344. package/src/daemon/disk-pressure-guard.ts +343 -0
  345. package/src/daemon/disk-pressure-policy.ts +163 -0
  346. package/src/daemon/doordash-steps.ts +1 -1
  347. package/src/daemon/handlers/shared.ts +4 -2
  348. package/src/daemon/handlers/skills.ts +3 -4
  349. package/src/daemon/host-app-control-proxy.ts +389 -0
  350. package/src/daemon/host-bash-proxy.ts +117 -82
  351. package/src/daemon/host-browser-proxy.ts +67 -82
  352. package/src/daemon/host-cu-proxy.ts +127 -86
  353. package/src/daemon/host-file-proxy.ts +129 -69
  354. package/src/daemon/host-proxy-base.ts +294 -0
  355. package/src/daemon/host-proxy-preactivation.ts +82 -0
  356. package/src/daemon/host-transfer-proxy.ts +338 -129
  357. package/src/daemon/lifecycle.ts +194 -145
  358. package/src/daemon/meet-host-supervisor.ts +4 -4
  359. package/src/daemon/meet-manifest-loader.ts +0 -1
  360. package/src/daemon/memory-v2-startup.ts +14 -4
  361. package/src/daemon/message-protocol.ts +6 -8
  362. package/src/daemon/message-types/contacts.ts +23 -1
  363. package/src/daemon/message-types/conversations.ts +15 -8
  364. package/src/daemon/message-types/disk-pressure.ts +9 -0
  365. package/src/daemon/message-types/host-app-control.ts +150 -0
  366. package/src/daemon/message-types/host-bash.ts +4 -0
  367. package/src/daemon/message-types/host-cu.ts +2 -0
  368. package/src/daemon/message-types/host-file.ts +4 -0
  369. package/src/daemon/message-types/host-transfer.ts +3 -0
  370. package/src/daemon/message-types/messages.ts +3 -0
  371. package/src/daemon/message-types/schedules.ts +8 -3
  372. package/src/daemon/message-types/skills.ts +2 -2
  373. package/src/daemon/process-message.ts +18 -1
  374. package/src/daemon/profiler-run-store.ts +5 -5
  375. package/src/daemon/shutdown-handlers.ts +0 -3
  376. package/src/daemon/tool-setup-types.ts +51 -0
  377. package/src/daemon/tool-side-effects.ts +1 -1
  378. package/src/documents/document-store.ts +85 -0
  379. package/src/events/tool-audit-listener.ts +2 -1
  380. package/src/filing/filing-service.ts +30 -5
  381. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +24 -23
  382. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +252 -0
  383. package/src/heartbeat/heartbeat-run-store.ts +249 -0
  384. package/src/heartbeat/heartbeat-service.ts +459 -54
  385. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  386. package/src/home/__tests__/relationship-state-writer.test.ts +11 -9
  387. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  388. package/src/home/feed-scheduler.ts +18 -0
  389. package/src/home/post-connect-feed.ts +68 -0
  390. package/src/home/relationship-state-writer.ts +17 -92
  391. package/src/home/suggested-prompts.ts +46 -10
  392. package/src/inbound/platform-callback-registration.ts +8 -15
  393. package/src/inbound/public-ingress-urls.ts +32 -34
  394. package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
  395. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  396. package/src/ipc/assistant-server.ts +70 -3
  397. package/src/ipc/cli-client.ts +32 -1
  398. package/src/ipc/gateway-client.ts +37 -3
  399. package/src/live-voice/live-voice-archive.ts +4 -4
  400. package/src/live-voice/live-voice-metrics.ts +10 -10
  401. package/src/live-voice/protocol.ts +5 -7
  402. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  403. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  404. package/src/mcp/mcp-auth-state.ts +133 -0
  405. package/src/mcp/mcp-oauth-provider.ts +19 -0
  406. package/src/media/image-service.ts +1 -7
  407. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
  408. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  409. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
  410. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
  411. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
  412. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  413. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  414. package/src/memory/admin.ts +5 -9
  415. package/src/memory/anisotropy.test.ts +247 -0
  416. package/src/memory/anisotropy.ts +443 -0
  417. package/src/memory/auto-analysis-constants.ts +17 -0
  418. package/src/memory/auto-analysis-guard.ts +5 -15
  419. package/src/memory/canonical-guardian-store.ts +7 -7
  420. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  421. package/src/memory/context-search/agent-protocol.ts +6 -6
  422. package/src/memory/context-search/agent-runner.ts +51 -9
  423. package/src/memory/context-search/sources/conversations.ts +2 -11
  424. package/src/memory/context-search/sources/memory-v2.ts +22 -9
  425. package/src/memory/context-search/sources/memory.ts +0 -1
  426. package/src/memory/context-search/types.ts +0 -1
  427. package/src/memory/conversation-crud.ts +5 -13
  428. package/src/memory/conversation-key-store.ts +2 -15
  429. package/src/memory/db-init.ts +6 -0
  430. package/src/memory/embedding-backend.ts +9 -21
  431. package/src/memory/embedding-runtime-manager.ts +119 -5
  432. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +81 -25
  433. package/src/memory/graph/conversation-graph-memory.ts +43 -78
  434. package/src/memory/graph/extraction.ts +1 -3
  435. package/src/memory/graph/graph-search.test.ts +10 -67
  436. package/src/memory/graph/graph-search.ts +9 -20
  437. package/src/memory/graph/retriever.test.ts +6 -0
  438. package/src/memory/graph/retriever.ts +34 -10
  439. package/src/memory/graph/tools.ts +1 -1
  440. package/src/memory/indexer.ts +54 -45
  441. package/src/memory/job-handlers/backfill.ts +2 -11
  442. package/src/memory/job-handlers/cleanup.ts +43 -0
  443. package/src/memory/job-handlers/embedding.ts +6 -8
  444. package/src/memory/job-handlers/summarization.ts +2 -7
  445. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  446. package/src/memory/jobs/embed-concept-page.ts +28 -2
  447. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  448. package/src/memory/jobs-store.ts +114 -22
  449. package/src/memory/jobs-worker.ts +193 -106
  450. package/src/memory/memory-v2-activation-log-store.ts +33 -15
  451. package/src/memory/memory-v2-concept-frequency.ts +169 -0
  452. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  453. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  454. package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
  455. package/src/memory/migrations/index.ts +6 -0
  456. package/src/memory/migrations/registry.ts +8 -0
  457. package/src/memory/pkb/pkb-search.test.ts +6 -0
  458. package/src/memory/pkb/pkb-search.ts +7 -0
  459. package/src/memory/qdrant-client.ts +49 -32
  460. package/src/memory/rerank-local.ts +374 -0
  461. package/src/memory/schema/infrastructure.ts +15 -0
  462. package/src/memory/search/semantic.ts +13 -67
  463. package/src/memory/sparse-tokenize.ts +49 -0
  464. package/src/memory/trace-event-store.ts +1 -17
  465. package/src/memory/v2/__tests__/activation.test.ts +387 -344
  466. package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
  467. package/src/memory/v2/__tests__/injection.test.ts +181 -169
  468. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
  469. package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
  470. package/src/memory/v2/__tests__/reranker.test.ts +338 -0
  471. package/src/memory/v2/__tests__/sim.test.ts +154 -188
  472. package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
  473. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  474. package/src/memory/v2/__tests__/static-context.test.ts +76 -2
  475. package/src/memory/v2/activation.ts +213 -239
  476. package/src/memory/v2/consolidation-job.ts +65 -17
  477. package/src/memory/v2/constants.ts +7 -0
  478. package/src/memory/v2/injection.ts +123 -103
  479. package/src/memory/v2/prompts/consolidation.ts +348 -92
  480. package/src/memory/v2/qdrant.ts +198 -1
  481. package/src/memory/v2/reranker.ts +177 -0
  482. package/src/memory/v2/sim.ts +113 -77
  483. package/src/memory/v2/skill-content.ts +4 -3
  484. package/src/memory/v2/skill-store.ts +91 -53
  485. package/src/memory/v2/sparse-bm25.ts +245 -0
  486. package/src/memory/v2/static-context.ts +28 -5
  487. package/src/memory/v2/types.ts +10 -10
  488. package/src/messaging/providers/gmail/types.ts +0 -49
  489. package/src/messaging/providers/slack/adapter.ts +1 -31
  490. package/src/messaging/providers/slack/types.ts +0 -32
  491. package/src/notifications/README.md +10 -10
  492. package/src/notifications/broadcaster.ts +1 -1
  493. package/src/notifications/copy-composer.ts +13 -0
  494. package/src/notifications/guardian-question-mode.ts +5 -5
  495. package/src/notifications/signal.ts +4 -0
  496. package/src/oauth/AGENTS.md +3 -1
  497. package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
  498. package/src/oauth/connect-orchestrator.ts +6 -0
  499. package/src/oauth/connection-resolver.test.ts +66 -1
  500. package/src/oauth/connection-resolver.ts +55 -1
  501. package/src/oauth/credential-token-resolver.ts +1 -3
  502. package/src/oauth/manual-token-connection.ts +0 -4
  503. package/src/oauth/oauth-connect-state.ts +77 -0
  504. package/src/oauth/seed-providers.ts +58 -1
  505. package/src/outbound-proxy/index.ts +1 -37
  506. package/src/outbound-proxy/logging.ts +1 -1
  507. package/src/outbound-proxy/policy.ts +6 -5
  508. package/src/outbound-proxy/router.ts +2 -1
  509. package/src/permissions/approval-policy.test.ts +6 -275
  510. package/src/permissions/approval-policy.ts +0 -51
  511. package/src/permissions/checker.test.ts +0 -1
  512. package/src/permissions/checker.ts +3 -17
  513. package/src/permissions/gateway-threshold-reader.ts +2 -0
  514. package/src/permissions/prompter.ts +34 -1
  515. package/src/permissions/secret-prompter.ts +6 -2
  516. package/src/plugins/defaults/injectors.ts +35 -2
  517. package/src/plugins/defaults/memory-retrieval.ts +5 -6
  518. package/src/plugins/types.ts +7 -0
  519. package/src/proactive-artifact/aux-message-injector.ts +74 -0
  520. package/src/proactive-artifact/decision.test.ts +226 -0
  521. package/src/proactive-artifact/decision.ts +165 -0
  522. package/src/proactive-artifact/index.ts +7 -0
  523. package/src/proactive-artifact/job.test.ts +867 -0
  524. package/src/proactive-artifact/job.ts +352 -0
  525. package/src/proactive-artifact/message-copy.ts +41 -0
  526. package/src/proactive-artifact/trigger-state.test.ts +277 -0
  527. package/src/proactive-artifact/trigger-state.ts +119 -0
  528. package/src/prompts/bootstrap-cleanup.ts +27 -0
  529. package/src/prompts/normalize-onboarding.ts +80 -0
  530. package/src/prompts/persona-resolver.ts +101 -9
  531. package/src/prompts/system-prompt.ts +23 -24
  532. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  533. package/src/prompts/templates/SOUL.md +13 -1
  534. package/src/providers/__tests__/retry-callsite.test.ts +222 -1
  535. package/src/providers/model-intents.ts +7 -0
  536. package/src/providers/openrouter/client.ts +8 -0
  537. package/src/providers/retry.ts +50 -0
  538. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  539. package/src/providers/types.ts +1 -0
  540. package/src/runtime/__tests__/agent-wake.test.ts +456 -3
  541. package/src/runtime/agent-wake.ts +238 -100
  542. package/src/runtime/assistant-event-hub.ts +151 -99
  543. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  544. package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
  545. package/src/runtime/auth/middleware.ts +0 -96
  546. package/src/runtime/auth/route-policy.ts +32 -0
  547. package/src/runtime/auth/same-actor.ts +216 -0
  548. package/src/runtime/btw-sidechain.ts +2 -3
  549. package/src/runtime/channel-invite-transport.ts +2 -48
  550. package/src/runtime/channel-invite-transports/email.ts +1 -1
  551. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  552. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  553. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  554. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  555. package/src/runtime/channel-invite-types.ts +54 -0
  556. package/src/runtime/channel-readiness-service.ts +32 -13
  557. package/src/runtime/channel-retry-sweep.ts +65 -1
  558. package/src/runtime/guardian-reply-router.ts +10 -0
  559. package/src/runtime/http-server.ts +3 -329
  560. package/src/runtime/http-types.ts +0 -5
  561. package/src/runtime/local-actor-identity.ts +52 -11
  562. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  563. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  564. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  565. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +153 -1
  566. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  567. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  568. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  569. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  570. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  571. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +51 -1
  572. package/src/runtime/migrations/migration-transport.ts +7 -7
  573. package/src/runtime/migrations/vbundle-builder.ts +327 -60
  574. package/src/runtime/migrations/vbundle-import-analyzer.ts +4 -4
  575. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  576. package/src/runtime/migrations/vbundle-importer.ts +245 -68
  577. package/src/runtime/migrations/vbundle-streaming-importer.ts +326 -35
  578. package/src/runtime/migrations/vbundle-streaming-validator.ts +157 -4
  579. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  580. package/src/runtime/migrations/vbundle-validator.ts +114 -0
  581. package/src/runtime/pending-interactions.ts +43 -9
  582. package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
  583. package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
  584. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -5
  585. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  586. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  587. package/src/runtime/routes/approval-interception-types.ts +13 -0
  588. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  589. package/src/runtime/routes/backup-routes.ts +15 -38
  590. package/src/runtime/routes/btw-routes.ts +14 -37
  591. package/src/runtime/routes/client-routes.ts +21 -2
  592. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  593. package/src/runtime/routes/contact-routes.ts +0 -25
  594. package/src/runtime/routes/conversation-query-routes.ts +36 -1
  595. package/src/runtime/routes/conversation-routes.ts +65 -39
  596. package/src/runtime/routes/debug-bash-routes.ts +163 -0
  597. package/src/runtime/routes/disk-pressure-routes.ts +121 -0
  598. package/src/runtime/routes/document-pdf-renderer.ts +169 -0
  599. package/src/runtime/routes/documents-routes.ts +32 -75
  600. package/src/runtime/routes/errors.ts +19 -4
  601. package/src/runtime/routes/events-routes.ts +38 -0
  602. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  603. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  604. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  605. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  606. package/src/runtime/routes/host-bash-routes.ts +56 -6
  607. package/src/runtime/routes/host-browser-routes.ts +108 -13
  608. package/src/runtime/routes/host-cu-routes.ts +66 -9
  609. package/src/runtime/routes/host-file-routes.ts +54 -5
  610. package/src/runtime/routes/host-transfer-routes.ts +122 -19
  611. package/src/runtime/routes/http-adapter.ts +1 -0
  612. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  613. package/src/runtime/routes/identity-routes.ts +21 -180
  614. package/src/runtime/routes/inbound-message-handler.ts +78 -21
  615. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -7
  616. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  617. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
  618. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  619. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  620. package/src/runtime/routes/index.ts +14 -0
  621. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  622. package/src/runtime/routes/memory-item-routes.test.ts +41 -15
  623. package/src/runtime/routes/memory-item-routes.ts +10 -12
  624. package/src/runtime/routes/memory-v2-routes.ts +474 -1
  625. package/src/runtime/routes/migration-routes.ts +96 -0
  626. package/src/runtime/routes/oauth-connect-routes.ts +153 -0
  627. package/src/runtime/routes/schedule-routes.ts +7 -0
  628. package/src/runtime/verification-outbound-actions.ts +4 -4
  629. package/src/runtime/verification-templates.ts +4 -7
  630. package/src/schedule/integration-status.ts +66 -2
  631. package/src/schedule/recurrence-engine.ts +4 -1
  632. package/src/schedule/retry-backoff.ts +18 -0
  633. package/src/schedule/retry-policy.ts +82 -0
  634. package/src/schedule/run-script.ts +37 -5
  635. package/src/schedule/schedule-recovery.ts +64 -0
  636. package/src/schedule/schedule-store.ts +106 -2
  637. package/src/schedule/scheduler-types.ts +25 -0
  638. package/src/schedule/scheduler.ts +83 -39
  639. package/src/security/encrypted-store.ts +2 -0
  640. package/src/security/oauth-callback-registry.ts +8 -0
  641. package/src/security/secure-keys.ts +55 -0
  642. package/src/sequence/analytics.ts +5 -5
  643. package/src/sequence/engine.ts +1 -1
  644. package/src/skills/catalog-files.ts +2 -8
  645. package/src/skills/include-graph.ts +5 -5
  646. package/src/skills/remote-skill-policy.ts +10 -16
  647. package/src/skills/skill-file-provider.ts +1 -1
  648. package/src/skills/skill-file-types.ts +13 -0
  649. package/src/skills/skillssh-audit-types.ts +28 -0
  650. package/src/skills/skillssh-registry.ts +8 -21
  651. package/src/subagent/index.ts +1 -7
  652. package/src/subagent/manager.ts +1 -15
  653. package/src/tasks/task-runner.ts +0 -1
  654. package/src/tasks/task-store.ts +0 -3
  655. package/src/telemetry/types.ts +2 -0
  656. package/src/telemetry/usage-telemetry-reporter.test.ts +21 -0
  657. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  658. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  659. package/src/tools/apps/executors.ts +56 -69
  660. package/src/tools/background-tool-registry.ts +17 -3
  661. package/src/tools/browser/__tests__/browser-status.test.ts +21 -18
  662. package/src/tools/browser/browser-execution.ts +2 -2
  663. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +55 -4
  664. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  665. package/src/tools/browser/cdp-client/factory.ts +23 -24
  666. package/src/tools/browser/cdp-client/index.ts +1 -14
  667. package/src/tools/computer-use/definitions.ts +42 -20
  668. package/src/tools/executor.ts +2 -0
  669. package/src/tools/host-filesystem/edit.test.ts +151 -0
  670. package/src/tools/host-filesystem/edit.ts +68 -0
  671. package/src/tools/host-filesystem/read.test.ts +129 -0
  672. package/src/tools/host-filesystem/read.ts +68 -0
  673. package/src/tools/host-filesystem/transfer.test.ts +127 -2
  674. package/src/tools/host-filesystem/transfer.ts +78 -3
  675. package/src/tools/host-filesystem/write.test.ts +134 -0
  676. package/src/tools/host-filesystem/write.ts +68 -0
  677. package/src/tools/host-terminal/host-shell.ts +66 -1
  678. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  679. package/src/tools/memory/register.test.ts +12 -9
  680. package/src/tools/memory/register.ts +1 -2
  681. package/src/tools/provider-tool-name.ts +28 -0
  682. package/src/tools/registry.ts +30 -9
  683. package/src/tools/schedule/create.ts +6 -0
  684. package/src/tools/schedule/list.ts +2 -0
  685. package/src/tools/schedule/update.ts +10 -0
  686. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  687. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  688. package/src/tools/skills/load.ts +0 -32
  689. package/src/tools/terminal/shell.ts +9 -1
  690. package/src/tools/tool-approval-handler.ts +32 -11
  691. package/src/tools/types.ts +28 -2
  692. package/src/tts/provider-catalog.ts +3 -5
  693. package/src/usage/pricing.ts +1 -1
  694. package/src/util/disk-usage.ts +138 -0
  695. package/src/util/platform.ts +21 -11
  696. package/src/util/process-liveness.ts +26 -0
  697. package/src/workspace/hatched-date.ts +86 -0
  698. package/src/workspace/heartbeat-service.ts +19 -0
  699. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  700. package/src/workspace/migrations/006-services-config.ts +8 -5
  701. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  702. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  703. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  704. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  705. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  706. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  707. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +2 -1
  708. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  709. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +3 -8
  710. package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
  711. package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
  712. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
  713. package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
  714. package/src/workspace/migrations/AGENTS.md +1 -1
  715. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  716. package/src/workspace/migrations/registry.ts +8 -0
  717. package/src/workspace/migrations/utils.ts +21 -0
  718. package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
  719. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -443
  720. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -226
  721. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -427
  722. package/src/__tests__/twilio-rest.test.ts +0 -34
  723. package/src/backup/__tests__/backup-key.test.ts +0 -152
  724. package/src/backup/__tests__/backup-worker.test.ts +0 -782
  725. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  726. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  727. package/src/backup/backup-key.ts +0 -137
  728. package/src/backup/backup-worker.ts +0 -472
  729. package/src/backup/offsite-writer.ts +0 -222
  730. package/src/backup/stream-crypt.ts +0 -263
  731. package/src/daemon/message-types/pairing.ts +0 -58
  732. package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
  733. package/src/memory/v2/skill-qdrant.ts +0 -395
  734. package/src/outbound-proxy/config.ts +0 -20
  735. package/src/outbound-proxy/health.ts +0 -18
  736. package/src/outbound-proxy/types.ts +0 -150
  737. package/src/runtime/capability-tokens.ts +0 -190
  738. package/src/signals/bash.ts +0 -198
  739. package/src/signals/mcp-reload.ts +0 -18
@@ -1,14 +1,52 @@
1
1
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
3
+ import {
4
+ afterEach,
5
+ beforeEach,
6
+ describe,
7
+ expect,
8
+ jest,
9
+ mock,
10
+ test,
11
+ } from "bun:test";
4
12
 
5
13
  const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
6
14
 
15
+ // ── Heartbeat run store mock ───────────────────────────────────────
16
+ const mockInsertPendingHeartbeatRun = mock(() => "mock-run-id");
17
+ const mockStartHeartbeatRun = mock(() => true);
18
+ const mockCompleteHeartbeatRun = mock(() => true);
19
+ const mockSkipHeartbeatRun = mock(() => true);
20
+ const mockSupersedePendingRun = mock(() => true);
21
+ const mockMarkStaleRunsAsMissed = mock(() => 0);
22
+ const mockMarkStaleRunningAsError = mock(() => 0);
23
+ const mockListHeartbeatRuns = mock(() => []);
24
+ const mockCountCompletedHeartbeatRuns = mock(() => 10);
25
+ mock.module("../heartbeat/heartbeat-run-store.js", () => ({
26
+ insertPendingHeartbeatRun: mockInsertPendingHeartbeatRun,
27
+ startHeartbeatRun: mockStartHeartbeatRun,
28
+ completeHeartbeatRun: mockCompleteHeartbeatRun,
29
+ skipHeartbeatRun: mockSkipHeartbeatRun,
30
+ supersedePendingRun: mockSupersedePendingRun,
31
+ markStaleRunsAsMissed: mockMarkStaleRunsAsMissed,
32
+ markStaleRunningAsError: mockMarkStaleRunningAsError,
33
+ listHeartbeatRuns: mockListHeartbeatRuns,
34
+ countCompletedHeartbeatRuns: mockCountCompletedHeartbeatRuns,
35
+ }));
36
+
37
+ // ── Feed event mock ───────────────────────────────────────────────
38
+ const mockEmitFeedEvent = mock(() => Promise.resolve());
39
+ mock.module("../home/emit-feed-event.js", () => ({
40
+ emitFeedEvent: mockEmitFeedEvent,
41
+ }));
42
+
7
43
  // Mock config loader
8
44
  let mockConfig = {
9
45
  heartbeat: {
10
46
  enabled: true,
11
47
  intervalMs: 60_000,
48
+ cronExpression: null as string | null,
49
+ timezone: null as string | null,
12
50
  activeHoursStart: undefined as number | undefined,
13
51
  activeHoursEnd: undefined as number | undefined,
14
52
  },
@@ -22,6 +60,32 @@ mock.module("../config/loader.js", () => ({
22
60
  invalidateConfigCache: () => {},
23
61
  }));
24
62
 
63
+ // ── Recurrence engine mock ──────────────────────────────────────────
64
+ //
65
+ // HeartbeatService imports computeNextRunAt for cron scheduling.
66
+ // Tests mutate `mockComputeNextRunAt` to control the next cron occurrence.
67
+ let mockComputeNextRunAtResult: number | null = null;
68
+ let mockComputeNextRunAtError: Error | null = null;
69
+ let computeNextRunAtCallCount = 0;
70
+
71
+ mock.module("../schedule/recurrence-engine.js", () => ({
72
+ computeNextRunAt: (_spec: {
73
+ syntax: string;
74
+ expression: string;
75
+ timezone?: string | null;
76
+ }) => {
77
+ computeNextRunAtCallCount++;
78
+ if (mockComputeNextRunAtError) {
79
+ throw mockComputeNextRunAtError;
80
+ }
81
+ if (mockComputeNextRunAtResult != null) {
82
+ return mockComputeNextRunAtResult;
83
+ }
84
+ // Default: 1 hour from now
85
+ return Date.now() + 3_600_000;
86
+ },
87
+ }));
88
+
25
89
  // ── Guardian persona mock ─────────────────────────────────────────
26
90
  //
27
91
  // `heartbeat-service.isShallowProfile` reads the guardian persona via
@@ -59,6 +123,14 @@ mock.module("../prompts/persona-resolver.js", () => ({
59
123
  const createdConversations: Array<{ title: string; conversationType: string }> =
60
124
  [];
61
125
  let conversationIdCounter = 0;
126
+ const mockStoredMessages: Array<{
127
+ id: string;
128
+ conversationId: string;
129
+ role: string;
130
+ content: string;
131
+ createdAt: number;
132
+ metadata: string | null;
133
+ }> = [];
62
134
 
63
135
  mock.module("../memory/conversation-crud.js", () => ({
64
136
  setConversationOriginChannelIfUnset: () => {},
@@ -67,7 +139,7 @@ mock.module("../memory/conversation-crud.js", () => ({
67
139
  updateConversationTitle: () => {},
68
140
  updateConversationUsage: () => {},
69
141
  addMessage: () => ({ id: "mock-msg-id" }),
70
- getMessages: () => [],
142
+ getMessages: () => mockStoredMessages,
71
143
  getConversation: () => ({
72
144
  id: "conv-1",
73
145
  contextSummary: null,
@@ -139,21 +211,36 @@ mock.module("../credential-health/credential-health-service.js", () => ({
139
211
  // `notifyUnhealthyCredentials` dynamically imports `emitNotificationSignal`.
140
212
  // Track calls so tests can assert which credentials were notified about.
141
213
  const emittedNotificationSignals: Array<{
214
+ sourceEventName?: string;
215
+ sourceChannel?: string;
142
216
  sourceContextId: string;
143
217
  dedupeKey: string;
218
+ attentionHints?: Record<string, unknown>;
144
219
  contextPayload: Record<string, unknown>;
220
+ conversationAffinityHint?: Record<string, string>;
221
+ conversationMetadata?: Record<string, unknown>;
145
222
  }> = [];
146
223
 
147
224
  mock.module("../notifications/emit-signal.js", () => ({
148
225
  emitNotificationSignal: async (opts: {
226
+ sourceEventName?: string;
227
+ sourceChannel?: string;
149
228
  sourceContextId: string;
150
229
  dedupeKey: string;
230
+ attentionHints?: Record<string, unknown>;
151
231
  contextPayload: Record<string, unknown>;
232
+ conversationAffinityHint?: Record<string, string>;
233
+ conversationMetadata?: Record<string, unknown>;
152
234
  }) => {
153
235
  emittedNotificationSignals.push({
236
+ sourceEventName: opts.sourceEventName,
237
+ sourceChannel: opts.sourceChannel,
154
238
  sourceContextId: opts.sourceContextId,
155
239
  dedupeKey: opts.dedupeKey,
240
+ attentionHints: opts.attentionHints,
156
241
  contextPayload: opts.contextPayload,
242
+ conversationAffinityHint: opts.conversationAffinityHint,
243
+ conversationMetadata: opts.conversationMetadata,
157
244
  });
158
245
  },
159
246
  }));
@@ -258,11 +345,15 @@ describe("HeartbeatService", () => {
258
345
  alerterCalls = [];
259
346
  createdConversations.length = 0;
260
347
  conversationIdCounter = 0;
348
+ mockStoredMessages.length = 0;
261
349
  mockGuardianPersona = null;
262
350
  mockCredentialHealthReport = null;
263
351
  mockCheckAllCredentialsFail = false;
264
352
  emittedNotificationSignals.length = 0;
265
353
  loggerWarnCalls.length = 0;
354
+ mockComputeNextRunAtResult = null;
355
+ mockComputeNextRunAtError = null;
356
+ computeNextRunAtCallCount = 0;
266
357
 
267
358
  // Default processMessage mock: capture calls for assertions.
268
359
  setTestProcessMessage(async (...args: unknown[]) => {
@@ -274,10 +365,33 @@ describe("HeartbeatService", () => {
274
365
  return { messageId: "msg-1" };
275
366
  });
276
367
 
368
+ mockInsertPendingHeartbeatRun.mockClear();
369
+ mockInsertPendingHeartbeatRun.mockImplementation(() => "mock-run-id");
370
+ mockStartHeartbeatRun.mockClear();
371
+ mockStartHeartbeatRun.mockImplementation(() => true);
372
+ mockCompleteHeartbeatRun.mockClear();
373
+ mockCompleteHeartbeatRun.mockImplementation(() => true);
374
+ mockSkipHeartbeatRun.mockClear();
375
+ mockSkipHeartbeatRun.mockImplementation(() => true);
376
+ mockSupersedePendingRun.mockClear();
377
+ mockSupersedePendingRun.mockImplementation(() => true);
378
+ mockMarkStaleRunsAsMissed.mockClear();
379
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
380
+ mockMarkStaleRunningAsError.mockClear();
381
+ mockMarkStaleRunningAsError.mockImplementation(() => 0);
382
+ mockListHeartbeatRuns.mockClear();
383
+ mockListHeartbeatRuns.mockImplementation(() => []);
384
+ mockCountCompletedHeartbeatRuns.mockClear();
385
+ mockCountCompletedHeartbeatRuns.mockImplementation(() => 10);
386
+ mockEmitFeedEvent.mockClear();
387
+ mockEmitFeedEvent.mockImplementation(() => Promise.resolve());
388
+
277
389
  mockConfig = {
278
390
  heartbeat: {
279
391
  enabled: true,
280
392
  intervalMs: 60_000,
393
+ cronExpression: null,
394
+ timezone: null,
281
395
  activeHoursStart: undefined,
282
396
  activeHoursEnd: undefined,
283
397
  },
@@ -287,6 +401,10 @@ describe("HeartbeatService", () => {
287
401
  function createService(overrides?: {
288
402
  processMessage?: (...args: unknown[]) => Promise<{ messageId: string }>;
289
403
  getCurrentHour?: () => number;
404
+ onConversationCreated?: (info: {
405
+ conversationId: string;
406
+ title: string;
407
+ }) => void;
290
408
  }) {
291
409
  if (overrides?.processMessage) {
292
410
  setTestProcessMessage(overrides.processMessage);
@@ -295,6 +413,7 @@ describe("HeartbeatService", () => {
295
413
  alerter: (alert: { type: string; title: string; body: string }) => {
296
414
  alerterCalls.push(alert);
297
415
  },
416
+ onConversationCreated: overrides?.onConversationCreated,
298
417
  getCurrentHour: overrides?.getCurrentHour,
299
418
  });
300
419
  }
@@ -639,6 +758,149 @@ describe("HeartbeatService", () => {
639
758
  });
640
759
  });
641
760
 
761
+ test("HEARTBEAT_ALERT emits a notification signal and surfaces the conversation", async () => {
762
+ const conversationCreatedCalls: Array<{
763
+ conversationId: string;
764
+ title: string;
765
+ }> = [];
766
+ const service = createService({
767
+ onConversationCreated: (info) => conversationCreatedCalls.push(info),
768
+ processMessage: async (...args: unknown[]) => {
769
+ const conversationId = args[0] as string;
770
+ mockStoredMessages.push({
771
+ id: "assistant-alert-1",
772
+ conversationId,
773
+ role: "assistant",
774
+ content: JSON.stringify([
775
+ {
776
+ type: "text",
777
+ text: "The first heartbeat found a concrete follow-up for the guardian.\nHEARTBEAT_ALERT",
778
+ },
779
+ ]),
780
+ createdAt: Date.now(),
781
+ metadata: null,
782
+ });
783
+ return { messageId: "user-heartbeat-1" };
784
+ },
785
+ });
786
+
787
+ await service.runOnce();
788
+ await new Promise((resolve) => setTimeout(resolve, 0));
789
+
790
+ expect(conversationCreatedCalls).toEqual([
791
+ { conversationId: "conv-1", title: "Heartbeat" },
792
+ ]);
793
+ expect(emittedNotificationSignals).toHaveLength(1);
794
+ expect(emittedNotificationSignals[0]).toMatchObject({
795
+ sourceEventName: "heartbeat.alert",
796
+ sourceChannel: "watcher",
797
+ sourceContextId: "mock-run-id",
798
+ dedupeKey: "heartbeat:alert:mock-run-id",
799
+ attentionHints: {
800
+ requiresAction: true,
801
+ urgency: "medium",
802
+ isAsyncBackground: true,
803
+ visibleInSourceNow: false,
804
+ },
805
+ conversationAffinityHint: { vellum: "conv-1" },
806
+ conversationMetadata: {
807
+ source: "heartbeat",
808
+ groupId: "system:background",
809
+ },
810
+ });
811
+ expect(emittedNotificationSignals[0].contextPayload.summary).toBe(
812
+ "The first heartbeat found a concrete follow-up for the guardian.",
813
+ );
814
+ expect(emittedNotificationSignals[0].contextPayload.messageId).toBe(
815
+ "assistant-alert-1",
816
+ );
817
+ expect(
818
+ emittedNotificationSignals[0].contextPayload.sourceInterface,
819
+ ).toBeUndefined();
820
+ });
821
+
822
+ test("HEARTBEAT_OK stays silent", async () => {
823
+ const conversationCreatedCalls: Array<{
824
+ conversationId: string;
825
+ title: string;
826
+ }> = [];
827
+ const service = createService({
828
+ onConversationCreated: (info) => conversationCreatedCalls.push(info),
829
+ processMessage: async (...args: unknown[]) => {
830
+ const conversationId = args[0] as string;
831
+ mockStoredMessages.push({
832
+ id: "assistant-ok-1",
833
+ conversationId,
834
+ role: "assistant",
835
+ content: JSON.stringify([
836
+ {
837
+ type: "text",
838
+ text: "Everything looks good.\nHEARTBEAT_OK",
839
+ },
840
+ ]),
841
+ createdAt: Date.now(),
842
+ metadata: null,
843
+ });
844
+ return { messageId: "user-heartbeat-1" };
845
+ },
846
+ });
847
+
848
+ await service.runOnce();
849
+ await new Promise((resolve) => setTimeout(resolve, 0));
850
+
851
+ expect(conversationCreatedCalls).toHaveLength(0);
852
+ expect(emittedNotificationSignals).toHaveLength(0);
853
+ const successFeedCalls = mockEmitFeedEvent.mock.calls.filter(
854
+ (call: unknown[]) => {
855
+ const opts = call[0] as { dedupKey?: string };
856
+ return opts.dedupKey?.startsWith("heartbeat:ok:");
857
+ },
858
+ );
859
+ expect(successFeedCalls).toHaveLength(0);
860
+ });
861
+
862
+ test("HEARTBEAT_OK stays silent when earlier content mentions HEARTBEAT_ALERT", async () => {
863
+ const conversationCreatedCalls: Array<{
864
+ conversationId: string;
865
+ title: string;
866
+ }> = [];
867
+ const service = createService({
868
+ onConversationCreated: (info) => conversationCreatedCalls.push(info),
869
+ processMessage: async (...args: unknown[]) => {
870
+ const conversationId = args[0] as string;
871
+ mockStoredMessages.push({
872
+ id: "assistant-ok-2",
873
+ conversationId,
874
+ role: "assistant",
875
+ content: JSON.stringify([
876
+ {
877
+ type: "thinking",
878
+ thinking:
879
+ "I should decide between HEARTBEAT_ALERT and HEARTBEAT_OK.",
880
+ },
881
+ {
882
+ type: "tool_result",
883
+ content: "Tool output mentions HEARTBEAT_ALERT.",
884
+ },
885
+ {
886
+ type: "text",
887
+ text: "I considered HEARTBEAT_ALERT, but there is nothing useful to surface.\nHEARTBEAT_OK",
888
+ },
889
+ ]),
890
+ createdAt: Date.now(),
891
+ metadata: null,
892
+ });
893
+ return { messageId: "user-heartbeat-1" };
894
+ },
895
+ });
896
+
897
+ await service.runOnce();
898
+ await new Promise((resolve) => setTimeout(resolve, 0));
899
+
900
+ expect(conversationCreatedCalls).toHaveLength(0);
901
+ expect(emittedNotificationSignals).toHaveLength(0);
902
+ });
903
+
642
904
  test("end-to-end: llm.callSites.heartbeatAgent.speed resolves to 'fast'", async () => {
643
905
  // Verifies the contract that PR 7 establishes: heartbeat passes
644
906
  // `callSite: 'heartbeatAgent'`, and the LLM resolver maps that to the
@@ -1053,4 +1315,708 @@ describe("HeartbeatService", () => {
1053
1315
  expect(unreachableWarns[0].unreachableCount).toBe(2);
1054
1316
  });
1055
1317
  });
1318
+
1319
+ describe("cron scheduling mode", () => {
1320
+ test("start() with cronExpression sets nextRunAt to cron occurrence, not now+intervalMs", () => {
1321
+ const cronNextRunAt = Date.now() + 7_200_000; // 2 hours from now
1322
+ mockComputeNextRunAtResult = cronNextRunAt;
1323
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1324
+ mockConfig.heartbeat.timezone = "America/New_York";
1325
+
1326
+ const service = createService();
1327
+ service.start();
1328
+
1329
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1330
+ // Should NOT be now + intervalMs
1331
+ expect(service.nextRunAt).not.toBeCloseTo(
1332
+ Date.now() + mockConfig.heartbeat.intervalMs,
1333
+ -3,
1334
+ );
1335
+ service.stop();
1336
+ });
1337
+
1338
+ test("runOnce() does not call scheduleNextRun(intervalMs) in cron mode — nextRunAt is not clobbered", async () => {
1339
+ const cronNextRunAt = Date.now() + 7_200_000;
1340
+ mockComputeNextRunAtResult = cronNextRunAt;
1341
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1342
+
1343
+ const service = createService();
1344
+ service.start();
1345
+
1346
+ // nextRunAt should be the cron time before runOnce
1347
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1348
+
1349
+ await service.runOnce();
1350
+
1351
+ // After runOnce(), nextRunAt should still reflect a cron time, not now + intervalMs.
1352
+ // The finally chain in scheduleNextCronRun recalculates it, but the runOnce()
1353
+ // finally block should NOT have called scheduleNextRun(intervalMs).
1354
+ // Since our mock always returns cronNextRunAt, nextRunAt should remain that value.
1355
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1356
+ service.stop();
1357
+ });
1358
+
1359
+ test("after runOnce() rejects in cron mode, the next cron run is still scheduled via finally", async () => {
1360
+ const cronNextRunAt = Date.now() + 7_200_000;
1361
+ mockComputeNextRunAtResult = cronNextRunAt;
1362
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1363
+
1364
+ const service = createService({
1365
+ processMessage: async () => {
1366
+ throw new Error("LLM down");
1367
+ },
1368
+ });
1369
+ service.start();
1370
+
1371
+ await service.runOnce();
1372
+
1373
+ // Even though executeRun failed, the service should still have a nextRunAt
1374
+ // set to the cron occurrence (the finally chain reschedules)
1375
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1376
+ service.stop();
1377
+ });
1378
+
1379
+ test("resetTimer() in cron mode recomputes from the current time", () => {
1380
+ const firstCronTime = Date.now() + 3_600_000;
1381
+ mockComputeNextRunAtResult = firstCronTime;
1382
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1383
+
1384
+ const service = createService();
1385
+ service.start();
1386
+ expect(service.nextRunAt).toBe(firstCronTime);
1387
+
1388
+ // Simulate time passing and a new cron occurrence
1389
+ const secondCronTime = Date.now() + 5_400_000;
1390
+ mockComputeNextRunAtResult = secondCronTime;
1391
+
1392
+ service.resetTimer();
1393
+ expect(service.nextRunAt).toBe(secondCronTime);
1394
+ service.stop();
1395
+ });
1396
+
1397
+ test("reconfigure() switches from interval to cron mode", () => {
1398
+ const service = createService();
1399
+ // Start in interval mode
1400
+ service.start();
1401
+ const intervalNextRunAt = service.nextRunAt;
1402
+ expect(intervalNextRunAt).not.toBeNull();
1403
+
1404
+ // Reconfigure to cron mode
1405
+ const cronNextRunAt = Date.now() + 7_200_000;
1406
+ mockComputeNextRunAtResult = cronNextRunAt;
1407
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1408
+ service.reconfigure();
1409
+
1410
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1411
+ service.stop();
1412
+ });
1413
+
1414
+ test("reconfigure() switches from cron to interval mode", () => {
1415
+ const cronNextRunAt = Date.now() + 7_200_000;
1416
+ mockComputeNextRunAtResult = cronNextRunAt;
1417
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1418
+
1419
+ const service = createService();
1420
+ service.start();
1421
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1422
+
1423
+ // Reconfigure to interval mode
1424
+ mockConfig.heartbeat.cronExpression = null;
1425
+ const before = Date.now();
1426
+ service.reconfigure();
1427
+
1428
+ expect(service.nextRunAt).not.toBeNull();
1429
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(
1430
+ before + mockConfig.heartbeat.intervalMs,
1431
+ );
1432
+ service.stop();
1433
+ });
1434
+
1435
+ test("active hours guard uses cron timezone when configured", async () => {
1436
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1437
+ mockConfig.heartbeat.timezone = "UTC";
1438
+ mockConfig.heartbeat.activeHoursStart = 9;
1439
+ mockConfig.heartbeat.activeHoursEnd = 17;
1440
+ mockComputeNextRunAtResult = Date.now() + 3_600_000;
1441
+
1442
+ const service = createService();
1443
+ service.start();
1444
+
1445
+ // In cron mode with timezone, the hour is computed via Intl.DateTimeFormat
1446
+ // rather than getCurrentHour(). The test verifies the code path runs without
1447
+ // error — the actual hour depends on the system clock and UTC conversion.
1448
+ // We just verify it doesn't throw and returns a boolean result.
1449
+ const result = await service.runOnce();
1450
+ // Result depends on current UTC hour vs active window — either outcome is valid
1451
+ expect(typeof result).toBe("boolean");
1452
+ service.stop();
1453
+ });
1454
+
1455
+ test("active hours guard falls back to getCurrentHour when cron mode has no timezone", async () => {
1456
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1457
+ mockConfig.heartbeat.timezone = null;
1458
+ mockConfig.heartbeat.activeHoursStart = 9;
1459
+ mockConfig.heartbeat.activeHoursEnd = 17;
1460
+ mockComputeNextRunAtResult = Date.now() + 3_600_000;
1461
+
1462
+ // getCurrentHour returns 3 (outside 9-17 window), so runOnce should skip
1463
+ const service = createService({ getCurrentHour: () => 3 });
1464
+ service.start();
1465
+ const result = await service.runOnce();
1466
+ expect(result).toBe(false);
1467
+ expect(processMessageCalls).toHaveLength(0);
1468
+ service.stop();
1469
+ });
1470
+
1471
+ test("runtime fallback: computeNextRunAt throws, service falls back to interval mode", () => {
1472
+ mockComputeNextRunAtError = new Error("No upcoming runs");
1473
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1474
+
1475
+ const service = createService();
1476
+ service.start();
1477
+
1478
+ // Should have fallen back to interval mode — nextRunAt should be ~now + intervalMs
1479
+ expect(service.nextRunAt).not.toBeNull();
1480
+ const expectedMin = Date.now() + mockConfig.heartbeat.intervalMs - 100;
1481
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(expectedMin);
1482
+
1483
+ // Should have logged a warning about the fallback
1484
+ const fallbackWarns = loggerWarnCalls.filter((call) => "err" in call);
1485
+ expect(fallbackWarns.length).toBeGreaterThanOrEqual(1);
1486
+ service.stop();
1487
+ });
1488
+
1489
+ test("null cronExpression behaves identically to current fixed-interval mode", () => {
1490
+ mockConfig.heartbeat.cronExpression = null;
1491
+
1492
+ const service = createService();
1493
+ const before = Date.now();
1494
+ service.start();
1495
+
1496
+ expect(service.nextRunAt).not.toBeNull();
1497
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(
1498
+ before + mockConfig.heartbeat.intervalMs,
1499
+ );
1500
+ // computeNextRunAt should not have been called
1501
+ expect(computeNextRunAtCallCount).toBe(0);
1502
+ service.stop();
1503
+ });
1504
+ });
1505
+
1506
+ describe("heartbeat run store instrumentation", () => {
1507
+ test("successful run: pending → running → ok with conversationId", async () => {
1508
+ const service = createService();
1509
+ await service.runOnce();
1510
+
1511
+ expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
1512
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
1513
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1514
+ status: "ok",
1515
+ conversationId: "conv-1",
1516
+ });
1517
+ });
1518
+
1519
+ test("failed run: pending → running → error preserving conversationId", async () => {
1520
+ const service = createService({
1521
+ processMessage: async () => {
1522
+ throw new Error("LLM timeout");
1523
+ },
1524
+ });
1525
+
1526
+ await service.runOnce();
1527
+
1528
+ expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
1529
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
1530
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1531
+ status: "error",
1532
+ conversationId: "conv-1",
1533
+ error: "LLM timeout",
1534
+ });
1535
+ });
1536
+
1537
+ test("CAS false suppresses success surfacing", async () => {
1538
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1539
+
1540
+ const conversationCreatedCalls: Array<{
1541
+ conversationId: string;
1542
+ title: string;
1543
+ }> = [];
1544
+ const service = createService({
1545
+ onConversationCreated: (info) => conversationCreatedCalls.push(info),
1546
+ processMessage: async (...args: unknown[]) => {
1547
+ const conversationId = args[0] as string;
1548
+ mockStoredMessages.push({
1549
+ id: "assistant-alert-1",
1550
+ conversationId,
1551
+ role: "assistant",
1552
+ content: JSON.stringify([
1553
+ {
1554
+ type: "text",
1555
+ text: "Something worth surfacing.\nHEARTBEAT_ALERT",
1556
+ },
1557
+ ]),
1558
+ createdAt: Date.now(),
1559
+ metadata: null,
1560
+ });
1561
+ return { messageId: "msg-1" };
1562
+ },
1563
+ });
1564
+ await service.runOnce();
1565
+ await new Promise((resolve) => setTimeout(resolve, 0));
1566
+
1567
+ expect(conversationCreatedCalls).toHaveLength(0);
1568
+ expect(emittedNotificationSignals).toHaveLength(0);
1569
+ });
1570
+
1571
+ test("CAS false suppresses failure alerter and feed event", async () => {
1572
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1573
+
1574
+ const service = createService({
1575
+ processMessage: async () => {
1576
+ throw new Error("LLM timeout");
1577
+ },
1578
+ });
1579
+
1580
+ await service.runOnce();
1581
+
1582
+ // completeHeartbeatRun returned false, so alerter should NOT be called
1583
+ expect(alerterCalls).toHaveLength(0);
1584
+
1585
+ // No failure feed event either
1586
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1587
+ (call: unknown[]) => {
1588
+ const opts = call[0] as { dedupKey?: string };
1589
+ return opts.dedupKey?.startsWith("heartbeat:fail:");
1590
+ },
1591
+ );
1592
+ expect(failCalls).toHaveLength(0);
1593
+ });
1594
+
1595
+ test("active-hours skip calls skipHeartbeatRun", async () => {
1596
+ mockConfig.heartbeat.activeHoursStart = 9;
1597
+ mockConfig.heartbeat.activeHoursEnd = 17;
1598
+
1599
+ const service = createService({ getCurrentHour: () => 3 });
1600
+ service.start();
1601
+ await service.runOnce();
1602
+
1603
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1604
+ "mock-run-id",
1605
+ "outside_active_hours",
1606
+ );
1607
+ service.stop();
1608
+ });
1609
+
1610
+ test("overlap skip calls skipHeartbeatRun", async () => {
1611
+ let resolveFirst: () => void;
1612
+ const firstPromise = new Promise<void>((r) => {
1613
+ resolveFirst = r;
1614
+ });
1615
+
1616
+ const service = createService({
1617
+ processMessage: async () => {
1618
+ await firstPromise;
1619
+ return { messageId: "msg-1" };
1620
+ },
1621
+ });
1622
+
1623
+ // Start first run (will block)
1624
+ const run1 = service.runOnce();
1625
+ await new Promise((r) => setTimeout(r, 10));
1626
+
1627
+ // Start service so the second runOnce has a pending row
1628
+ service.start();
1629
+ mockSkipHeartbeatRun.mockClear();
1630
+
1631
+ // Second run should be skipped due to overlap
1632
+ await service.runOnce();
1633
+
1634
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1635
+ "mock-run-id",
1636
+ "overlap",
1637
+ );
1638
+
1639
+ resolveFirst!();
1640
+ await run1;
1641
+ service.stop();
1642
+ });
1643
+
1644
+ test("start() calls markStaleRunsAsMissed and markStaleRunningAsError", () => {
1645
+ const service = createService();
1646
+ service.start();
1647
+
1648
+ expect(mockMarkStaleRunsAsMissed).toHaveBeenCalledTimes(1);
1649
+ expect(mockMarkStaleRunningAsError).toHaveBeenCalledTimes(1);
1650
+ service.stop();
1651
+ });
1652
+
1653
+ test("scheduleNextRun supersedes old pending row before creating new one", () => {
1654
+ const service = createService();
1655
+ service.start();
1656
+
1657
+ // start() called scheduleNextRun which set _pendingRunId.
1658
+ // Calling resetTimer triggers another scheduleNextRun which
1659
+ // should supersede the existing pending row before inserting
1660
+ // a new one.
1661
+ const callOrder: string[] = [];
1662
+ mockSupersedePendingRun.mockImplementation(() => {
1663
+ callOrder.push("supersede");
1664
+ return true;
1665
+ });
1666
+ mockInsertPendingHeartbeatRun.mockImplementation(() => {
1667
+ callOrder.push("insert");
1668
+ return "mock-run-id";
1669
+ });
1670
+
1671
+ service.resetTimer();
1672
+
1673
+ // resetTimer's scheduleNextRun should supersede then insert
1674
+ expect(callOrder.filter((c) => c === "supersede").length).toBeGreaterThan(
1675
+ 0,
1676
+ );
1677
+ const firstSupersede = callOrder.indexOf("supersede");
1678
+ const firstInsert = callOrder.indexOf("insert");
1679
+ expect(firstSupersede).toBeLessThan(firstInsert);
1680
+
1681
+ service.stop();
1682
+ });
1683
+
1684
+ test("resetTimer() supersedes pending row", () => {
1685
+ const service = createService();
1686
+ service.start();
1687
+
1688
+ mockSupersedePendingRun.mockClear();
1689
+ service.resetTimer();
1690
+
1691
+ // resetTimer calls scheduleNextRun which supersedes existing pending
1692
+ expect(mockSupersedePendingRun).toHaveBeenCalled();
1693
+ service.stop();
1694
+ });
1695
+
1696
+ test("force run creates its own pending row, does not consume scheduled one", async () => {
1697
+ const service = createService();
1698
+ service.start();
1699
+
1700
+ // Clear to track only the force run's calls
1701
+ mockInsertPendingHeartbeatRun.mockClear();
1702
+
1703
+ await service.runOnce({ force: true });
1704
+
1705
+ // Force run should have called insertPendingHeartbeatRun for itself
1706
+ // (at least once for its own row, plus the scheduleNextRun in finally)
1707
+ expect(mockInsertPendingHeartbeatRun).toHaveBeenCalled();
1708
+
1709
+ // The scheduled pending row (from start()) should NOT have been consumed
1710
+ // by the force run — force creates its own
1711
+ service.stop();
1712
+ });
1713
+
1714
+ test("disabled config with stale pending row skips it as disabled", async () => {
1715
+ const service = createService();
1716
+ service.start();
1717
+
1718
+ // Now disable config and call runOnce — should skip the pending row
1719
+ mockConfig.heartbeat.enabled = false;
1720
+ mockSkipHeartbeatRun.mockClear();
1721
+
1722
+ await service.runOnce();
1723
+
1724
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1725
+ "mock-run-id",
1726
+ "disabled",
1727
+ );
1728
+ service.stop();
1729
+ });
1730
+
1731
+ test("stop() supersedes outstanding pending row", async () => {
1732
+ const service = createService();
1733
+ service.start();
1734
+
1735
+ mockSupersedePendingRun.mockClear();
1736
+ await service.stop();
1737
+
1738
+ expect(mockSupersedePendingRun).toHaveBeenCalledWith("mock-run-id");
1739
+ });
1740
+
1741
+ test("timeout calls completeHeartbeatRun with status timeout", async () => {
1742
+ jest.useFakeTimers();
1743
+ try {
1744
+ let resolveRun: () => void;
1745
+ const runPromise = new Promise<void>((r) => {
1746
+ resolveRun = r;
1747
+ });
1748
+
1749
+ const service = createService({
1750
+ processMessage: async () => {
1751
+ await runPromise;
1752
+ return { messageId: "msg-1" };
1753
+ },
1754
+ });
1755
+
1756
+ const runOncePromise = service.runOnce();
1757
+ // Advance past the 30-minute timeout
1758
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1759
+ await runOncePromise;
1760
+
1761
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1762
+ status: "timeout",
1763
+ error: "Heartbeat execution exceeded the 30-minute timeout",
1764
+ });
1765
+
1766
+ // Clean up — resolve the hanging promise so it doesn't leak
1767
+ resolveRun!();
1768
+ } finally {
1769
+ jest.useRealTimers();
1770
+ }
1771
+ });
1772
+
1773
+ test("failure feed event has urgency high and includes error message", async () => {
1774
+ const service = createService({
1775
+ processMessage: async () => {
1776
+ throw new Error("web_search outage");
1777
+ },
1778
+ });
1779
+
1780
+ await service.runOnce();
1781
+
1782
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1783
+ (call: unknown[]) => {
1784
+ const opts = call[0] as { title?: string };
1785
+ return opts.title === "Heartbeat Failed";
1786
+ },
1787
+ );
1788
+ expect(failCalls).toHaveLength(1);
1789
+ const opts = (failCalls as any[][])[0][0] as {
1790
+ urgency?: string;
1791
+ summary?: string;
1792
+ };
1793
+ expect(opts.urgency).toBe("high");
1794
+ expect(opts.summary).toContain("web_search outage");
1795
+ });
1796
+
1797
+ test("CAS false on complete suppresses failure feed event", async () => {
1798
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1799
+
1800
+ const service = createService({
1801
+ processMessage: async () => {
1802
+ throw new Error("some error");
1803
+ },
1804
+ });
1805
+
1806
+ await service.runOnce();
1807
+
1808
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1809
+ (call: unknown[]) => {
1810
+ const opts = call[0] as { title?: string };
1811
+ return opts.title === "Heartbeat Failed";
1812
+ },
1813
+ );
1814
+ expect(failCalls).toHaveLength(0);
1815
+ });
1816
+
1817
+ test("timeout emits feed event with urgency high", async () => {
1818
+ jest.useFakeTimers();
1819
+ try {
1820
+ let resolveRun: () => void;
1821
+ const runPromise = new Promise<void>((r) => {
1822
+ resolveRun = r;
1823
+ });
1824
+
1825
+ const service = createService({
1826
+ processMessage: async () => {
1827
+ await runPromise;
1828
+ return { messageId: "msg-1" };
1829
+ },
1830
+ });
1831
+
1832
+ const runOncePromise = service.runOnce();
1833
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1834
+ await runOncePromise;
1835
+
1836
+ const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
1837
+ (call: unknown[]) => {
1838
+ const opts = call[0] as { title?: string };
1839
+ return opts.title === "Heartbeat Timed Out";
1840
+ },
1841
+ );
1842
+ expect(timeoutCalls).toHaveLength(1);
1843
+ const opts = (timeoutCalls as any[][])[0][0] as {
1844
+ urgency?: string;
1845
+ };
1846
+ expect(opts.urgency).toBe("high");
1847
+
1848
+ resolveRun!();
1849
+ } finally {
1850
+ jest.useRealTimers();
1851
+ }
1852
+ });
1853
+
1854
+ test("CAS false on timeout suppresses timeout feed event", async () => {
1855
+ jest.useFakeTimers();
1856
+ try {
1857
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1858
+
1859
+ let resolveRun: () => void;
1860
+ const runPromise = new Promise<void>((r) => {
1861
+ resolveRun = r;
1862
+ });
1863
+
1864
+ const service = createService({
1865
+ processMessage: async () => {
1866
+ await runPromise;
1867
+ return { messageId: "msg-1" };
1868
+ },
1869
+ });
1870
+
1871
+ const runOncePromise = service.runOnce();
1872
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1873
+ await runOncePromise;
1874
+
1875
+ // completeHeartbeatRun returned false, so no timeout feed event
1876
+ const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
1877
+ (call: unknown[]) => {
1878
+ const opts = call[0] as { title?: string };
1879
+ return opts.title === "Heartbeat Timed Out";
1880
+ },
1881
+ );
1882
+ expect(timeoutCalls).toHaveLength(0);
1883
+
1884
+ resolveRun!();
1885
+ } finally {
1886
+ jest.useRealTimers();
1887
+ }
1888
+ });
1889
+
1890
+ test("late run emits late feed event", async () => {
1891
+ const service = createService();
1892
+ service.start();
1893
+
1894
+ // Set the pending run to be 10 minutes in the past
1895
+ (service as any)._nextRunAt = Date.now() - 10 * 60 * 1000;
1896
+ (service as any)._pendingRunId = "late-run-id";
1897
+
1898
+ await service.runOnce();
1899
+
1900
+ const lateCalls = mockEmitFeedEvent.mock.calls.filter(
1901
+ (call: unknown[]) => {
1902
+ const opts = call[0] as { title?: string };
1903
+ return opts.title === "Heartbeat Ran Late";
1904
+ },
1905
+ );
1906
+ expect(lateCalls).toHaveLength(1);
1907
+ const opts = (lateCalls as any[][])[0][0] as {
1908
+ urgency?: string;
1909
+ summary?: string;
1910
+ };
1911
+ expect(opts.urgency).toBe("medium");
1912
+ expect(opts.summary).toContain("10 minutes late");
1913
+
1914
+ await service.stop();
1915
+ });
1916
+
1917
+ test("on-time run does not emit late feed event", async () => {
1918
+ const service = createService();
1919
+ await service.runOnce();
1920
+
1921
+ const lateCalls = mockEmitFeedEvent.mock.calls.filter(
1922
+ (call: unknown[]) => {
1923
+ const opts = call[0] as { title?: string };
1924
+ return opts.title === "Heartbeat Ran Late";
1925
+ },
1926
+ );
1927
+ expect(lateCalls).toHaveLength(0);
1928
+ });
1929
+
1930
+ test("start() emits missed-run feed event when stale rows exist", () => {
1931
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 2);
1932
+ mockMarkStaleRunningAsError.mockImplementation(() => 1);
1933
+
1934
+ const service = createService();
1935
+ service.start();
1936
+
1937
+ const missedCalls = mockEmitFeedEvent.mock.calls.filter(
1938
+ (call: unknown[]) => {
1939
+ const opts = call[0] as { title?: string };
1940
+ return opts.title === "Heartbeat Runs Missed";
1941
+ },
1942
+ );
1943
+ expect(missedCalls).toHaveLength(1);
1944
+ const opts = (missedCalls as any[][])[0][0] as {
1945
+ urgency?: string;
1946
+ summary?: string;
1947
+ };
1948
+ expect(opts.urgency).toBe("high");
1949
+ expect(opts.summary).toContain("3");
1950
+
1951
+ service.stop();
1952
+ });
1953
+
1954
+ test("start() does not emit missed-run feed event when counts are 0", () => {
1955
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
1956
+ mockMarkStaleRunningAsError.mockImplementation(() => 0);
1957
+
1958
+ const service = createService();
1959
+ service.start();
1960
+
1961
+ const missedCalls = mockEmitFeedEvent.mock.calls.filter(
1962
+ (call: unknown[]) => {
1963
+ const opts = call[0] as { title?: string };
1964
+ return opts.title === "Heartbeat Runs Missed";
1965
+ },
1966
+ );
1967
+ expect(missedCalls).toHaveLength(0);
1968
+ service.stop();
1969
+ });
1970
+ });
1971
+
1972
+ describe("early heartbeat nudge", () => {
1973
+ test("includes <early-heartbeat> when completedRunCount is 0", () => {
1974
+ const service = createService();
1975
+ const { prompt } = service.buildPrompt("- Check things", [], 0);
1976
+
1977
+ expect(prompt).toContain("<early-heartbeat>");
1978
+ expect(prompt).toContain("first heartbeats");
1979
+ });
1980
+
1981
+ test("includes <early-heartbeat> when completedRunCount is 2", () => {
1982
+ const service = createService();
1983
+ const { prompt } = service.buildPrompt("- Check things", [], 2);
1984
+
1985
+ expect(prompt).toContain("<early-heartbeat>");
1986
+ });
1987
+
1988
+ test("omits <early-heartbeat> when completedRunCount is 3", () => {
1989
+ const service = createService();
1990
+ const { prompt } = service.buildPrompt("- Check things", [], 3);
1991
+
1992
+ expect(prompt).not.toContain("<early-heartbeat>");
1993
+ });
1994
+
1995
+ test("omits <early-heartbeat> when completedRunCount is 10", () => {
1996
+ const service = createService();
1997
+ const { prompt } = service.buildPrompt("- Check things", [], 10);
1998
+
1999
+ expect(prompt).not.toContain("<early-heartbeat>");
2000
+ });
2001
+
2002
+ test("executeRun passes completed run count to buildPrompt", async () => {
2003
+ mockCountCompletedHeartbeatRuns.mockImplementation(() => 0);
2004
+
2005
+ const service = createService();
2006
+ await service.runOnce();
2007
+
2008
+ expect(processMessageCalls).toHaveLength(1);
2009
+ expect(processMessageCalls[0].content).toContain("<early-heartbeat>");
2010
+ });
2011
+
2012
+ test("executeRun omits nudge when enough runs have completed", async () => {
2013
+ mockCountCompletedHeartbeatRuns.mockImplementation(() => 5);
2014
+
2015
+ const service = createService();
2016
+ await service.runOnce();
2017
+
2018
+ expect(processMessageCalls).toHaveLength(1);
2019
+ expect(processMessageCalls[0].content).not.toContain("<early-heartbeat>");
2020
+ });
2021
+ });
1056
2022
  });