@vellumai/assistant 0.7.0 → 0.7.2

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 (989) hide show
  1. package/ARCHITECTURE.md +38 -56
  2. package/Dockerfile +2 -0
  3. package/README.md +3 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +88 -142
  5. package/bun.lock +29 -26
  6. package/docs/architecture/security.md +38 -16
  7. package/docs/plugins.md +7 -9
  8. package/knip.json +2 -0
  9. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -0
  10. package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +39 -1
  11. package/node_modules/@vellumai/gateway-client/src/types.ts +11 -0
  12. package/node_modules/@vellumai/service-contracts/package.json +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +4 -0
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +107 -0
  15. package/node_modules/@vellumai/service-contracts/src/index.ts +5 -1
  16. package/node_modules/@vellumai/service-contracts/src/ingress.ts +24 -0
  17. package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +84 -0
  18. package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +1 -5
  19. package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +9 -5
  20. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -16
  21. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +1 -9
  22. package/node_modules/@vellumai/skill-host-contracts/src/tool-types.ts +12 -12
  23. package/node_modules/@vellumai/slack-text/bun.lock +24 -0
  24. package/node_modules/@vellumai/slack-text/package.json +18 -0
  25. package/node_modules/@vellumai/slack-text/src/index.test.ts +153 -0
  26. package/node_modules/@vellumai/slack-text/src/index.ts +235 -0
  27. package/node_modules/@vellumai/slack-text/tsconfig.json +20 -0
  28. package/node_modules/@vellumai/twilio-client/bun.lock +24 -0
  29. package/node_modules/@vellumai/twilio-client/package.json +18 -0
  30. package/node_modules/@vellumai/twilio-client/src/__tests__/twilio-client.test.ts +128 -0
  31. package/node_modules/@vellumai/twilio-client/src/index.ts +179 -0
  32. package/node_modules/@vellumai/twilio-client/tsconfig.json +20 -0
  33. package/openapi.yaml +869 -129
  34. package/package.json +8 -3
  35. package/scripts/generate-openapi.ts +16 -111
  36. package/src/__tests__/agent-wake-override-profile.test.ts +23 -1
  37. package/src/__tests__/anthropic-provider.test.ts +56 -13
  38. package/src/__tests__/app-builder-tool-scripts.test.ts +3 -3
  39. package/src/__tests__/app-bundler.test.ts +170 -1
  40. package/src/__tests__/app-control-flow.test.ts +374 -0
  41. package/src/__tests__/app-control-no-global-cgevent.test.ts +98 -0
  42. package/src/__tests__/app-control-tool-schemas.test.ts +621 -0
  43. package/src/__tests__/app-conversation-ids-backfill.test.ts +278 -0
  44. package/src/__tests__/app-conversation-ids.test.ts +151 -0
  45. package/src/__tests__/app-executors.test.ts +30 -43
  46. package/src/__tests__/approval-cascade.test.ts +0 -15
  47. package/src/__tests__/approval-routes-http.test.ts +29 -23
  48. package/src/__tests__/assistant-event-hub-machine-name.test.ts +146 -0
  49. package/src/__tests__/assistant-event-hub-targeted.test.ts +257 -0
  50. package/src/__tests__/assistant-event-hub.test.ts +235 -79
  51. package/src/__tests__/assistant-event.test.ts +10 -5
  52. package/src/__tests__/assistant-events-sse-hardening.test.ts +44 -17
  53. package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -36
  54. package/src/__tests__/background-shell-host-bash.test.ts +46 -56
  55. package/src/__tests__/bootstrap-turn-cleanup.test.ts +44 -0
  56. package/src/__tests__/btw-routes.test.ts +13 -4
  57. package/src/__tests__/call-controller.test.ts +50 -2
  58. package/src/__tests__/call-domain.test.ts +0 -2
  59. package/src/__tests__/call-routes-http.test.ts +0 -2
  60. package/src/__tests__/call-site-routing-provider.test.ts +193 -0
  61. package/src/__tests__/channel-approval-routes.test.ts +10 -296
  62. package/src/__tests__/channel-approvals.test.ts +25 -17
  63. package/src/__tests__/channel-guardian.test.ts +100 -146
  64. package/src/__tests__/channel-readiness-service.test.ts +59 -1
  65. package/src/__tests__/checker.test.ts +23 -38
  66. package/src/__tests__/compact-event-conversation-id-guard.test.ts +50 -0
  67. package/src/__tests__/compaction-events.test.ts +2 -0
  68. package/src/__tests__/config-loader-backfill.test.ts +90 -155
  69. package/src/__tests__/config-loader-platform-defaults.test.ts +196 -0
  70. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  71. package/src/__tests__/config-schema.test.ts +6 -48
  72. package/src/__tests__/config-set-platform-guard.test.ts +48 -4
  73. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
  74. package/src/__tests__/config-watcher.test.ts +14 -2
  75. package/src/__tests__/connection-policy.test.ts +1 -52
  76. package/src/__tests__/contacts-write.test.ts +2 -64
  77. package/src/__tests__/context-image-dimensions.test.ts +1 -1
  78. package/src/__tests__/context-search-memory-source.test.ts +120 -1
  79. package/src/__tests__/context-search-memory-v2-source.test.ts +383 -0
  80. package/src/__tests__/context-search-pkb-source.test.ts +49 -0
  81. package/src/__tests__/context-search-workspace-source.test.ts +9 -22
  82. package/src/__tests__/context-window-manager.test.ts +46 -0
  83. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  84. package/src/__tests__/conversation-agent-loop-overflow.test.ts +102 -29
  85. package/src/__tests__/conversation-agent-loop.test.ts +980 -13
  86. package/src/__tests__/conversation-analysis-routes.test.ts +12 -10
  87. package/src/__tests__/conversation-app-control-instantiation.test.ts +392 -0
  88. package/src/__tests__/conversation-app-control-lifecycle.test.ts +237 -0
  89. package/src/__tests__/conversation-attention-telegram.test.ts +11 -3
  90. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -291
  91. package/src/__tests__/conversation-history-web-search.test.ts +4 -3
  92. package/src/__tests__/conversation-inference-profile-route.test.ts +12 -23
  93. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  94. package/src/__tests__/conversation-lifecycle.test.ts +40 -4
  95. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +283 -0
  96. package/src/__tests__/conversation-process-callsite.test.ts +79 -2
  97. package/src/__tests__/conversation-queue.test.ts +3 -8
  98. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -161
  99. package/src/__tests__/conversation-routes-guardian-reply.test.ts +120 -104
  100. package/src/__tests__/conversation-routes-slash-commands.test.ts +76 -66
  101. package/src/__tests__/conversation-runtime-assembly.test.ts +257 -3
  102. package/src/__tests__/conversation-slash-commands.test.ts +24 -8
  103. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  104. package/src/__tests__/conversation-speed-override.test.ts +0 -3
  105. package/src/__tests__/conversation-starter-routes.test.ts +79 -2
  106. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
  107. package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -0
  108. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +12 -5
  109. package/src/__tests__/conversation-surfaces-standalone.test.ts +18 -14
  110. package/src/__tests__/conversation-surfaces-state-update.test.ts +3 -2
  111. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +8 -46
  112. package/src/__tests__/conversation-usage.test.ts +253 -3
  113. package/src/__tests__/credential-execution-feature-gates.test.ts +5 -12
  114. package/src/__tests__/credential-execution-managed-contract.test.ts +3 -131
  115. package/src/__tests__/credential-execution-shell-lockdown.test.ts +0 -39
  116. package/src/__tests__/credential-health-service.test.ts +68 -0
  117. package/src/__tests__/credential-security-e2e.test.ts +4 -3
  118. package/src/__tests__/credential-security-invariants.test.ts +1 -5
  119. package/src/__tests__/credential-token-resolver.test.ts +180 -0
  120. package/src/__tests__/credentials-cli.test.ts +5 -12
  121. package/src/__tests__/cu-unified-flow.test.ts +206 -27
  122. package/src/__tests__/daemon-assistant-events.test.ts +34 -21
  123. package/src/__tests__/daemon-credential-client.test.ts +102 -17
  124. package/src/__tests__/db-connection-isolation.test.ts +125 -0
  125. package/src/__tests__/db-migration-rollback.test.ts +101 -0
  126. package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
  127. package/src/__tests__/db-slack-compaction-watermark-migration.test.ts +169 -0
  128. package/src/__tests__/deterministic-verification-control-plane.test.ts +7 -80
  129. package/src/__tests__/document-conversations.test.ts +332 -0
  130. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  131. package/src/__tests__/embedding-managed-proxy-selection.test.ts +2 -2
  132. package/src/__tests__/emit-event-signal.test.ts +4 -6
  133. package/src/__tests__/events-client-registration.test.ts +193 -49
  134. package/src/__tests__/filing-service.test.ts +58 -7
  135. package/src/__tests__/first-greeting.test.ts +156 -150
  136. package/src/__tests__/fixtures/mock-chrome-extension.ts +108 -66
  137. package/src/__tests__/gateway-only-enforcement.test.ts +0 -1
  138. package/src/__tests__/get-skill-detail-audit.test.ts +3 -8
  139. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  140. package/src/__tests__/guardian-dispatch.test.ts +1 -1
  141. package/src/__tests__/guardian-grant-minting.test.ts +7 -2
  142. package/src/__tests__/guardian-routing-invariants.test.ts +7 -2
  143. package/src/__tests__/guardian-routing-state.test.ts +1 -1
  144. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -2
  145. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +30 -11
  146. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +2 -84
  147. package/src/__tests__/headless-browser-mode.test.ts +4 -9
  148. package/src/__tests__/headless-browser-navigate.test.ts +21 -20
  149. package/src/__tests__/heartbeat-service.test.ts +1007 -8
  150. package/src/__tests__/helpers/call-route-handler.ts +7 -1
  151. package/src/__tests__/helpers/channel-test-adapter.ts +2 -2
  152. package/src/__tests__/helpers/create-guardian-binding.ts +91 -0
  153. package/src/__tests__/host-app-control-proxy.test.ts +602 -0
  154. package/src/__tests__/host-app-control-routes.test.ts +263 -0
  155. package/src/__tests__/host-bash-proxy.test.ts +270 -147
  156. package/src/__tests__/host-bash-routes.test.ts +294 -0
  157. package/src/__tests__/host-browser-proxy.test.ts +126 -198
  158. package/src/__tests__/host-browser-routes.test.ts +50 -54
  159. package/src/__tests__/host-cu-proxy.test.ts +78 -144
  160. package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
  161. package/src/__tests__/host-file-edit-tool.test.ts +47 -1
  162. package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
  163. package/src/__tests__/host-file-proxy.test.ts +62 -122
  164. package/src/__tests__/host-file-read-tool.test.ts +59 -21
  165. package/src/__tests__/host-file-routes-targeted.test.ts +262 -0
  166. package/src/__tests__/host-file-write-tool.test.ts +42 -1
  167. package/src/__tests__/host-proxy-base.test.ts +312 -0
  168. package/src/__tests__/host-shell-tool.test.ts +53 -70
  169. package/src/__tests__/host-transfer-pending-interactions.test.ts +2 -18
  170. package/src/__tests__/host-transfer-proxy-targeted.test.ts +583 -0
  171. package/src/__tests__/host-transfer-proxy.test.ts +145 -56
  172. package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
  173. package/src/__tests__/http-user-message-parity.test.ts +1 -6
  174. package/src/__tests__/identity-intro-cache.test.ts +29 -0
  175. package/src/__tests__/identity-routes.test.ts +103 -1
  176. package/src/__tests__/inbound-slack-persistence.test.ts +31 -0
  177. package/src/__tests__/init-feature-flag-overrides.test.ts +26 -3
  178. package/src/__tests__/injector-chain.test.ts +10 -5
  179. package/src/__tests__/injector-pkb-v2-silenced.test.ts +124 -0
  180. package/src/__tests__/inline-command-runner.test.ts +0 -67
  181. package/src/__tests__/inline-skill-load-permissions.test.ts +5 -13
  182. package/src/__tests__/install-skill-routing.test.ts +1 -13
  183. package/src/__tests__/integration-status.test.ts +85 -5
  184. package/src/__tests__/intent-routing.test.ts +0 -1
  185. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +95 -5
  186. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +17 -0
  187. package/src/__tests__/llm-callsite-catalog.test.ts +34 -0
  188. package/src/__tests__/llm-catalog-parity.test.ts +90 -0
  189. package/src/__tests__/llm-context-resolution.test.ts +180 -0
  190. package/src/__tests__/llm-resolver.test.ts +80 -12
  191. package/src/__tests__/llm-usage-store.test.ts +269 -4
  192. package/src/__tests__/log-export-routes.test.ts +89 -0
  193. package/src/__tests__/managed-profile-guard.test.ts +225 -0
  194. package/src/__tests__/managed-skill-lifecycle.test.ts +0 -11
  195. package/src/__tests__/manual-token-reconciliation.test.ts +334 -0
  196. package/src/__tests__/mcp-auth-routes.test.ts +197 -0
  197. package/src/__tests__/mcp-cli.test.ts +338 -2
  198. package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
  199. package/src/__tests__/memory-v2-static-injector.test.ts +95 -0
  200. package/src/__tests__/migration-cross-version-compatibility.test.ts +197 -291
  201. package/src/__tests__/migration-export-http.test.ts +33 -26
  202. package/src/__tests__/migration-export-streaming.test.ts +18 -10
  203. package/src/__tests__/migration-export-to-gcs.test.ts +49 -9
  204. package/src/__tests__/migration-import-commit-http.test.ts +172 -21
  205. package/src/__tests__/migration-import-from-gcs.test.ts +50 -9
  206. package/src/__tests__/migration-import-from-url.test.ts +20 -6
  207. package/src/__tests__/migration-import-preflight-http.test.ts +95 -95
  208. package/src/__tests__/migration-parity-persistence.test.ts +62 -25
  209. package/src/__tests__/migration-transport.test.ts +115 -23
  210. package/src/__tests__/migration-validate-http.test.ts +105 -80
  211. package/src/__tests__/migration-wizard.test.ts +133 -27
  212. package/src/__tests__/mock-gateway-ipc.ts +1 -0
  213. package/src/__tests__/non-member-access-request.test.ts +1 -1
  214. package/src/__tests__/notification-guardian-path.test.ts +1 -1
  215. package/src/__tests__/oauth-cli.test.ts +0 -2
  216. package/src/__tests__/oauth-store.test.ts +19 -0
  217. package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
  218. package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
  219. package/src/__tests__/platform-bash-auto-approve.test.ts +26 -21
  220. package/src/__tests__/prechat-onboarding-contract.test.ts +34 -8
  221. package/src/__tests__/pricing.test.ts +68 -4
  222. package/src/__tests__/process-message-background-slack.test.ts +333 -0
  223. package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
  224. package/src/__tests__/provider-managed-proxy-integration.test.ts +153 -17
  225. package/src/__tests__/provider-send-message-override-profile.test.ts +50 -0
  226. package/src/__tests__/provider-usage-tracking.test.ts +208 -0
  227. package/src/__tests__/public-ingress-urls.test.ts +97 -0
  228. package/src/__tests__/reaction-persistence.test.ts +9 -6
  229. package/src/__tests__/rebind-secrets-screen.test.ts +53 -16
  230. package/src/__tests__/recording-handler.test.ts +64 -81
  231. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +4 -3
  232. package/src/__tests__/relay-server.test.ts +18 -13
  233. package/src/__tests__/require-fresh-approval.test.ts +13 -23
  234. package/src/__tests__/retry-backoff.test.ts +87 -0
  235. package/src/__tests__/runtime-attachment-metadata.test.ts +1 -1
  236. package/src/__tests__/runtime-events-sse-parity.test.ts +3 -4
  237. package/src/__tests__/runtime-events-sse.test.ts +13 -18
  238. package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
  239. package/src/__tests__/schedule-retry.test.ts +715 -0
  240. package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
  241. package/src/__tests__/search-skills-unified.test.ts +9 -15
  242. package/src/__tests__/secret-ingress-cli.test.ts +2 -5
  243. package/src/__tests__/secret-ingress-http.test.ts +1 -4
  244. package/src/__tests__/secret-onetime-send.test.ts +4 -2
  245. package/src/__tests__/secret-prompt-log-hygiene.test.ts +24 -7
  246. package/src/__tests__/secret-prompter-channel-fallback.test.ts +42 -47
  247. package/src/__tests__/secret-response-routing.test.ts +29 -15
  248. package/src/__tests__/secret-routes-managed-proxy.test.ts +5 -1
  249. package/src/__tests__/secret-scanner.test.ts +2 -545
  250. package/src/__tests__/send-endpoint-busy.test.ts +12 -24
  251. package/src/__tests__/settings-routes.test.ts +1 -1
  252. package/src/__tests__/shell-credential-ref.test.ts +0 -8
  253. package/src/__tests__/shell-tool-proxy-mode.test.ts +0 -57
  254. package/src/__tests__/skill-feature-flags.test.ts +43 -41
  255. package/src/__tests__/skill-load-feature-flag.test.ts +13 -14
  256. package/src/__tests__/skill-load-inline-command.test.ts +0 -51
  257. package/src/__tests__/skill-load-inline-includes.test.ts +0 -43
  258. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  259. package/src/__tests__/skill-script-runner-sandbox.test.ts +0 -12
  260. package/src/__tests__/skill-tool-factory.test.ts +97 -0
  261. package/src/__tests__/skills-file-content-endpoint.test.ts +9 -30
  262. package/src/__tests__/skills-files-catalog-fallback.test.ts +11 -17
  263. package/src/__tests__/slack-channel-config.test.ts +9 -14
  264. package/src/__tests__/slack-inbound-verification.test.ts +1 -62
  265. package/src/__tests__/subagent-fork-notifications.test.ts +57 -47
  266. package/src/__tests__/subagent-manager-notify.test.ts +70 -70
  267. package/src/__tests__/subagent-notify-parent.test.ts +80 -83
  268. package/src/__tests__/system-prompt-ask-mode.test.ts +0 -1
  269. package/src/__tests__/system-prompt.test.ts +115 -14
  270. package/src/__tests__/telegram-config.test.ts +0 -1
  271. package/src/__tests__/terminal-tools.test.ts +0 -89
  272. package/src/__tests__/test-preload.ts +8 -0
  273. package/src/__tests__/thread-backfill.test.ts +945 -31
  274. package/src/__tests__/tool-approval-handler.test.ts +3 -4
  275. package/src/__tests__/tool-audit-listener.test.ts +48 -0
  276. package/src/__tests__/tool-domain-event-publisher.test.ts +0 -36
  277. package/src/__tests__/tool-execute-pipeline.test.ts +0 -7
  278. package/src/__tests__/tool-execution-abort-cleanup.test.ts +0 -17
  279. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +9 -19
  280. package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -8
  281. package/src/__tests__/tool-executor.test.ts +12 -20
  282. package/src/__tests__/tool-metrics-listener.test.ts +0 -35
  283. package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
  284. package/src/__tests__/tool-trace-listener.test.ts +0 -17
  285. package/src/__tests__/transfer-progress-screen.test.ts +63 -26
  286. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +2 -149
  287. package/src/__tests__/trusted-contact-multichannel.test.ts +2 -4
  288. package/src/__tests__/trusted-contact-verification.test.ts +1 -1
  289. package/src/__tests__/tts-catalog-parity.test.ts +16 -5
  290. package/src/__tests__/twilio-config.test.ts +3 -16
  291. package/src/__tests__/twilio-routes.test.ts +3 -5
  292. package/src/__tests__/twilio-validation.test.ts +93 -0
  293. package/src/__tests__/usage-attribution.test.ts +247 -0
  294. package/src/__tests__/usage-cli.test.ts +143 -0
  295. package/src/__tests__/usage-grouped-buckets.test.ts +155 -0
  296. package/src/__tests__/usage-routes.test.ts +150 -0
  297. package/src/__tests__/validation-results-screen.test.ts +39 -16
  298. package/src/__tests__/vbundle-pax-and-symlink.test.ts +12 -3
  299. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +47 -138
  300. package/src/__tests__/verification-control-plane-policy.test.ts +6 -11
  301. package/src/__tests__/voice-ingress-preflight.test.ts +19 -0
  302. package/src/__tests__/voice-session-bridge.test.ts +5 -5
  303. package/src/__tests__/workspace-migration-006-services-config.test.ts +3 -2
  304. package/src/__tests__/workspace-migration-062-drop-memory-v2-edges-json.test.ts +103 -0
  305. package/src/__tests__/workspace-migration-063-release-notes-dynamic-model-context.test.ts +77 -0
  306. package/src/__tests__/workspace-migration-064-unwind-main-agent-opus-seed.test.ts +225 -0
  307. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +1 -5
  308. package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
  309. package/src/__tests__/workspace-migration-memory-v2-init.test.ts +8 -30
  310. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
  311. package/src/acp/index.ts +0 -15
  312. package/src/acp/session-manager.ts +37 -34
  313. package/src/agent/loop.ts +16 -1
  314. package/src/approvals/AGENTS.md +4 -0
  315. package/src/approvals/__tests__/guardian-feed-event.test.ts +10 -3
  316. package/src/approvals/guardian-request-resolvers.ts +10 -2
  317. package/src/backup/__tests__/paths.test.ts +0 -22
  318. package/src/backup/__tests__/restore.test.ts +94 -177
  319. package/src/backup/paths.ts +2 -15
  320. package/src/backup/restore.ts +107 -231
  321. package/src/browser-session/events.ts +0 -9
  322. package/src/bundler/app-bundler.ts +51 -3
  323. package/src/calls/call-store.ts +1 -34
  324. package/src/calls/guardian-question-copy.ts +0 -108
  325. package/src/calls/relay-server.ts +4 -68
  326. package/src/calls/twilio-config.ts +2 -17
  327. package/src/calls/twilio-rest.ts +31 -141
  328. package/src/calls/twilio-routes.ts +12 -13
  329. package/src/calls/voice-session-bridge.ts +7 -38
  330. package/src/channels/types.ts +8 -42
  331. package/src/cli/commands/__tests__/backup.test.ts +6 -277
  332. package/src/cli/commands/__tests__/cache.test.ts +152 -5
  333. package/src/cli/commands/__tests__/gateway.test.ts +288 -0
  334. package/src/cli/commands/__tests__/memory-v2.test.ts +18 -28
  335. package/src/cli/commands/__tests__/trust.test.ts +21 -387
  336. package/src/cli/commands/__tests__/webhooks.test.ts +0 -1
  337. package/src/cli/commands/backup.ts +6 -331
  338. package/src/cli/commands/cache-fs.ts +8 -0
  339. package/src/cli/commands/cache.ts +153 -82
  340. package/src/cli/commands/clients.ts +64 -7
  341. package/src/cli/commands/completions.ts +3 -3
  342. package/src/cli/commands/contacts.ts +304 -76
  343. package/src/cli/commands/conversations.ts +2 -5
  344. package/src/cli/commands/credentials.ts +15 -7
  345. package/src/cli/commands/domain.ts +66 -15
  346. package/src/cli/commands/gateway.ts +183 -0
  347. package/src/cli/commands/keys.ts +13 -7
  348. package/src/cli/commands/mcp.ts +116 -156
  349. package/src/cli/commands/memory-v2.ts +320 -53
  350. package/src/cli/commands/oauth/shared.ts +2 -29
  351. package/src/cli/commands/pending.ts +102 -0
  352. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
  353. package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
  354. package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
  355. package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
  356. package/src/cli/commands/platform/disconnect.ts +5 -4
  357. package/src/cli/commands/platform/index.ts +0 -18
  358. package/src/cli/commands/skills.ts +77 -35
  359. package/src/cli/commands/trust.ts +70 -430
  360. package/src/cli/commands/usage.ts +25 -16
  361. package/src/cli/lib/daemon-credential-client.ts +115 -19
  362. package/src/cli/program.ts +4 -0
  363. package/src/cli.ts +0 -21
  364. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  365. package/src/config/assistant-feature-flags.ts +67 -10
  366. package/src/config/bundled-skills/acp/SKILL.md +6 -0
  367. package/src/config/bundled-skills/acp/TOOLS.json +1 -22
  368. package/src/config/bundled-skills/app-builder/SKILL.md +14 -109
  369. package/src/config/bundled-skills/app-builder/TOOLS.json +1 -28
  370. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -10
  371. package/src/config/bundled-skills/app-control/SKILL.md +75 -0
  372. package/src/config/bundled-skills/app-control/TOOLS.json +299 -0
  373. package/src/config/bundled-skills/app-control/tools/app-control-click.ts +12 -0
  374. package/src/config/bundled-skills/app-control/tools/app-control-combo.ts +12 -0
  375. package/src/config/bundled-skills/app-control/tools/app-control-drag.ts +12 -0
  376. package/src/config/bundled-skills/app-control/tools/app-control-observe.ts +12 -0
  377. package/src/config/bundled-skills/app-control/tools/app-control-press.ts +12 -0
  378. package/src/config/bundled-skills/app-control/tools/app-control-sequence.ts +12 -0
  379. package/src/config/bundled-skills/app-control/tools/app-control-start.ts +12 -0
  380. package/src/config/bundled-skills/app-control/tools/app-control-stop.ts +12 -0
  381. package/src/config/bundled-skills/app-control/tools/app-control-type.ts +12 -0
  382. package/src/config/bundled-skills/computer-use/SKILL.md +6 -0
  383. package/src/config/bundled-skills/computer-use/TOOLS.json +67 -43
  384. package/src/config/bundled-skills/contacts/TOOLS.json +0 -16
  385. package/src/config/bundled-skills/document/TOOLS.json +0 -8
  386. package/src/config/bundled-skills/followups/TOOLS.json +0 -12
  387. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  388. package/src/config/bundled-skills/image-studio/TOOLS.json +0 -4
  389. package/src/config/bundled-skills/media-processing/TOOLS.json +0 -24
  390. package/src/config/bundled-skills/messaging/TOOLS.json +14 -44
  391. package/src/config/bundled-skills/phone-calls/TOOLS.json +0 -12
  392. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
  393. package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
  394. package/src/config/bundled-skills/schedule/TOOLS.json +14 -14
  395. package/src/config/bundled-skills/sequences/TOOLS.json +0 -36
  396. package/src/config/bundled-skills/settings/SKILL.md +4 -0
  397. package/src/config/bundled-skills/settings/TOOLS.json +0 -12
  398. package/src/config/bundled-skills/skill-management/SKILL.md +6 -0
  399. package/src/config/bundled-skills/skill-management/TOOLS.json +0 -8
  400. package/src/config/bundled-skills/subagent/SKILL.md +6 -2
  401. package/src/config/bundled-skills/subagent/TOOLS.json +0 -20
  402. package/src/config/bundled-skills/transcribe/SKILL.md +4 -0
  403. package/src/config/bundled-skills/transcribe/TOOLS.json +0 -4
  404. package/src/config/bundled-tool-registry.ts +21 -0
  405. package/src/config/env-registry.ts +12 -4
  406. package/src/config/env.ts +22 -26
  407. package/src/config/feature-flag-registry.json +40 -152
  408. package/src/config/llm-callsite-catalog.ts +12 -0
  409. package/src/config/llm-context-resolution.ts +80 -0
  410. package/src/config/llm-resolver.ts +58 -22
  411. package/src/config/loader.ts +76 -102
  412. package/src/config/sanitize-for-transfer.ts +2 -0
  413. package/src/config/schema.ts +2 -158
  414. package/src/config/schemas/__tests__/memory-lifecycle.test.ts +80 -0
  415. package/src/config/schemas/__tests__/memory-v2.test.ts +8 -4
  416. package/src/config/schemas/call-site-catalog.ts +271 -0
  417. package/src/config/schemas/calls.ts +5 -14
  418. package/src/config/schemas/heartbeat.ts +63 -0
  419. package/src/config/schemas/inference.ts +1 -1
  420. package/src/config/schemas/ingress.ts +11 -7
  421. package/src/config/schemas/llm.ts +34 -11
  422. package/src/config/schemas/memory-lifecycle.ts +77 -24
  423. package/src/config/schemas/memory-retrieval.ts +2 -2
  424. package/src/config/schemas/memory-v2.ts +57 -4
  425. package/src/config/schemas/platform.ts +6 -0
  426. package/src/config/schemas/security.ts +1 -42
  427. package/src/config/schemas/services.ts +7 -21
  428. package/src/config/schemas/skills.ts +5 -11
  429. package/src/config/schemas/tts.ts +1 -1
  430. package/src/config/seed-inference-profiles.ts +117 -0
  431. package/src/config/skills.ts +0 -90
  432. package/src/config/types.ts +3 -6
  433. package/src/contacts/contact-store.ts +0 -47
  434. package/src/contacts/contacts-write.ts +1 -132
  435. package/src/context/window-manager.ts +43 -5
  436. package/src/credential-execution/feature-gates.ts +10 -10
  437. package/src/credential-execution/process-manager.ts +46 -51
  438. package/src/credential-health/credential-health-service.ts +21 -16
  439. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +75 -82
  440. package/src/daemon/__tests__/conversation-tool-setup.test.ts +126 -5
  441. package/src/daemon/__tests__/daemon-skill-host.test.ts +2 -9
  442. package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
  443. package/src/daemon/config-watcher.ts +4 -3
  444. package/src/daemon/connection-policy.ts +1 -26
  445. package/src/daemon/conversation-agent-loop-handlers.ts +74 -7
  446. package/src/daemon/conversation-agent-loop.ts +309 -64
  447. package/src/daemon/conversation-history.ts +8 -8
  448. package/src/daemon/conversation-launch.ts +20 -135
  449. package/src/daemon/conversation-lifecycle.ts +8 -1
  450. package/src/daemon/conversation-messaging.ts +1 -0
  451. package/src/daemon/conversation-process.ts +97 -172
  452. package/src/daemon/conversation-runtime-assembly.ts +219 -76
  453. package/src/daemon/conversation-slash.ts +47 -5
  454. package/src/daemon/conversation-store.ts +7 -31
  455. package/src/daemon/conversation-surfaces.ts +144 -29
  456. package/src/daemon/conversation-tool-setup.ts +18 -87
  457. package/src/daemon/conversation-usage.ts +36 -0
  458. package/src/daemon/conversation.ts +134 -231
  459. package/src/daemon/daemon-control.ts +3 -71
  460. package/src/daemon/daemon-skill-host.ts +8 -11
  461. package/src/daemon/dictation-profile-store.ts +2 -26
  462. package/src/daemon/doordash-steps.ts +1 -1
  463. package/src/daemon/first-greeting.ts +44 -156
  464. package/src/daemon/handlers/config-channels.ts +12 -12
  465. package/src/daemon/handlers/config-ingress.ts +4 -165
  466. package/src/daemon/handlers/config-model.ts +1 -1
  467. package/src/daemon/handlers/config-voice.ts +0 -42
  468. package/src/daemon/handlers/conversations.ts +11 -190
  469. package/src/daemon/handlers/recording.ts +26 -158
  470. package/src/daemon/handlers/shared.ts +27 -72
  471. package/src/daemon/handlers/skills.ts +42 -93
  472. package/src/daemon/host-app-control-proxy.ts +293 -0
  473. package/src/daemon/host-bash-proxy.ts +124 -92
  474. package/src/daemon/host-browser-proxy.ts +111 -88
  475. package/src/daemon/host-cu-proxy.ts +100 -104
  476. package/src/daemon/host-file-proxy.ts +136 -91
  477. package/src/daemon/host-proxy-base.ts +294 -0
  478. package/src/daemon/host-proxy-preactivation.ts +82 -0
  479. package/src/daemon/host-transfer-proxy.ts +303 -147
  480. package/src/daemon/lifecycle.ts +164 -132
  481. package/src/daemon/message-protocol.ts +3 -8
  482. package/src/daemon/message-types/contacts.ts +23 -1
  483. package/src/daemon/message-types/conversations.ts +18 -8
  484. package/src/daemon/message-types/host-app-control.ts +150 -0
  485. package/src/daemon/message-types/host-bash.ts +5 -0
  486. package/src/daemon/message-types/host-cu.ts +3 -0
  487. package/src/daemon/message-types/host-file.ts +5 -0
  488. package/src/daemon/message-types/host-transfer.ts +4 -0
  489. package/src/daemon/message-types/messages.ts +10 -9
  490. package/src/daemon/message-types/schedules.ts +8 -3
  491. package/src/daemon/message-types/skills.ts +2 -2
  492. package/src/daemon/message-types/workspace.ts +1 -1
  493. package/src/daemon/process-message.ts +119 -239
  494. package/src/daemon/server.ts +13 -462
  495. package/src/daemon/shutdown-handlers.ts +2 -5
  496. package/src/daemon/tool-setup-types.ts +51 -0
  497. package/src/daemon/tool-side-effects.ts +126 -108
  498. package/src/daemon/trust-context.ts +13 -0
  499. package/src/daemon/wake-target-adapter.ts +4 -9
  500. package/src/events/domain-events.ts +0 -8
  501. package/src/events/tool-audit-listener.ts +5 -2
  502. package/src/events/tool-domain-event-publisher.ts +0 -10
  503. package/src/events/tool-metrics-listener.ts +0 -17
  504. package/src/events/tool-trace-listener.ts +0 -14
  505. package/src/filing/filing-service.ts +13 -1
  506. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +21 -9
  507. package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
  508. package/src/heartbeat/heartbeat-run-store.ts +236 -0
  509. package/src/heartbeat/heartbeat-service.ts +303 -54
  510. package/src/home/__tests__/feed-writer.test.ts +0 -4
  511. package/src/home/__tests__/post-connect-feed.test.ts +99 -0
  512. package/src/home/__tests__/relationship-state-writer.test.ts +41 -9
  513. package/src/home/__tests__/suggested-prompts.test.ts +89 -0
  514. package/src/home/feed-writer.ts +1 -2
  515. package/src/home/post-connect-feed.ts +68 -0
  516. package/src/home/relationship-state-writer.ts +33 -95
  517. package/src/home/suggested-prompts.ts +46 -10
  518. package/src/inbound/public-ingress-urls.ts +32 -34
  519. package/src/ipc/__tests__/browser-ipc.test.ts +2 -12
  520. package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
  521. package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -1
  522. package/src/ipc/assistant-server.ts +17 -11
  523. package/src/ipc/cli-client.ts +32 -1
  524. package/src/ipc/routes/__tests__/memory-v2-backfill.test.ts +39 -20
  525. package/src/ipc/routes/route-adapter.ts +1 -1
  526. package/src/ipc/routes/trust-rules.test.ts +0 -95
  527. package/src/ipc/skill-ipc-types.ts +41 -0
  528. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +13 -27
  529. package/src/ipc/skill-routes/__tests__/identity.test.ts +4 -23
  530. package/src/ipc/skill-routes/events.ts +12 -23
  531. package/src/ipc/skill-routes/identity.ts +4 -17
  532. package/src/ipc/skill-routes/index.ts +1 -1
  533. package/src/ipc/skill-server.ts +6 -39
  534. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +0 -8
  535. package/src/live-voice/live-voice-metrics.ts +10 -10
  536. package/src/live-voice/protocol.ts +4 -13
  537. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +304 -0
  538. package/src/mcp/manager.ts +0 -5
  539. package/src/mcp/mcp-auth-orchestrator.ts +213 -0
  540. package/src/mcp/mcp-auth-state.ts +133 -0
  541. package/src/mcp/mcp-oauth-provider.ts +19 -0
  542. package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +55 -0
  543. package/src/memory/__tests__/jobs-store-job-classes.test.ts +24 -0
  544. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +127 -0
  545. package/src/memory/__tests__/qdrant-client-sentinel.test.ts +49 -0
  546. package/src/memory/__tests__/sparse-tokenize.test.ts +66 -0
  547. package/src/memory/anisotropy.test.ts +247 -0
  548. package/src/memory/anisotropy.ts +443 -0
  549. package/src/memory/app-git-service.ts +0 -32
  550. package/src/memory/app-store.ts +154 -0
  551. package/src/memory/attachments-store.ts +6 -0
  552. package/src/memory/auto-analysis-constants.ts +17 -0
  553. package/src/memory/auto-analysis-guard.ts +5 -15
  554. package/src/memory/canonical-guardian-store.ts +7 -7
  555. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +122 -0
  556. package/src/memory/context-search/agent-protocol.ts +6 -6
  557. package/src/memory/context-search/agent-runner.ts +32 -7
  558. package/src/memory/context-search/sources/memory-v2.ts +590 -0
  559. package/src/memory/context-search/sources/memory.ts +5 -0
  560. package/src/memory/context-search/sources/pkb.ts +10 -1
  561. package/src/memory/context-search/sources/workspace.ts +3 -2
  562. package/src/memory/conversation-crud.ts +30 -5
  563. package/src/memory/conversation-disk-view.ts +1 -5
  564. package/src/memory/conversation-key-store.ts +2 -15
  565. package/src/memory/conversation-starter-checkpoints.ts +63 -0
  566. package/src/memory/db-connection.ts +62 -0
  567. package/src/memory/db-init.ts +18 -0
  568. package/src/memory/embedding-backend.ts +12 -42
  569. package/src/memory/embedding-gemini.ts +0 -2
  570. package/src/memory/embedding-local.ts +6 -6
  571. package/src/memory/embedding-ollama.ts +6 -6
  572. package/src/memory/embedding-openai.ts +6 -6
  573. package/src/memory/embedding-types.ts +21 -0
  574. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -8
  575. package/src/memory/graph/conversation-graph-memory.ts +35 -36
  576. package/src/memory/graph/graph-search.ts +8 -0
  577. package/src/memory/graph/injection.test.ts +2 -2
  578. package/src/memory/graph/injection.ts +1 -1
  579. package/src/memory/graph/retriever.ts +28 -0
  580. package/src/memory/graph/tools.ts +1 -1
  581. package/src/memory/guardian-action-store.ts +0 -83
  582. package/src/memory/guardian-approvals.ts +0 -48
  583. package/src/memory/indexer.ts +1 -15
  584. package/src/memory/job-handlers/conversation-starters.ts +36 -53
  585. package/src/memory/job-utils.ts +0 -6
  586. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +8 -2
  587. package/src/memory/jobs/embed-concept-page.ts +28 -2
  588. package/src/memory/jobs/embed-pkb-file.test.ts +2 -2
  589. package/src/memory/jobs-store.ts +66 -23
  590. package/src/memory/jobs-worker.ts +114 -79
  591. package/src/memory/llm-request-log-store.ts +0 -41
  592. package/src/memory/llm-usage-store.ts +129 -43
  593. package/src/memory/memory-v2-activation-log-store.ts +115 -0
  594. package/src/memory/migrations/233-document-conversations.ts +54 -0
  595. package/src/memory/migrations/234-memory-v2-activation-logs.ts +55 -0
  596. package/src/memory/migrations/235-llm-usage-attribution.ts +31 -0
  597. package/src/memory/migrations/235-slack-compaction-watermark.ts +44 -0
  598. package/src/memory/migrations/236-tool-invocations-matched-rule-id.ts +26 -0
  599. package/src/memory/migrations/237-heartbeat-runs.ts +45 -0
  600. package/src/memory/migrations/238-schedule-retry-policy.ts +20 -0
  601. package/src/memory/migrations/__tests__/234-memory-v2-activation-logs.test.ts +182 -0
  602. package/src/memory/migrations/index.ts +19 -0
  603. package/src/memory/migrations/registry.ts +32 -0
  604. package/src/memory/pkb/pkb-search.ts +7 -0
  605. package/src/memory/qdrant-client.ts +50 -20
  606. package/src/memory/raw-query.ts +2 -68
  607. package/src/memory/schema/conversations.ts +7 -0
  608. package/src/memory/schema/infrastructure.ts +40 -0
  609. package/src/memory/search/semantic.ts +12 -16
  610. package/src/memory/sparse-tokenize.ts +49 -0
  611. package/src/memory/tool-usage-store.ts +2 -0
  612. package/src/memory/usage-buckets.ts +40 -1
  613. package/src/memory/usage-grouped-buckets.ts +127 -0
  614. package/src/memory/v2/__tests__/activation.test.ts +361 -180
  615. package/src/memory/v2/__tests__/backfill-jobs.test.ts +2 -129
  616. package/src/memory/v2/__tests__/consolidation-job.test.ts +28 -11
  617. package/src/memory/v2/__tests__/edge-index.test.ts +278 -0
  618. package/src/memory/v2/__tests__/injection.test.ts +424 -33
  619. package/src/memory/v2/__tests__/migration.test.ts +64 -36
  620. package/src/memory/v2/__tests__/page-store.test.ts +191 -8
  621. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +181 -0
  622. package/src/memory/v2/__tests__/sim.test.ts +166 -6
  623. package/src/memory/v2/__tests__/skill-store.test.ts +115 -3
  624. package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
  625. package/src/memory/v2/__tests__/static-context.test.ts +152 -0
  626. package/src/memory/v2/activation.ts +215 -163
  627. package/src/memory/v2/backfill-jobs.ts +15 -100
  628. package/src/memory/v2/consolidation-job.ts +17 -17
  629. package/src/memory/v2/constants.ts +7 -0
  630. package/src/memory/v2/edge-index.ts +191 -0
  631. package/src/memory/v2/injection.ts +241 -84
  632. package/src/memory/v2/migration.ts +57 -64
  633. package/src/memory/v2/now-text.ts +2 -3
  634. package/src/memory/v2/page-store.ts +168 -31
  635. package/src/memory/v2/prompts/consolidation.ts +385 -88
  636. package/src/memory/v2/prompts/sweep.ts +3 -3
  637. package/src/memory/v2/qdrant.ts +99 -1
  638. package/src/memory/v2/sim.ts +126 -16
  639. package/src/memory/v2/skill-qdrant.ts +12 -3
  640. package/src/memory/v2/skill-store.ts +71 -8
  641. package/src/memory/v2/sparse-bm25.ts +245 -0
  642. package/src/memory/v2/static-context.ts +63 -0
  643. package/src/memory/v2/types.ts +10 -20
  644. package/src/memory/validation.ts +0 -11
  645. package/src/messaging/draft-store.ts +0 -6
  646. package/src/messaging/provider-types.ts +8 -0
  647. package/src/messaging/provider.ts +7 -0
  648. package/src/messaging/providers/gmail/client.ts +1 -121
  649. package/src/messaging/providers/gmail/types.ts +0 -49
  650. package/src/messaging/providers/outlook/client.ts +0 -73
  651. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +226 -0
  652. package/src/messaging/providers/slack/adapter.ts +123 -52
  653. package/src/messaging/providers/slack/backfill.test.ts +95 -6
  654. package/src/messaging/providers/slack/backfill.ts +89 -11
  655. package/src/messaging/providers/slack/client.ts +10 -124
  656. package/src/messaging/providers/slack/message-metadata.ts +12 -2
  657. package/src/messaging/providers/slack/render-transcript.test.ts +56 -0
  658. package/src/messaging/providers/slack/render-transcript.ts +126 -25
  659. package/src/messaging/providers/slack/types.ts +1 -32
  660. package/src/notifications/README.md +10 -10
  661. package/src/notifications/broadcaster.ts +1 -1
  662. package/src/notifications/guardian-question-mode.ts +5 -5
  663. package/src/oauth/connect-orchestrator.ts +4 -0
  664. package/src/oauth/connection-resolver.test.ts +8 -0
  665. package/src/oauth/connection-resolver.ts +8 -16
  666. package/src/oauth/credential-token-resolver.ts +95 -0
  667. package/src/oauth/manual-token-connection.ts +26 -34
  668. package/src/oauth/oauth-store.ts +6 -4
  669. package/src/outbound-proxy/certs.ts +0 -7
  670. package/src/outbound-proxy/index.ts +1 -59
  671. package/src/outbound-proxy/logging.ts +1 -1
  672. package/src/outbound-proxy/policy.ts +6 -5
  673. package/src/outbound-proxy/router.ts +2 -1
  674. package/src/permissions/approval-policy.test.ts +6 -275
  675. package/src/permissions/approval-policy.ts +0 -51
  676. package/src/permissions/approval-provenance.test.ts +184 -0
  677. package/src/permissions/approval-provenance.ts +70 -0
  678. package/src/permissions/checker.test.ts +0 -1
  679. package/src/permissions/checker.ts +7 -18
  680. package/src/permissions/gateway-threshold-reader.ts +6 -1
  681. package/src/permissions/prompter.ts +43 -3
  682. package/src/permissions/secret-prompter.ts +25 -48
  683. package/src/permissions/types.ts +33 -0
  684. package/src/permissions/workspace-policy.ts +0 -5
  685. package/src/platform/sync-identity.ts +0 -8
  686. package/src/plugins/defaults/injectors.ts +69 -2
  687. package/src/plugins/defaults/overflow-reduce.ts +3 -2
  688. package/src/plugins/types.ts +8 -0
  689. package/src/prompts/bootstrap-cleanup.ts +27 -0
  690. package/src/prompts/system-prompt.ts +37 -88
  691. package/src/prompts/templates/BOOTSTRAP.md +52 -6
  692. package/src/prompts/templates/SOUL.md +13 -1
  693. package/src/prompts/update-bulletin-job.ts +2 -0
  694. package/src/providers/__tests__/retry-callsite.test.ts +138 -1
  695. package/src/providers/anthropic/client.ts +72 -33
  696. package/src/providers/call-site-routing.ts +42 -3
  697. package/src/providers/gemini/client.ts +18 -2
  698. package/src/providers/managed-proxy/context.ts +0 -5
  699. package/src/providers/model-catalog.ts +105 -19
  700. package/src/providers/openai/chat-completions-provider.ts +6 -0
  701. package/src/providers/openai/responses-provider.ts +7 -1
  702. package/src/providers/provider-send-message.ts +45 -2
  703. package/src/providers/ratelimit.ts +7 -2
  704. package/src/providers/registry.ts +14 -9
  705. package/src/providers/retry.ts +96 -8
  706. package/src/providers/speech-to-text/provider-catalog.ts +7 -8
  707. package/src/providers/types.ts +13 -0
  708. package/src/providers/usage-tracking.ts +96 -0
  709. package/src/runtime/AGENTS.md +10 -6
  710. package/src/runtime/__tests__/agent-wake.test.ts +89 -0
  711. package/src/runtime/agent-wake.ts +39 -2
  712. package/src/runtime/assistant-event-hub.ts +570 -52
  713. package/src/runtime/assistant-event.ts +2 -6
  714. package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
  715. package/src/runtime/auth/context.ts +0 -9
  716. package/src/runtime/auth/middleware.ts +1 -97
  717. package/src/runtime/auth/route-policy.ts +30 -9
  718. package/src/runtime/auth/token-service.ts +0 -11
  719. package/src/runtime/btw-sidechain.ts +2 -3
  720. package/src/runtime/channel-approvals.ts +6 -2
  721. package/src/runtime/channel-invite-transport.ts +2 -48
  722. package/src/runtime/channel-invite-transports/email.ts +1 -1
  723. package/src/runtime/channel-invite-transports/slack.ts +1 -1
  724. package/src/runtime/channel-invite-transports/telegram.ts +1 -1
  725. package/src/runtime/channel-invite-transports/voice.ts +1 -1
  726. package/src/runtime/channel-invite-transports/whatsapp.ts +1 -1
  727. package/src/runtime/channel-invite-types.ts +54 -0
  728. package/src/runtime/channel-readiness-service.ts +32 -13
  729. package/src/runtime/channel-verification-service.ts +3 -5
  730. package/src/runtime/http-errors.ts +0 -34
  731. package/src/runtime/http-router.ts +6 -3
  732. package/src/runtime/http-server.ts +16 -402
  733. package/src/runtime/http-types.ts +5 -5
  734. package/src/runtime/interactive-ui.ts +0 -1
  735. package/src/runtime/middleware/auth.ts +0 -20
  736. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +112 -0
  737. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +11 -4
  738. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +253 -0
  739. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +19 -6
  740. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +413 -0
  741. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +260 -0
  742. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +189 -0
  743. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +71 -27
  744. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +41 -2
  745. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +296 -80
  746. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +143 -23
  747. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +451 -0
  748. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -0
  749. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +515 -0
  750. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +437 -0
  751. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +319 -0
  752. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +2 -2
  753. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +421 -0
  754. package/src/runtime/migrations/migration-transport.ts +49 -16
  755. package/src/runtime/migrations/migration-wizard.ts +2 -2
  756. package/src/runtime/migrations/origin-mode.ts +40 -0
  757. package/src/runtime/migrations/vbundle-builder.ts +457 -136
  758. package/src/runtime/migrations/vbundle-import-analyzer.ts +13 -11
  759. package/src/runtime/migrations/vbundle-import-policy.ts +172 -0
  760. package/src/runtime/migrations/vbundle-importer.ts +251 -74
  761. package/src/runtime/migrations/vbundle-metadata-merge.ts +1 -1
  762. package/src/runtime/migrations/vbundle-streaming-importer.ts +329 -38
  763. package/src/runtime/migrations/vbundle-streaming-validator.ts +203 -28
  764. package/src/runtime/migrations/vbundle-tar-stream.ts +15 -6
  765. package/src/runtime/migrations/vbundle-validator.ts +328 -41
  766. package/src/runtime/pending-interactions.ts +48 -13
  767. package/src/runtime/routes/__tests__/acp-routes.test.ts +0 -1
  768. package/src/runtime/routes/__tests__/backup-routes.test.ts +49 -168
  769. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +333 -0
  770. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +242 -0
  771. package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +112 -0
  772. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +58 -0
  773. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +54 -0
  774. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +19 -6
  775. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +7 -7
  776. package/src/runtime/routes/acp-routes.test.ts +0 -3
  777. package/src/runtime/routes/acp-routes.ts +3 -7
  778. package/src/runtime/routes/app-management-routes.ts +18 -9
  779. package/src/runtime/routes/approval-interception-types.ts +13 -0
  780. package/src/runtime/routes/approval-routes.ts +55 -14
  781. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +1 -1
  782. package/src/runtime/routes/avatar-routes.ts +3 -5
  783. package/src/runtime/routes/backup-routes.ts +15 -38
  784. package/src/runtime/routes/browser-routes.ts +1 -15
  785. package/src/runtime/routes/btw-routes.ts +14 -37
  786. package/src/runtime/routes/channel-guardian-routes.ts +1 -5
  787. package/src/runtime/routes/channel-readiness-routes.ts +3 -7
  788. package/src/runtime/routes/channel-route-shared.ts +2 -28
  789. package/src/runtime/routes/client-routes.ts +46 -12
  790. package/src/runtime/routes/consolidation-routes.ts +115 -0
  791. package/src/runtime/routes/contact-prompt-routes.ts +183 -0
  792. package/src/runtime/routes/conversation-list-routes.ts +12 -29
  793. package/src/runtime/routes/conversation-management-routes.ts +14 -51
  794. package/src/runtime/routes/conversation-query-routes.ts +156 -9
  795. package/src/runtime/routes/conversation-routes.ts +72 -539
  796. package/src/runtime/routes/conversation-starter-routes.ts +19 -40
  797. package/src/runtime/routes/document-pdf-renderer.ts +165 -0
  798. package/src/runtime/routes/documents-routes.ts +83 -18
  799. package/src/runtime/routes/errors.ts +19 -4
  800. package/src/runtime/routes/events-routes.ts +68 -94
  801. package/src/runtime/routes/filing-routes.ts +18 -1
  802. package/src/runtime/routes/gateway-log-routes.ts +79 -0
  803. package/src/runtime/routes/guardian-action-routes.ts +4 -9
  804. package/src/runtime/routes/guardian-approval-interception.ts +2 -8
  805. package/src/runtime/routes/heartbeat-routes.ts +103 -38
  806. package/src/runtime/routes/host-app-control-routes.ts +134 -0
  807. package/src/runtime/routes/host-bash-routes.ts +37 -6
  808. package/src/runtime/routes/host-browser-routes.ts +96 -25
  809. package/src/runtime/routes/host-cu-routes.ts +48 -13
  810. package/src/runtime/routes/host-file-routes.ts +35 -11
  811. package/src/runtime/routes/host-transfer-routes.ts +73 -37
  812. package/src/runtime/routes/http-adapter.ts +1 -0
  813. package/src/runtime/routes/identity-intro-cache.ts +30 -0
  814. package/src/runtime/routes/identity-routes.ts +93 -49
  815. package/src/runtime/routes/inbound-message-handler.ts +581 -146
  816. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +2 -95
  817. package/src/runtime/routes/inbound-stages/background-dispatch.ts +3 -0
  818. package/src/runtime/routes/inbound-stages/edit-intercept.ts +0 -8
  819. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +0 -20
  820. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
  821. package/src/runtime/routes/index.ts +12 -0
  822. package/src/runtime/routes/integrations/slack/channel.ts +0 -24
  823. package/src/runtime/routes/llm-call-sites-routes.ts +22 -0
  824. package/src/runtime/routes/mcp-auth-routes.ts +132 -0
  825. package/src/runtime/routes/memory-item-routes.ts +10 -12
  826. package/src/runtime/routes/memory-v2-routes.ts +451 -16
  827. package/src/runtime/routes/migration-routes.ts +284 -31
  828. package/src/runtime/routes/playground/guard.ts +1 -1
  829. package/src/runtime/routes/playground/index.ts +0 -2
  830. package/src/runtime/routes/recording-routes.ts +4 -24
  831. package/src/runtime/routes/rename-conversation-routes.ts +2 -6
  832. package/src/runtime/routes/schedule-routes.ts +10 -6
  833. package/src/runtime/routes/secret-routes.ts +87 -18
  834. package/src/runtime/routes/settings-routes.ts +29 -28
  835. package/src/runtime/routes/skills-routes.ts +12 -31
  836. package/src/runtime/routes/suggest-trust-rule-routes.ts +32 -1
  837. package/src/runtime/routes/task-routes.ts +6 -6
  838. package/src/runtime/routes/trust-rules-routes.ts +3 -94
  839. package/src/runtime/routes/types.ts +4 -4
  840. package/src/runtime/routes/upgrade-broadcast-routes.ts +3 -10
  841. package/src/runtime/routes/usage-routes.ts +87 -10
  842. package/src/runtime/routes/user-routes.ts +17 -31
  843. package/src/runtime/routes/work-items-routes.ts +1 -4
  844. package/src/runtime/services/__tests__/analyze-conversation.test.ts +2 -2
  845. package/src/runtime/services/analyze-conversation.ts +7 -17
  846. package/src/runtime/services/conversation-serializer.ts +2 -4
  847. package/src/runtime/verification-outbound-actions.ts +1 -1
  848. package/src/runtime/verification-rate-limiter.ts +1 -1
  849. package/src/runtime/verification-templates.ts +4 -7
  850. package/src/schedule/integration-status.ts +66 -2
  851. package/src/schedule/recurrence-engine.ts +4 -1
  852. package/src/schedule/retry-backoff.ts +18 -0
  853. package/src/schedule/retry-policy.ts +82 -0
  854. package/src/schedule/schedule-recovery.ts +64 -0
  855. package/src/schedule/schedule-store.ts +106 -18
  856. package/src/schedule/scheduler-types.ts +25 -0
  857. package/src/schedule/scheduler.ts +63 -38
  858. package/src/security/oauth-callback-registry.ts +8 -0
  859. package/src/security/secret-scanner.ts +14 -547
  860. package/src/security/secure-keys.ts +31 -11
  861. package/src/security/token-manager.ts +7 -3
  862. package/src/sequence/analytics.ts +5 -5
  863. package/src/sequence/engine.ts +1 -1
  864. package/src/signals/cancel.ts +16 -25
  865. package/src/signals/conversation-undo.ts +2 -27
  866. package/src/signals/emit-event.ts +1 -2
  867. package/src/signals/user-message.ts +108 -22
  868. package/src/skills/catalog-files.ts +2 -8
  869. package/src/skills/catalog-install.ts +1 -0
  870. package/src/skills/clawhub.ts +2 -2
  871. package/src/skills/include-graph.ts +5 -5
  872. package/src/skills/inline-command-runner.ts +1 -7
  873. package/src/skills/remote-skill-policy.ts +5 -5
  874. package/src/skills/skill-file-provider.ts +1 -1
  875. package/src/skills/skill-file-types.ts +13 -0
  876. package/src/skills/skillssh-audit-types.ts +28 -0
  877. package/src/skills/skillssh-registry.ts +8 -21
  878. package/src/subagent/manager.ts +67 -84
  879. package/src/tasks/task-store.ts +1 -28
  880. package/src/telemetry/types.ts +8 -0
  881. package/src/telemetry/usage-telemetry-reporter.test.ts +59 -15
  882. package/src/telemetry/usage-telemetry-reporter.ts +4 -5
  883. package/src/tools/acp/spawn.test.ts +1 -2
  884. package/src/tools/acp/steer.test.ts +1 -2
  885. package/src/tools/app-control/skill-proxy-bridge.ts +28 -0
  886. package/src/tools/apps/executors.ts +56 -69
  887. package/src/tools/browser/__tests__/browser-status.test.ts +55 -135
  888. package/src/tools/browser/browser-execution.ts +31 -147
  889. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +145 -70
  890. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +12 -6
  891. package/src/tools/browser/cdp-client/factory.ts +62 -91
  892. package/src/tools/browser/cdp-client/index.ts +1 -27
  893. package/src/tools/computer-use/definitions.ts +42 -20
  894. package/src/tools/executor.ts +46 -31
  895. package/src/tools/host-filesystem/edit.ts +29 -2
  896. package/src/tools/host-filesystem/read.ts +29 -2
  897. package/src/tools/host-filesystem/transfer.test.ts +45 -42
  898. package/src/tools/host-filesystem/transfer.ts +35 -4
  899. package/src/tools/host-filesystem/write.ts +29 -2
  900. package/src/tools/host-terminal/host-shell.ts +62 -3
  901. package/src/tools/network/script-proxy/index.ts +1 -10
  902. package/src/tools/permission-checker.ts +66 -1
  903. package/src/tools/schedule/create.ts +6 -0
  904. package/src/tools/schedule/list.ts +2 -0
  905. package/src/tools/schedule/update.ts +10 -0
  906. package/src/tools/shared/filesystem/file-ops-service.ts +2 -0
  907. package/src/tools/shared/filesystem/path-policy.ts +25 -1
  908. package/src/tools/skills/load.ts +0 -32
  909. package/src/tools/skills/sandbox-runner.ts +1 -6
  910. package/src/tools/skills/skill-tool-factory.ts +32 -0
  911. package/src/tools/terminal/safe-env.ts +1 -0
  912. package/src/tools/terminal/shell.ts +2 -78
  913. package/src/tools/tool-approval-handler.ts +1 -5
  914. package/src/tools/types.ts +16 -39
  915. package/src/tts/__tests__/provider-catalog.test.ts +2 -2
  916. package/src/tts/provider-catalog.ts +1 -1
  917. package/src/usage/actors.ts +2 -1
  918. package/src/usage/attribution.ts +185 -0
  919. package/src/usage/pricing.ts +166 -0
  920. package/src/usage/types.ts +14 -0
  921. package/src/util/json.ts +13 -0
  922. package/src/util/logger.ts +3 -3
  923. package/src/util/pricing.ts +50 -3
  924. package/src/work-items/work-item-runner.ts +15 -42
  925. package/src/workspace/hatched-date.ts +86 -0
  926. package/src/workspace/migrations/003-seed-device-id.ts +1 -1
  927. package/src/workspace/migrations/006-services-config.ts +8 -5
  928. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +3 -9
  929. package/src/workspace/migrations/021-move-signals-to-workspace.ts +4 -10
  930. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +4 -10
  931. package/src/workspace/migrations/023-move-config-files-to-workspace.ts +4 -11
  932. package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +3 -10
  933. package/src/workspace/migrations/040-seed-latency-callsite-defaults.ts +3 -2
  934. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +6 -4
  935. package/src/workspace/migrations/052-seed-default-inference-profiles.ts +3 -3
  936. package/src/workspace/migrations/059-move-pid-to-workspace.ts +3 -8
  937. package/src/workspace/migrations/060-memory-v2-init.ts +2 -18
  938. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +54 -0
  939. package/src/workspace/migrations/062-drop-memory-v2-edges-json.ts +27 -0
  940. package/src/workspace/migrations/063-release-notes-dynamic-model-context.ts +70 -0
  941. package/src/workspace/migrations/064-unwind-main-agent-opus-seed.ts +64 -0
  942. package/src/workspace/migrations/AGENTS.md +1 -1
  943. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
  944. package/src/workspace/migrations/registry.ts +8 -0
  945. package/src/workspace/migrations/utils.ts +21 -0
  946. package/src/workspace/provider-commit-message-generator.ts +3 -3
  947. package/src/__tests__/host-browser-e2e-cloud.test.ts +0 -904
  948. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +0 -296
  949. package/src/__tests__/host-browser-ws-events-e2e.test.ts +0 -431
  950. package/src/__tests__/sandbox-diagnostics.test.ts +0 -138
  951. package/src/__tests__/sandbox-host-parity.test.ts +0 -1024
  952. package/src/__tests__/secret-detection-handler.test.ts +0 -67
  953. package/src/__tests__/secret-scanner-executor.test.ts +0 -450
  954. package/src/__tests__/tcc-sandbox-deny.test.ts +0 -198
  955. package/src/__tests__/terminal-sandbox.test.ts +0 -374
  956. package/src/__tests__/tool-notification-listener.test.ts +0 -65
  957. package/src/__tests__/twilio-rest.test.ts +0 -34
  958. package/src/backup/__tests__/backup-key.test.ts +0 -152
  959. package/src/backup/__tests__/backup-worker.test.ts +0 -754
  960. package/src/backup/__tests__/offsite-writer.test.ts +0 -641
  961. package/src/backup/__tests__/stream-crypt.test.ts +0 -228
  962. package/src/backup/backup-key.ts +0 -137
  963. package/src/backup/backup-worker.ts +0 -438
  964. package/src/backup/offsite-writer.ts +0 -222
  965. package/src/backup/stream-crypt.ts +0 -263
  966. package/src/context/__tests__/microcompact.test.ts +0 -805
  967. package/src/context/microcompact.ts +0 -443
  968. package/src/daemon/handlers/slack-channel-oauth-install.ts +0 -197
  969. package/src/daemon/message-types/pairing.ts +0 -58
  970. package/src/events/tool-notification-listener.ts +0 -17
  971. package/src/ipc/routes/__tests__/memory-v2-validate.test.ts +0 -219
  972. package/src/memory/v2/__tests__/edges.test.ts +0 -435
  973. package/src/memory/v2/edges.ts +0 -217
  974. package/src/outbound-proxy/config.ts +0 -94
  975. package/src/outbound-proxy/health.ts +0 -62
  976. package/src/outbound-proxy/types.ts +0 -150
  977. package/src/prompts/__tests__/system-prompt-memory-v2.test.ts +0 -197
  978. package/src/runtime/__tests__/chrome-extension-registry.test.ts +0 -518
  979. package/src/runtime/__tests__/client-registry.test.ts +0 -271
  980. package/src/runtime/capability-tokens.ts +0 -190
  981. package/src/runtime/chrome-extension-registry.ts +0 -368
  982. package/src/runtime/client-registry.ts +0 -254
  983. package/src/runtime/routes/inbound-stages/verification-intercept.ts +0 -329
  984. package/src/signals/mcp-reload.ts +0 -18
  985. package/src/tools/secret-detection-handler.ts +0 -269
  986. package/src/tools/terminal/backends/native.ts +0 -327
  987. package/src/tools/terminal/backends/types.ts +0 -37
  988. package/src/tools/terminal/sandbox-diagnostics.ts +0 -87
  989. package/src/tools/terminal/sandbox.ts +0 -40
