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