@@ -1,14 +1,48 @@
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
+ mock.module("../heartbeat/heartbeat-run-store.js", () => ({
24
+ insertPendingHeartbeatRun: mockInsertPendingHeartbeatRun,
25
+ startHeartbeatRun: mockStartHeartbeatRun,
26
+ completeHeartbeatRun: mockCompleteHeartbeatRun,
27
+ skipHeartbeatRun: mockSkipHeartbeatRun,
28
+ supersedePendingRun: mockSupersedePendingRun,
29
+ markStaleRunsAsMissed: mockMarkStaleRunsAsMissed,
30
+ markStaleRunningAsError: mockMarkStaleRunningAsError,
31
+ }));
32
+
33
+ // ── Feed event mock ───────────────────────────────────────────────
34
+ const mockEmitFeedEvent = mock(() => Promise.resolve());
35
+ mock.module("../home/emit-feed-event.js", () => ({
36
+ emitFeedEvent: mockEmitFeedEvent,
37
+ }));
38
+
7
39
  // Mock config loader
8
40
  let mockConfig = {
9
41
  heartbeat: {
10
42
  enabled: true,
11
43
  intervalMs: 60_000,
44
+ cronExpression: null as string | null,
45
+ timezone: null as string | null,
12
46
  activeHoursStart: undefined as number | undefined,
13
47
  activeHoursEnd: undefined as number | undefined,
14
48
  },
@@ -22,6 +56,32 @@ mock.module("../config/loader.js", () => ({
22
56
  invalidateConfigCache: () => {},
23
57
  }));
24
58
 
59
+ // ── Recurrence engine mock ──────────────────────────────────────────
60
+ //
61
+ // HeartbeatService imports computeNextRunAt for cron scheduling.
62
+ // Tests mutate `mockComputeNextRunAt` to control the next cron occurrence.
63
+ let mockComputeNextRunAtResult: number | null = null;
64
+ let mockComputeNextRunAtError: Error | null = null;
65
+ let computeNextRunAtCallCount = 0;
66
+
67
+ mock.module("../schedule/recurrence-engine.js", () => ({
68
+ computeNextRunAt: (_spec: {
69
+ syntax: string;
70
+ expression: string;
71
+ timezone?: string | null;
72
+ }) => {
73
+ computeNextRunAtCallCount++;
74
+ if (mockComputeNextRunAtError) {
75
+ throw mockComputeNextRunAtError;
76
+ }
77
+ if (mockComputeNextRunAtResult != null) {
78
+ return mockComputeNextRunAtResult;
79
+ }
80
+ // Default: 1 hour from now
81
+ return Date.now() + 3_600_000;
82
+ },
83
+ }));
84
+
25
85
  // ── Guardian persona mock ─────────────────────────────────────────
26
86
  //
27
87
  // `heartbeat-service.isShallowProfile` reads the guardian persona via
@@ -90,16 +150,74 @@ mock.module("../memory/conversation-crud.js", () => ({
90
150
  },
91
151
  }));
92
152
 
93
- // Mock logger
153
+ // Mock logger — capture warn calls for unreachable-credential assertions
154
+ const loggerWarnCalls: Array<Record<string, unknown>> = [];
94
155
  mock.module("../util/logger.js", () => ({
95
156
  getLogger: () => ({
96
157
  info: () => {},
97
158
  debug: () => {},
98
- warn: () => {},
159
+ warn: (...args: unknown[]) => {
160
+ if (args.length > 0 && typeof args[0] === "object" && args[0] !== null) {
161
+ loggerWarnCalls.push(args[0] as Record<string, unknown>);
162
+ }
163
+ },
99
164
  error: () => {},
100
165
  }),
101
166
  }));
102
167
 
168
+ // ── Credential health mock ──────────────────────────────────────────
169
+ //
170
+ // HeartbeatService dynamically imports `checkAllCredentials` inside
171
+ // `runCredentialHealthCheck`, so `mock.module` intercepts it. Tests
172
+ // mutate `mockCredentialHealthReport` to drive different scenarios.
173
+ import type {
174
+ CredentialHealthReport,
175
+ CredentialHealthResult,
176
+ CredentialHealthStatus,
177
+ } from "../credential-health/credential-health-service.js";
178
+
179
+ let mockCredentialHealthReport: CredentialHealthReport | null = null;
180
+ let mockCheckAllCredentialsFail = false;
181
+
182
+ mock.module("../credential-health/credential-health-service.js", () => ({
183
+ checkAllCredentials: async () => {
184
+ if (mockCheckAllCredentialsFail) {
185
+ throw new Error("CES unreachable");
186
+ }
187
+ return (
188
+ mockCredentialHealthReport ?? {
189
+ checkedAt: Date.now(),
190
+ results: [],
191
+ unhealthy: [],
192
+ }
193
+ );
194
+ },
195
+ }));
196
+
197
+ // ── Notification signal mock ────────────────────────────────────────
198
+ //
199
+ // `notifyUnhealthyCredentials` dynamically imports `emitNotificationSignal`.
200
+ // Track calls so tests can assert which credentials were notified about.
201
+ const emittedNotificationSignals: Array<{
202
+ sourceContextId: string;
203
+ dedupeKey: string;
204
+ contextPayload: Record<string, unknown>;
205
+ }> = [];
206
+
207
+ mock.module("../notifications/emit-signal.js", () => ({
208
+ emitNotificationSignal: async (opts: {
209
+ sourceContextId: string;
210
+ dedupeKey: string;
211
+ contextPayload: Record<string, unknown>;
212
+ }) => {
213
+ emittedNotificationSignals.push({
214
+ sourceContextId: opts.sourceContextId,
215
+ dedupeKey: opts.dedupeKey,
216
+ contextPayload: opts.contextPayload,
217
+ });
218
+ },
219
+ }));
220
+
103
221
  // Mock conversation title service
104
222
  mock.module("../memory/conversation-title-service.js", () => ({
105
223
  GENERATING_TITLE: "Generating title...",
@@ -108,16 +226,18 @@ mock.module("../memory/conversation-title-service.js", () => ({
108
226
 
109
227
  // Mock processMessage — HeartbeatService now imports it directly.
110
228
  // Tests override _testProcessMessage to capture / customize calls.
111
- let _testProcessMessage: ((...args: unknown[]) => Promise<{ messageId: string }>) | undefined;
229
+ let _testProcessMessage:
230
+ | ((...args: unknown[]) => Promise<{ messageId: string }>)
231
+ | undefined;
112
232
 
113
233
  mock.module("../daemon/process-message.js", () => ({
114
234
  processMessage: async (...args: unknown[]) => {
115
235
  if (_testProcessMessage) return _testProcessMessage(...args);
116
236
  return { messageId: `mock-msg-${Date.now()}` };
117
237
  },
238
+ processMessageInBackground: async () => ({ messageId: "mock-bg" }),
118
239
  resolveTurnChannel: () => "vellum",
119
240
  resolveTurnInterface: () => "vellum",
120
- makePendingInteractionRegistrar: () => () => {},
121
241
  prepareConversationForMessage: async () => ({}),
122
242
  }));
123
243
 
@@ -199,6 +319,13 @@ describe("HeartbeatService", () => {
199
319
  createdConversations.length = 0;
200
320
  conversationIdCounter = 0;
201
321
  mockGuardianPersona = null;
322
+ mockCredentialHealthReport = null;
323
+ mockCheckAllCredentialsFail = false;
324
+ emittedNotificationSignals.length = 0;
325
+ loggerWarnCalls.length = 0;
326
+ mockComputeNextRunAtResult = null;
327
+ mockComputeNextRunAtError = null;
328
+ computeNextRunAtCallCount = 0;
202
329
 
203
330
  // Default processMessage mock: capture calls for assertions.
204
331
  setTestProcessMessage(async (...args: unknown[]) => {
@@ -210,10 +337,29 @@ describe("HeartbeatService", () => {
210
337
  return { messageId: "msg-1" };
211
338
  });
212
339
 
340
+ mockInsertPendingHeartbeatRun.mockClear();
341
+ mockInsertPendingHeartbeatRun.mockImplementation(() => "mock-run-id");
342
+ mockStartHeartbeatRun.mockClear();
343
+ mockStartHeartbeatRun.mockImplementation(() => true);
344
+ mockCompleteHeartbeatRun.mockClear();
345
+ mockCompleteHeartbeatRun.mockImplementation(() => true);
346
+ mockSkipHeartbeatRun.mockClear();
347
+ mockSkipHeartbeatRun.mockImplementation(() => true);
348
+ mockSupersedePendingRun.mockClear();
349
+ mockSupersedePendingRun.mockImplementation(() => true);
350
+ mockMarkStaleRunsAsMissed.mockClear();
351
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
352
+ mockMarkStaleRunningAsError.mockClear();
353
+ mockMarkStaleRunningAsError.mockImplementation(() => 0);
354
+ mockEmitFeedEvent.mockClear();
355
+ mockEmitFeedEvent.mockImplementation(() => Promise.resolve());
356
+
213
357
  mockConfig = {
214
358
  heartbeat: {
215
359
  enabled: true,
216
360
  intervalMs: 60_000,
361
+ cronExpression: null,
362
+ timezone: null,
217
363
  activeHoursStart: undefined,
218
364
  activeHoursEnd: undefined,
219
365
  },
@@ -221,9 +367,7 @@ describe("HeartbeatService", () => {
221
367
  });
222
368
 
223
369
  function createService(overrides?: {
224
- processMessage?: (
225
- ...args: unknown[]
226
- ) => Promise<{ messageId: string }>;
370
+ processMessage?: (...args: unknown[]) => Promise<{ messageId: string }>;
227
371
  getCurrentHour?: () => number;
228
372
  }) {
229
373
  if (overrides?.processMessage) {
@@ -771,4 +915,859 @@ describe("HeartbeatService", () => {
771
915
  expect(prompt).toContain("google, slack");
772
916
  });
773
917
  });
918
+
919
+ describe("transient credential health suppression", () => {
920
+ function makeUnhealthyResult(
921
+ overrides: Partial<CredentialHealthResult> = {},
922
+ ): CredentialHealthResult {
923
+ return {
924
+ connectionId: overrides.connectionId ?? "conn-1",
925
+ provider: overrides.provider ?? "google",
926
+ accountInfo: overrides.accountInfo ?? "user@example.com",
927
+ status: overrides.status ?? ("missing_token" as CredentialHealthStatus),
928
+ details: overrides.details ?? "Token not found",
929
+ missingScopes: overrides.missingScopes ?? [],
930
+ canAutoRecover: overrides.canAutoRecover ?? false,
931
+ };
932
+ }
933
+
934
+ test("unreachable credentials do not trigger notifications", async () => {
935
+ mockCredentialHealthReport = {
936
+ checkedAt: Date.now(),
937
+ results: [
938
+ makeUnhealthyResult({
939
+ connectionId: "conn-google",
940
+ provider: "google",
941
+ status: "unreachable",
942
+ details: "CES backend unavailable",
943
+ }),
944
+ ],
945
+ unhealthy: [
946
+ makeUnhealthyResult({
947
+ connectionId: "conn-google",
948
+ provider: "google",
949
+ status: "unreachable",
950
+ details: "CES backend unavailable",
951
+ }),
952
+ ],
953
+ };
954
+
955
+ const service = createService();
956
+ await service.runOnce();
957
+
958
+ // No notification signals should have been emitted for unreachable
959
+ expect(emittedNotificationSignals).toHaveLength(0);
960
+ });
961
+
962
+ test("unreachable credentials do not block provider tools in heartbeat prompt", async () => {
963
+ mockCredentialHealthReport = {
964
+ checkedAt: Date.now(),
965
+ results: [
966
+ makeUnhealthyResult({
967
+ connectionId: "conn-google",
968
+ provider: "google",
969
+ status: "unreachable",
970
+ details: "CES backend unavailable",
971
+ }),
972
+ ],
973
+ unhealthy: [
974
+ makeUnhealthyResult({
975
+ connectionId: "conn-google",
976
+ provider: "google",
977
+ status: "unreachable",
978
+ details: "CES backend unavailable",
979
+ }),
980
+ ],
981
+ };
982
+
983
+ const service = createService();
984
+ await service.runOnce();
985
+
986
+ // The prompt should NOT contain <credential-status> since unreachable
987
+ // is not a hard failure and should not tell the LLM to skip providers
988
+ expect(processMessageCalls).toHaveLength(1);
989
+ expect(processMessageCalls[0].content).not.toContain(
990
+ "<credential-status>",
991
+ );
992
+ });
993
+
994
+ test("unreachable credentials log a warning", async () => {
995
+ mockCredentialHealthReport = {
996
+ checkedAt: Date.now(),
997
+ results: [
998
+ makeUnhealthyResult({
999
+ connectionId: "conn-google",
1000
+ provider: "google",
1001
+ status: "unreachable",
1002
+ details: "CES backend unavailable",
1003
+ }),
1004
+ ],
1005
+ unhealthy: [
1006
+ makeUnhealthyResult({
1007
+ connectionId: "conn-google",
1008
+ provider: "google",
1009
+ status: "unreachable",
1010
+ details: "CES backend unavailable",
1011
+ }),
1012
+ ],
1013
+ };
1014
+
1015
+ const service = createService();
1016
+ await service.runOnce();
1017
+
1018
+ // Logger warn should have been called with unreachableCount
1019
+ const unreachableWarns = loggerWarnCalls.filter(
1020
+ (call) => "unreachableCount" in call,
1021
+ );
1022
+ expect(unreachableWarns).toHaveLength(1);
1023
+ expect(unreachableWarns[0].unreachableCount).toBe(1);
1024
+ });
1025
+
1026
+ test("missing_token still notifies and blocks provider tools", async () => {
1027
+ mockCredentialHealthReport = {
1028
+ checkedAt: Date.now(),
1029
+ results: [
1030
+ makeUnhealthyResult({
1031
+ connectionId: "conn-google",
1032
+ provider: "google",
1033
+ status: "missing_token",
1034
+ details: "Token not found in keychain",
1035
+ }),
1036
+ ],
1037
+ unhealthy: [
1038
+ makeUnhealthyResult({
1039
+ connectionId: "conn-google",
1040
+ provider: "google",
1041
+ status: "missing_token",
1042
+ details: "Token not found in keychain",
1043
+ }),
1044
+ ],
1045
+ };
1046
+
1047
+ const service = createService();
1048
+ await service.runOnce();
1049
+
1050
+ // Should have emitted a notification for missing_token
1051
+ expect(emittedNotificationSignals).toHaveLength(1);
1052
+ expect(emittedNotificationSignals[0].contextPayload.status).toBe(
1053
+ "missing_token",
1054
+ );
1055
+ expect(emittedNotificationSignals[0].contextPayload.provider).toBe(
1056
+ "google",
1057
+ );
1058
+
1059
+ // Prompt should include <credential-status> blocking google
1060
+ expect(processMessageCalls).toHaveLength(1);
1061
+ expect(processMessageCalls[0].content).toContain("<credential-status>");
1062
+ expect(processMessageCalls[0].content).toContain("google");
1063
+ });
1064
+
1065
+ test("mixed report notifies only actionable failures, not unreachable", async () => {
1066
+ mockCredentialHealthReport = {
1067
+ checkedAt: Date.now(),
1068
+ results: [
1069
+ makeUnhealthyResult({
1070
+ connectionId: "conn-google",
1071
+ provider: "google",
1072
+ status: "unreachable",
1073
+ details: "CES backend unavailable",
1074
+ }),
1075
+ makeUnhealthyResult({
1076
+ connectionId: "conn-slack",
1077
+ provider: "slack",
1078
+ status: "revoked",
1079
+ details: "Token was revoked by user",
1080
+ }),
1081
+ makeUnhealthyResult({
1082
+ connectionId: "conn-github",
1083
+ provider: "github",
1084
+ status: "unreachable",
1085
+ details: "CES backend unavailable",
1086
+ }),
1087
+ ],
1088
+ unhealthy: [
1089
+ makeUnhealthyResult({
1090
+ connectionId: "conn-google",
1091
+ provider: "google",
1092
+ status: "unreachable",
1093
+ details: "CES backend unavailable",
1094
+ }),
1095
+ makeUnhealthyResult({
1096
+ connectionId: "conn-slack",
1097
+ provider: "slack",
1098
+ status: "revoked",
1099
+ details: "Token was revoked by user",
1100
+ }),
1101
+ makeUnhealthyResult({
1102
+ connectionId: "conn-github",
1103
+ provider: "github",
1104
+ status: "unreachable",
1105
+ details: "CES backend unavailable",
1106
+ }),
1107
+ ],
1108
+ };
1109
+
1110
+ const service = createService();
1111
+ await service.runOnce();
1112
+
1113
+ // Only the revoked credential should trigger a notification
1114
+ expect(emittedNotificationSignals).toHaveLength(1);
1115
+ expect(emittedNotificationSignals[0].contextPayload.provider).toBe(
1116
+ "slack",
1117
+ );
1118
+ expect(emittedNotificationSignals[0].contextPayload.status).toBe(
1119
+ "revoked",
1120
+ );
1121
+
1122
+ // Only slack (revoked = hard failure) should appear in credential-status
1123
+ expect(processMessageCalls).toHaveLength(1);
1124
+ expect(processMessageCalls[0].content).toContain("<credential-status>");
1125
+ expect(processMessageCalls[0].content).toContain("slack");
1126
+ // google and github are unreachable — should NOT be in credential-status
1127
+ expect(processMessageCalls[0].content).not.toContain("google");
1128
+ expect(processMessageCalls[0].content).not.toContain("github");
1129
+
1130
+ // Should have logged a warning about the 2 unreachable credentials
1131
+ const unreachableWarns = loggerWarnCalls.filter(
1132
+ (call) => "unreachableCount" in call,
1133
+ );
1134
+ expect(unreachableWarns).toHaveLength(1);
1135
+ expect(unreachableWarns[0].unreachableCount).toBe(2);
1136
+ });
1137
+ });
1138
+
1139
+ describe("cron scheduling mode", () => {
1140
+ test("start() with cronExpression sets nextRunAt to cron occurrence, not now+intervalMs", () => {
1141
+ const cronNextRunAt = Date.now() + 7_200_000; // 2 hours from now
1142
+ mockComputeNextRunAtResult = cronNextRunAt;
1143
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1144
+ mockConfig.heartbeat.timezone = "America/New_York";
1145
+
1146
+ const service = createService();
1147
+ service.start();
1148
+
1149
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1150
+ // Should NOT be now + intervalMs
1151
+ expect(service.nextRunAt).not.toBeCloseTo(
1152
+ Date.now() + mockConfig.heartbeat.intervalMs,
1153
+ -3,
1154
+ );
1155
+ service.stop();
1156
+ });
1157
+
1158
+ test("runOnce() does not call scheduleNextRun(intervalMs) in cron mode — nextRunAt is not clobbered", async () => {
1159
+ const cronNextRunAt = Date.now() + 7_200_000;
1160
+ mockComputeNextRunAtResult = cronNextRunAt;
1161
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1162
+
1163
+ const service = createService();
1164
+ service.start();
1165
+
1166
+ // nextRunAt should be the cron time before runOnce
1167
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1168
+
1169
+ await service.runOnce();
1170
+
1171
+ // After runOnce(), nextRunAt should still reflect a cron time, not now + intervalMs.
1172
+ // The finally chain in scheduleNextCronRun recalculates it, but the runOnce()
1173
+ // finally block should NOT have called scheduleNextRun(intervalMs).
1174
+ // Since our mock always returns cronNextRunAt, nextRunAt should remain that value.
1175
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1176
+ service.stop();
1177
+ });
1178
+
1179
+ test("after runOnce() rejects in cron mode, the next cron run is still scheduled via finally", async () => {
1180
+ const cronNextRunAt = Date.now() + 7_200_000;
1181
+ mockComputeNextRunAtResult = cronNextRunAt;
1182
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1183
+
1184
+ const service = createService({
1185
+ processMessage: async () => {
1186
+ throw new Error("LLM down");
1187
+ },
1188
+ });
1189
+ service.start();
1190
+
1191
+ await service.runOnce();
1192
+
1193
+ // Even though executeRun failed, the service should still have a nextRunAt
1194
+ // set to the cron occurrence (the finally chain reschedules)
1195
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1196
+ service.stop();
1197
+ });
1198
+
1199
+ test("resetTimer() in cron mode recomputes from the current time", () => {
1200
+ const firstCronTime = Date.now() + 3_600_000;
1201
+ mockComputeNextRunAtResult = firstCronTime;
1202
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1203
+
1204
+ const service = createService();
1205
+ service.start();
1206
+ expect(service.nextRunAt).toBe(firstCronTime);
1207
+
1208
+ // Simulate time passing and a new cron occurrence
1209
+ const secondCronTime = Date.now() + 5_400_000;
1210
+ mockComputeNextRunAtResult = secondCronTime;
1211
+
1212
+ service.resetTimer();
1213
+ expect(service.nextRunAt).toBe(secondCronTime);
1214
+ service.stop();
1215
+ });
1216
+
1217
+ test("reconfigure() switches from interval to cron mode", () => {
1218
+ const service = createService();
1219
+ // Start in interval mode
1220
+ service.start();
1221
+ const intervalNextRunAt = service.nextRunAt;
1222
+ expect(intervalNextRunAt).not.toBeNull();
1223
+
1224
+ // Reconfigure to cron mode
1225
+ const cronNextRunAt = Date.now() + 7_200_000;
1226
+ mockComputeNextRunAtResult = cronNextRunAt;
1227
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1228
+ service.reconfigure();
1229
+
1230
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1231
+ service.stop();
1232
+ });
1233
+
1234
+ test("reconfigure() switches from cron to interval mode", () => {
1235
+ const cronNextRunAt = Date.now() + 7_200_000;
1236
+ mockComputeNextRunAtResult = cronNextRunAt;
1237
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1238
+
1239
+ const service = createService();
1240
+ service.start();
1241
+ expect(service.nextRunAt).toBe(cronNextRunAt);
1242
+
1243
+ // Reconfigure to interval mode
1244
+ mockConfig.heartbeat.cronExpression = null;
1245
+ const before = Date.now();
1246
+ service.reconfigure();
1247
+
1248
+ expect(service.nextRunAt).not.toBeNull();
1249
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(
1250
+ before + mockConfig.heartbeat.intervalMs,
1251
+ );
1252
+ service.stop();
1253
+ });
1254
+
1255
+ test("active hours guard uses cron timezone when configured", async () => {
1256
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1257
+ mockConfig.heartbeat.timezone = "UTC";
1258
+ mockConfig.heartbeat.activeHoursStart = 9;
1259
+ mockConfig.heartbeat.activeHoursEnd = 17;
1260
+ mockComputeNextRunAtResult = Date.now() + 3_600_000;
1261
+
1262
+ const service = createService();
1263
+ service.start();
1264
+
1265
+ // In cron mode with timezone, the hour is computed via Intl.DateTimeFormat
1266
+ // rather than getCurrentHour(). The test verifies the code path runs without
1267
+ // error — the actual hour depends on the system clock and UTC conversion.
1268
+ // We just verify it doesn't throw and returns a boolean result.
1269
+ const result = await service.runOnce();
1270
+ // Result depends on current UTC hour vs active window — either outcome is valid
1271
+ expect(typeof result).toBe("boolean");
1272
+ service.stop();
1273
+ });
1274
+
1275
+ test("active hours guard falls back to getCurrentHour when cron mode has no timezone", async () => {
1276
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1277
+ mockConfig.heartbeat.timezone = null;
1278
+ mockConfig.heartbeat.activeHoursStart = 9;
1279
+ mockConfig.heartbeat.activeHoursEnd = 17;
1280
+ mockComputeNextRunAtResult = Date.now() + 3_600_000;
1281
+
1282
+ // getCurrentHour returns 3 (outside 9-17 window), so runOnce should skip
1283
+ const service = createService({ getCurrentHour: () => 3 });
1284
+ service.start();
1285
+ const result = await service.runOnce();
1286
+ expect(result).toBe(false);
1287
+ expect(processMessageCalls).toHaveLength(0);
1288
+ service.stop();
1289
+ });
1290
+
1291
+ test("runtime fallback: computeNextRunAt throws, service falls back to interval mode", () => {
1292
+ mockComputeNextRunAtError = new Error("No upcoming runs");
1293
+ mockConfig.heartbeat.cronExpression = "0 9,12,15,18 * * *";
1294
+
1295
+ const service = createService();
1296
+ service.start();
1297
+
1298
+ // Should have fallen back to interval mode — nextRunAt should be ~now + intervalMs
1299
+ expect(service.nextRunAt).not.toBeNull();
1300
+ const expectedMin = Date.now() + mockConfig.heartbeat.intervalMs - 100;
1301
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(expectedMin);
1302
+
1303
+ // Should have logged a warning about the fallback
1304
+ const fallbackWarns = loggerWarnCalls.filter((call) => "err" in call);
1305
+ expect(fallbackWarns.length).toBeGreaterThanOrEqual(1);
1306
+ service.stop();
1307
+ });
1308
+
1309
+ test("null cronExpression behaves identically to current fixed-interval mode", () => {
1310
+ mockConfig.heartbeat.cronExpression = null;
1311
+
1312
+ const service = createService();
1313
+ const before = Date.now();
1314
+ service.start();
1315
+
1316
+ expect(service.nextRunAt).not.toBeNull();
1317
+ expect(service.nextRunAt!).toBeGreaterThanOrEqual(
1318
+ before + mockConfig.heartbeat.intervalMs,
1319
+ );
1320
+ // computeNextRunAt should not have been called
1321
+ expect(computeNextRunAtCallCount).toBe(0);
1322
+ service.stop();
1323
+ });
1324
+ });
1325
+
1326
+ describe("heartbeat run store instrumentation", () => {
1327
+ test("successful run: pending → running → ok with conversationId", async () => {
1328
+ const service = createService();
1329
+ await service.runOnce();
1330
+
1331
+ expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
1332
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
1333
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1334
+ status: "ok",
1335
+ conversationId: "conv-1",
1336
+ });
1337
+ });
1338
+
1339
+ test("failed run: pending → running → error preserving conversationId", async () => {
1340
+ const service = createService({
1341
+ processMessage: async () => {
1342
+ throw new Error("LLM timeout");
1343
+ },
1344
+ });
1345
+
1346
+ await service.runOnce();
1347
+
1348
+ expect(mockStartHeartbeatRun).toHaveBeenCalledTimes(1);
1349
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledTimes(1);
1350
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1351
+ status: "error",
1352
+ conversationId: "conv-1",
1353
+ error: "LLM timeout",
1354
+ });
1355
+ });
1356
+
1357
+ test("CAS false suppresses success feed event", async () => {
1358
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1359
+
1360
+ const service = createService();
1361
+ await service.runOnce();
1362
+
1363
+ // completeHeartbeatRun returned false, so no feed event should be emitted for success
1364
+ const successCalls = mockEmitFeedEvent.mock.calls.filter(
1365
+ (call: unknown[]) => {
1366
+ const opts = call[0] as { dedupKey?: string };
1367
+ return opts.dedupKey?.startsWith("heartbeat:ok:");
1368
+ },
1369
+ );
1370
+ expect(successCalls).toHaveLength(0);
1371
+ });
1372
+
1373
+ test("CAS false suppresses failure alerter and feed event", async () => {
1374
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1375
+
1376
+ const service = createService({
1377
+ processMessage: async () => {
1378
+ throw new Error("LLM timeout");
1379
+ },
1380
+ });
1381
+
1382
+ await service.runOnce();
1383
+
1384
+ // completeHeartbeatRun returned false, so alerter should NOT be called
1385
+ expect(alerterCalls).toHaveLength(0);
1386
+
1387
+ // No failure feed event either
1388
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1389
+ (call: unknown[]) => {
1390
+ const opts = call[0] as { dedupKey?: string };
1391
+ return opts.dedupKey?.startsWith("heartbeat:fail:");
1392
+ },
1393
+ );
1394
+ expect(failCalls).toHaveLength(0);
1395
+ });
1396
+
1397
+ test("active-hours skip calls skipHeartbeatRun", async () => {
1398
+ mockConfig.heartbeat.activeHoursStart = 9;
1399
+ mockConfig.heartbeat.activeHoursEnd = 17;
1400
+
1401
+ const service = createService({ getCurrentHour: () => 3 });
1402
+ service.start();
1403
+ await service.runOnce();
1404
+
1405
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1406
+ "mock-run-id",
1407
+ "outside_active_hours",
1408
+ );
1409
+ service.stop();
1410
+ });
1411
+
1412
+ test("overlap skip calls skipHeartbeatRun", async () => {
1413
+ let resolveFirst: () => void;
1414
+ const firstPromise = new Promise<void>((r) => {
1415
+ resolveFirst = r;
1416
+ });
1417
+
1418
+ const service = createService({
1419
+ processMessage: async () => {
1420
+ await firstPromise;
1421
+ return { messageId: "msg-1" };
1422
+ },
1423
+ });
1424
+
1425
+ // Start first run (will block)
1426
+ const run1 = service.runOnce();
1427
+ await new Promise((r) => setTimeout(r, 10));
1428
+
1429
+ // Start service so the second runOnce has a pending row
1430
+ service.start();
1431
+ mockSkipHeartbeatRun.mockClear();
1432
+
1433
+ // Second run should be skipped due to overlap
1434
+ await service.runOnce();
1435
+
1436
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1437
+ "mock-run-id",
1438
+ "overlap",
1439
+ );
1440
+
1441
+ resolveFirst!();
1442
+ await run1;
1443
+ service.stop();
1444
+ });
1445
+
1446
+ test("start() calls markStaleRunsAsMissed and markStaleRunningAsError", () => {
1447
+ const service = createService();
1448
+ service.start();
1449
+
1450
+ expect(mockMarkStaleRunsAsMissed).toHaveBeenCalledTimes(1);
1451
+ expect(mockMarkStaleRunningAsError).toHaveBeenCalledTimes(1);
1452
+ service.stop();
1453
+ });
1454
+
1455
+ test("scheduleNextRun supersedes old pending row before creating new one", () => {
1456
+ const service = createService();
1457
+ service.start();
1458
+
1459
+ // start() called scheduleNextRun which set _pendingRunId.
1460
+ // Calling resetTimer triggers another scheduleNextRun which
1461
+ // should supersede the existing pending row before inserting
1462
+ // a new one.
1463
+ const callOrder: string[] = [];
1464
+ mockSupersedePendingRun.mockImplementation(() => {
1465
+ callOrder.push("supersede");
1466
+ return true;
1467
+ });
1468
+ mockInsertPendingHeartbeatRun.mockImplementation(() => {
1469
+ callOrder.push("insert");
1470
+ return "mock-run-id";
1471
+ });
1472
+
1473
+ service.resetTimer();
1474
+
1475
+ // resetTimer's scheduleNextRun should supersede then insert
1476
+ expect(callOrder.filter((c) => c === "supersede").length).toBeGreaterThan(
1477
+ 0,
1478
+ );
1479
+ const firstSupersede = callOrder.indexOf("supersede");
1480
+ const firstInsert = callOrder.indexOf("insert");
1481
+ expect(firstSupersede).toBeLessThan(firstInsert);
1482
+
1483
+ service.stop();
1484
+ });
1485
+
1486
+ test("resetTimer() supersedes pending row", () => {
1487
+ const service = createService();
1488
+ service.start();
1489
+
1490
+ mockSupersedePendingRun.mockClear();
1491
+ service.resetTimer();
1492
+
1493
+ // resetTimer calls scheduleNextRun which supersedes existing pending
1494
+ expect(mockSupersedePendingRun).toHaveBeenCalled();
1495
+ service.stop();
1496
+ });
1497
+
1498
+ test("force run creates its own pending row, does not consume scheduled one", async () => {
1499
+ const service = createService();
1500
+ service.start();
1501
+
1502
+ // Clear to track only the force run's calls
1503
+ mockInsertPendingHeartbeatRun.mockClear();
1504
+
1505
+ await service.runOnce({ force: true });
1506
+
1507
+ // Force run should have called insertPendingHeartbeatRun for itself
1508
+ // (at least once for its own row, plus the scheduleNextRun in finally)
1509
+ expect(mockInsertPendingHeartbeatRun).toHaveBeenCalled();
1510
+
1511
+ // The scheduled pending row (from start()) should NOT have been consumed
1512
+ // by the force run — force creates its own
1513
+ service.stop();
1514
+ });
1515
+
1516
+ test("disabled config with stale pending row skips it as disabled", async () => {
1517
+ const service = createService();
1518
+ service.start();
1519
+
1520
+ // Now disable config and call runOnce — should skip the pending row
1521
+ mockConfig.heartbeat.enabled = false;
1522
+ mockSkipHeartbeatRun.mockClear();
1523
+
1524
+ await service.runOnce();
1525
+
1526
+ expect(mockSkipHeartbeatRun).toHaveBeenCalledWith(
1527
+ "mock-run-id",
1528
+ "disabled",
1529
+ );
1530
+ service.stop();
1531
+ });
1532
+
1533
+ test("stop() supersedes outstanding pending row", async () => {
1534
+ const service = createService();
1535
+ service.start();
1536
+
1537
+ mockSupersedePendingRun.mockClear();
1538
+ await service.stop();
1539
+
1540
+ expect(mockSupersedePendingRun).toHaveBeenCalledWith("mock-run-id");
1541
+ });
1542
+
1543
+ test("timeout calls completeHeartbeatRun with status timeout", async () => {
1544
+ jest.useFakeTimers();
1545
+ try {
1546
+ let resolveRun: () => void;
1547
+ const runPromise = new Promise<void>((r) => {
1548
+ resolveRun = r;
1549
+ });
1550
+
1551
+ const service = createService({
1552
+ processMessage: async () => {
1553
+ await runPromise;
1554
+ return { messageId: "msg-1" };
1555
+ },
1556
+ });
1557
+
1558
+ const runOncePromise = service.runOnce();
1559
+ // Advance past the 30-minute timeout
1560
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1561
+ await runOncePromise;
1562
+
1563
+ expect(mockCompleteHeartbeatRun).toHaveBeenCalledWith("mock-run-id", {
1564
+ status: "timeout",
1565
+ error: "Heartbeat execution exceeded the 30-minute timeout",
1566
+ });
1567
+
1568
+ // Clean up — resolve the hanging promise so it doesn't leak
1569
+ resolveRun!();
1570
+ } finally {
1571
+ jest.useRealTimers();
1572
+ }
1573
+ });
1574
+
1575
+ test("failure feed event has urgency high and includes error message", async () => {
1576
+ const service = createService({
1577
+ processMessage: async () => {
1578
+ throw new Error("web_search outage");
1579
+ },
1580
+ });
1581
+
1582
+ await service.runOnce();
1583
+
1584
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1585
+ (call: unknown[]) => {
1586
+ const opts = call[0] as { title?: string };
1587
+ return opts.title === "Heartbeat Failed";
1588
+ },
1589
+ );
1590
+ expect(failCalls).toHaveLength(1);
1591
+ const opts = (failCalls as any[][])[0][0] as {
1592
+ urgency?: string;
1593
+ summary?: string;
1594
+ };
1595
+ expect(opts.urgency).toBe("high");
1596
+ expect(opts.summary).toContain("web_search outage");
1597
+ });
1598
+
1599
+ test("CAS false on complete suppresses failure feed event", async () => {
1600
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1601
+
1602
+ const service = createService({
1603
+ processMessage: async () => {
1604
+ throw new Error("some error");
1605
+ },
1606
+ });
1607
+
1608
+ await service.runOnce();
1609
+
1610
+ const failCalls = mockEmitFeedEvent.mock.calls.filter(
1611
+ (call: unknown[]) => {
1612
+ const opts = call[0] as { title?: string };
1613
+ return opts.title === "Heartbeat Failed";
1614
+ },
1615
+ );
1616
+ expect(failCalls).toHaveLength(0);
1617
+ });
1618
+
1619
+ test("timeout emits feed event with urgency high", async () => {
1620
+ jest.useFakeTimers();
1621
+ try {
1622
+ let resolveRun: () => void;
1623
+ const runPromise = new Promise<void>((r) => {
1624
+ resolveRun = r;
1625
+ });
1626
+
1627
+ const service = createService({
1628
+ processMessage: async () => {
1629
+ await runPromise;
1630
+ return { messageId: "msg-1" };
1631
+ },
1632
+ });
1633
+
1634
+ const runOncePromise = service.runOnce();
1635
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1636
+ await runOncePromise;
1637
+
1638
+ const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
1639
+ (call: unknown[]) => {
1640
+ const opts = call[0] as { title?: string };
1641
+ return opts.title === "Heartbeat Timed Out";
1642
+ },
1643
+ );
1644
+ expect(timeoutCalls).toHaveLength(1);
1645
+ const opts = (timeoutCalls as any[][])[0][0] as {
1646
+ urgency?: string;
1647
+ };
1648
+ expect(opts.urgency).toBe("high");
1649
+
1650
+ resolveRun!();
1651
+ } finally {
1652
+ jest.useRealTimers();
1653
+ }
1654
+ });
1655
+
1656
+ test("CAS false on timeout suppresses timeout feed event", async () => {
1657
+ jest.useFakeTimers();
1658
+ try {
1659
+ mockCompleteHeartbeatRun.mockImplementation(() => false);
1660
+
1661
+ let resolveRun: () => void;
1662
+ const runPromise = new Promise<void>((r) => {
1663
+ resolveRun = r;
1664
+ });
1665
+
1666
+ const service = createService({
1667
+ processMessage: async () => {
1668
+ await runPromise;
1669
+ return { messageId: "msg-1" };
1670
+ },
1671
+ });
1672
+
1673
+ const runOncePromise = service.runOnce();
1674
+ jest.advanceTimersByTime(30 * 60 * 1000 + 1000);
1675
+ await runOncePromise;
1676
+
1677
+ // completeHeartbeatRun returned false, so no timeout feed event
1678
+ const timeoutCalls = mockEmitFeedEvent.mock.calls.filter(
1679
+ (call: unknown[]) => {
1680
+ const opts = call[0] as { title?: string };
1681
+ return opts.title === "Heartbeat Timed Out";
1682
+ },
1683
+ );
1684
+ expect(timeoutCalls).toHaveLength(0);
1685
+
1686
+ resolveRun!();
1687
+ } finally {
1688
+ jest.useRealTimers();
1689
+ }
1690
+ });
1691
+
1692
+ test("late run emits late feed event", async () => {
1693
+ const service = createService();
1694
+ service.start();
1695
+
1696
+ // Set the pending run to be 10 minutes in the past
1697
+ (service as any)._nextRunAt = Date.now() - 10 * 60 * 1000;
1698
+ (service as any)._pendingRunId = "late-run-id";
1699
+
1700
+ await service.runOnce();
1701
+
1702
+ const lateCalls = mockEmitFeedEvent.mock.calls.filter(
1703
+ (call: unknown[]) => {
1704
+ const opts = call[0] as { title?: string };
1705
+ return opts.title === "Heartbeat Ran Late";
1706
+ },
1707
+ );
1708
+ expect(lateCalls).toHaveLength(1);
1709
+ const opts = (lateCalls as any[][])[0][0] as {
1710
+ urgency?: string;
1711
+ summary?: string;
1712
+ };
1713
+ expect(opts.urgency).toBe("medium");
1714
+ expect(opts.summary).toContain("10 minutes late");
1715
+
1716
+ await service.stop();
1717
+ });
1718
+
1719
+ test("on-time run does not emit late feed event", async () => {
1720
+ const service = createService();
1721
+ await service.runOnce();
1722
+
1723
+ const lateCalls = mockEmitFeedEvent.mock.calls.filter(
1724
+ (call: unknown[]) => {
1725
+ const opts = call[0] as { title?: string };
1726
+ return opts.title === "Heartbeat Ran Late";
1727
+ },
1728
+ );
1729
+ expect(lateCalls).toHaveLength(0);
1730
+ });
1731
+
1732
+ test("start() emits missed-run feed event when stale rows exist", () => {
1733
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 2);
1734
+ mockMarkStaleRunningAsError.mockImplementation(() => 1);
1735
+
1736
+ const service = createService();
1737
+ service.start();
1738
+
1739
+ const missedCalls = mockEmitFeedEvent.mock.calls.filter(
1740
+ (call: unknown[]) => {
1741
+ const opts = call[0] as { title?: string };
1742
+ return opts.title === "Heartbeat Runs Missed";
1743
+ },
1744
+ );
1745
+ expect(missedCalls).toHaveLength(1);
1746
+ const opts = (missedCalls as any[][])[0][0] as {
1747
+ urgency?: string;
1748
+ summary?: string;
1749
+ };
1750
+ expect(opts.urgency).toBe("high");
1751
+ expect(opts.summary).toContain("3");
1752
+
1753
+ service.stop();
1754
+ });
1755
+
1756
+ test("start() does not emit missed-run feed event when counts are 0", () => {
1757
+ mockMarkStaleRunsAsMissed.mockImplementation(() => 0);
1758
+ mockMarkStaleRunningAsError.mockImplementation(() => 0);
1759
+
1760
+ const service = createService();
1761
+ service.start();
1762
+
1763
+ const missedCalls = mockEmitFeedEvent.mock.calls.filter(
1764
+ (call: unknown[]) => {
1765
+ const opts = call[0] as { title?: string };
1766
+ return opts.title === "Heartbeat Runs Missed";
1767
+ },
1768
+ );
1769
+ expect(missedCalls).toHaveLength(0);
1770
+ service.stop();
1771
+ });
1772
+ });
774
1773
  });