@vellumai/assistant 0.7.1 → 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.
- package/ARCHITECTURE.md +32 -49
- 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/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 +39 -1
- 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/skill-host-contracts/src/assistant-event.ts +9 -0
- 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 +565 -12
- 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 +374 -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 +109 -2
- package/src/__tests__/assistant-event.test.ts +10 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -2
- package/src/__tests__/assistant-feature-flags-integration.test.ts +11 -7
- package/src/__tests__/background-shell-host-bash.test.ts +14 -15
- 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-domain.test.ts +0 -2
- package/src/__tests__/call-routes-http.test.ts +0 -2
- package/src/__tests__/channel-readiness-service.test.ts +59 -1
- package/src/__tests__/checker.test.ts +3 -4
- package/src/__tests__/config-loader-backfill.test.ts +90 -155
- 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-set-platform-guard.test.ts +48 -4
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +2 -2
- package/src/__tests__/config-watcher.test.ts +2 -2
- 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-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-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-slash-commands.test.ts +0 -4
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +202 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +317 -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 +5 -12
- package/src/__tests__/cu-unified-flow.test.ts +185 -23
- package/src/__tests__/daemon-credential-client.test.ts +101 -19
- package/src/__tests__/db-schedule-syntax-migration.test.ts +2 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- 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-service.test.ts +718 -1
- package/src/__tests__/helpers/call-route-handler.ts +7 -1
- package/src/__tests__/host-app-control-proxy.test.ts +602 -0
- package/src/__tests__/host-app-control-routes.test.ts +263 -0
- package/src/__tests__/host-bash-proxy.test.ts +246 -47
- package/src/__tests__/host-bash-routes.test.ts +294 -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 +41 -52
- package/src/__tests__/host-cu-routes-targeted.test.ts +300 -0
- package/src/__tests__/host-file-edit-tool.test.ts +47 -1
- package/src/__tests__/host-file-proxy-targeted.test.ts +339 -0
- package/src/__tests__/host-file-proxy.test.ts +37 -43
- package/src/__tests__/host-file-read-tool.test.ts +17 -0
- package/src/__tests__/host-file-routes-targeted.test.ts +262 -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 +583 -0
- package/src/__tests__/host-transfer-proxy.test.ts +121 -22
- package/src/__tests__/host-transfer-routes-targeted.test.ts +447 -0
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- 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__/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-skill-lifecycle.test.ts +0 -1
- package/src/__tests__/mcp-auth-routes.test.ts +197 -0
- package/src/__tests__/mcp-cli.test.ts +338 -2
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +188 -0
- package/src/__tests__/migration-import-commit-http.test.ts +108 -2
- package/src/__tests__/mock-gateway-ipc.ts +1 -0
- package/src/__tests__/oauth-cli.test.ts +0 -2
- package/src/__tests__/oauth2-gateway-transport.test.ts +0 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +299 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +5 -9
- package/src/__tests__/prechat-onboarding-contract.test.ts +3 -1
- 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__/public-ingress-urls.test.ts +97 -0
- 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 +10 -6
- package/src/__tests__/sanitize-config-for-transfer.test.ts +24 -2
- package/src/__tests__/schedule-retry.test.ts +715 -0
- package/src/__tests__/script-proxy-mitm-handler.test.ts +1 -1
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- 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__/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-backfill-installation-id.test.ts +1 -5
- package/src/__tests__/workspace-migration-down-functions.test.ts +8 -8
- package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +10 -6
- 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/bundler/app-bundler.ts +51 -3
- 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 -1
- package/src/cli/commands/backup.ts +6 -331
- package/src/cli/commands/clients.ts +36 -37
- package/src/cli/commands/contacts.ts +73 -0
- package/src/cli/commands/conversations.ts +2 -5
- package/src/cli/commands/credentials.ts +15 -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 +296 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +0 -2
- package/src/cli/commands/platform/__tests__/status.test.ts +13 -15
- package/src/cli/commands/platform/disconnect.ts +5 -4
- package/src/cli/commands/platform/index.ts +0 -18
- package/src/cli/lib/daemon-credential-client.ts +110 -28
- package/src/cli/program.ts +2 -0
- package/src/config/assistant-feature-flags.ts +67 -10
- 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/phone-calls/TOOLS.json +0 -12
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +19 -4
- package/src/config/bundled-skills/playbooks/TOOLS.json +0 -16
- 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 -12
- package/src/config/feature-flag-registry.json +21 -133
- package/src/config/loader.ts +73 -99
- 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 +7 -4
- package/src/config/schemas/calls.ts +0 -9
- package/src/config/schemas/heartbeat.ts +63 -0
- package/src/config/schemas/ingress.ts +10 -6
- package/src/config/schemas/llm.ts +5 -10
- package/src/config/schemas/memory-lifecycle.ts +77 -24
- package/src/config/schemas/memory-v2.ts +48 -4
- package/src/config/schemas/platform.ts +6 -0
- package/src/config/schemas/services.ts +1 -15
- package/src/config/schemas/skills.ts +0 -6
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/contacts/contact-store.ts +0 -30
- 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 +126 -5
- package/src/daemon/bootstrap-turn-cleanup.ts +45 -0
- package/src/daemon/config-watcher.ts +4 -3
- package/src/daemon/conversation-agent-loop-handlers.ts +21 -3
- package/src/daemon/conversation-agent-loop.ts +32 -28
- package/src/daemon/conversation-lifecycle.ts +8 -1
- package/src/daemon/conversation-process.ts +16 -11
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +125 -4
- package/src/daemon/conversation-tool-setup.ts +16 -55
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/doordash-steps.ts +1 -1
- package/src/daemon/handlers/shared.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +293 -0
- package/src/daemon/host-bash-proxy.ts +84 -74
- package/src/daemon/host-browser-proxy.ts +67 -82
- package/src/daemon/host-cu-proxy.ts +81 -86
- package/src/daemon/host-file-proxy.ts +93 -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 +247 -129
- package/src/daemon/lifecycle.ts +115 -117
- package/src/daemon/message-protocol.ts +3 -8
- package/src/daemon/message-types/contacts.ts +23 -1
- package/src/daemon/message-types/conversations.ts +11 -8
- 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/schedules.ts +8 -3
- package/src/daemon/message-types/skills.ts +2 -2
- package/src/daemon/process-message.ts +18 -1
- 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/events/tool-audit-listener.ts +2 -1
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +15 -7
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +216 -0
- package/src/heartbeat/heartbeat-run-store.ts +236 -0
- package/src/heartbeat/heartbeat-service.ts +280 -49
- 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/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/public-ingress-urls.ts +32 -34
- package/src/ipc/__tests__/route-error-envelope.test.ts +80 -0
- package/src/ipc/assistant-server.ts +14 -1
- package/src/ipc/cli-client.ts +32 -1
- package/src/live-voice/live-voice-metrics.ts +10 -10
- 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/memory/__tests__/jobs-store-job-classes.test.ts +24 -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/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 +32 -7
- package/src/memory/context-search/sources/memory-v2.ts +17 -5
- package/src/memory/conversation-crud.ts +1 -1
- package/src/memory/conversation-key-store.ts +2 -15
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-backend.ts +9 -21
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +49 -4
- package/src/memory/graph/conversation-graph-memory.ts +1 -24
- package/src/memory/graph/graph-search.ts +8 -0
- package/src/memory/graph/retriever.ts +28 -0
- package/src/memory/graph/tools.ts +1 -1
- 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 +66 -22
- package/src/memory/jobs-worker.ts +112 -63
- package/src/memory/memory-v2-activation-log-store.ts +1 -1
- 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/index.ts +5 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/pkb/pkb-search.ts +7 -0
- package/src/memory/qdrant-client.ts +50 -20
- package/src/memory/schema/infrastructure.ts +15 -0
- package/src/memory/search/semantic.ts +7 -0
- package/src/memory/sparse-tokenize.ts +49 -0
- package/src/memory/v2/__tests__/activation.test.ts +77 -95
- package/src/memory/v2/__tests__/injection.test.ts +43 -21
- package/src/memory/v2/__tests__/sim.test.ts +166 -6
- package/src/memory/v2/__tests__/sparse-bm25.test.ts +292 -0
- package/src/memory/v2/__tests__/static-context.test.ts +0 -1
- package/src/memory/v2/activation.ts +69 -88
- package/src/memory/v2/consolidation-job.ts +3 -5
- package/src/memory/v2/constants.ts +7 -0
- package/src/memory/v2/injection.ts +86 -53
- package/src/memory/v2/prompts/consolidation.ts +312 -91
- package/src/memory/v2/qdrant.ts +99 -1
- package/src/memory/v2/sim.ts +126 -16
- package/src/memory/v2/skill-qdrant.ts +12 -3
- package/src/memory/v2/skill-store.ts +16 -1
- package/src/memory/v2/sparse-bm25.ts +245 -0
- package/src/memory/v2/static-context.ts +6 -5
- 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/guardian-question-mode.ts +5 -5
- package/src/oauth/connect-orchestrator.ts +4 -0
- package/src/oauth/credential-token-resolver.ts +1 -3
- package/src/oauth/manual-token-connection.ts +0 -4
- 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/prompts/bootstrap-cleanup.ts +27 -0
- package/src/prompts/system-prompt.ts +3 -18
- package/src/prompts/templates/SOUL.md +13 -1
- package/src/providers/speech-to-text/provider-catalog.ts +7 -8
- package/src/runtime/assistant-event-hub.ts +118 -96
- package/src/runtime/assistant-event.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +11 -56
- package/src/runtime/auth/middleware.ts +0 -96
- package/src/runtime/auth/route-policy.ts +19 -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/http-server.ts +3 -329
- package/src/runtime/http-types.ts +0 -5
- 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 +35 -9
- package/src/runtime/routes/__tests__/backup-routes.test.ts +22 -150
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +98 -0
- 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 +1 -0
- package/src/runtime/routes/contact-prompt-routes.ts +183 -0
- package/src/runtime/routes/conversation-query-routes.ts +36 -1
- package/src/runtime/routes/conversation-routes.ts +30 -13
- package/src/runtime/routes/document-pdf-renderer.ts +165 -0
- package/src/runtime/routes/documents-routes.ts +30 -0
- package/src/runtime/routes/errors.ts +19 -4
- package/src/runtime/routes/events-routes.ts +12 -6
- 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 +36 -6
- package/src/runtime/routes/host-browser-routes.ts +108 -13
- package/src/runtime/routes/host-cu-routes.ts +44 -14
- package/src/runtime/routes/host-file-routes.ts +33 -10
- package/src/runtime/routes/host-transfer-routes.ts +64 -24
- 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 +15 -43
- package/src/runtime/routes/inbound-message-handler.ts +1 -9
- 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/transcribe-audio.test.ts +0 -20
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +5 -13
- package/src/runtime/routes/index.ts +8 -0
- package/src/runtime/routes/mcp-auth-routes.ts +132 -0
- package/src/runtime/routes/memory-item-routes.ts +10 -12
- package/src/runtime/routes/memory-v2-routes.ts +441 -1
- package/src/runtime/routes/migration-routes.ts +96 -0
- package/src/runtime/routes/schedule-routes.ts +7 -0
- 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/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 +63 -38
- package/src/security/oauth-callback-registry.ts +8 -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 +5 -5
- 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/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/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.ts +26 -0
- package/src/tools/host-filesystem/read.ts +26 -0
- package/src/tools/host-filesystem/transfer.ts +31 -1
- package/src/tools/host-filesystem/write.ts +26 -0
- package/src/tools/host-terminal/host-shell.ts +58 -0
- 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/tool-approval-handler.ts +1 -5
- package/src/tools/types.ts +4 -0
- package/src/usage/pricing.ts +1 -1
- package/src/workspace/hatched-date.ts +86 -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/AGENTS.md +1 -1
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -10
- package/src/workspace/migrations/utils.ts +21 -0
- 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/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/mcp-reload.ts +0 -18
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { getConfig } from "../config/loader.js";
|
|
5
|
+
import type { HeartbeatConfig } from "../config/schemas/heartbeat.js";
|
|
5
6
|
import type { HeartbeatAlert } from "../daemon/message-protocol.js";
|
|
6
7
|
import { processMessage } from "../daemon/process-message.js";
|
|
7
8
|
import { emitFeedEvent } from "../home/emit-feed-event.js";
|
|
@@ -13,10 +14,20 @@ import {
|
|
|
13
14
|
resolveGuardianPersona,
|
|
14
15
|
} from "../prompts/persona-resolver.js";
|
|
15
16
|
import { isTemplateContent } from "../prompts/system-prompt.js";
|
|
17
|
+
import { computeNextRunAt } from "../schedule/recurrence-engine.js";
|
|
16
18
|
import { readTextFileSync } from "../util/fs.js";
|
|
17
19
|
import { getLogger } from "../util/logger.js";
|
|
18
20
|
import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
|
|
19
21
|
import { stripCommentLines } from "../util/strip-comment-lines.js";
|
|
22
|
+
import {
|
|
23
|
+
completeHeartbeatRun,
|
|
24
|
+
insertPendingHeartbeatRun,
|
|
25
|
+
markStaleRunningAsError,
|
|
26
|
+
markStaleRunsAsMissed,
|
|
27
|
+
skipHeartbeatRun,
|
|
28
|
+
startHeartbeatRun,
|
|
29
|
+
supersedePendingRun,
|
|
30
|
+
} from "./heartbeat-run-store.js";
|
|
20
31
|
|
|
21
32
|
const log = getLogger("heartbeat-check");
|
|
22
33
|
|
|
@@ -100,10 +111,20 @@ export class HeartbeatService {
|
|
|
100
111
|
}
|
|
101
112
|
|
|
102
113
|
private readonly deps: HeartbeatDeps;
|
|
103
|
-
private timer:
|
|
114
|
+
private timer:
|
|
115
|
+
| ReturnType<typeof setInterval>
|
|
116
|
+
| ReturnType<typeof setTimeout>
|
|
117
|
+
| null = null;
|
|
104
118
|
private activeRun: Promise<void> | null = null;
|
|
105
119
|
private _lastRunAt: number | null = null;
|
|
106
120
|
private _nextRunAt: number | null = null;
|
|
121
|
+
private cronMode = false;
|
|
122
|
+
private stopped = false;
|
|
123
|
+
private configEpoch = 0;
|
|
124
|
+
private _pendingRunId: string | null = null;
|
|
125
|
+
private _startupMissedCount = 0;
|
|
126
|
+
private _startupCrashedCount = 0;
|
|
127
|
+
private _hasRunStartupRecovery = false;
|
|
107
128
|
|
|
108
129
|
constructor(deps: HeartbeatDeps) {
|
|
109
130
|
this.deps = deps;
|
|
@@ -121,6 +142,7 @@ export class HeartbeatService {
|
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
start(): void {
|
|
145
|
+
this.stopped = false;
|
|
124
146
|
const config = getConfig().heartbeat;
|
|
125
147
|
if (!config.enabled) {
|
|
126
148
|
log.info("Heartbeat disabled by config");
|
|
@@ -129,7 +151,57 @@ export class HeartbeatService {
|
|
|
129
151
|
}
|
|
130
152
|
if (this.timer) return;
|
|
131
153
|
|
|
132
|
-
|
|
154
|
+
if (!this._hasRunStartupRecovery) {
|
|
155
|
+
this._hasRunStartupRecovery = true;
|
|
156
|
+
try {
|
|
157
|
+
this._startupMissedCount = markStaleRunsAsMissed();
|
|
158
|
+
this._startupCrashedCount = markStaleRunningAsError();
|
|
159
|
+
} catch (err) {
|
|
160
|
+
log.error({ err }, "Failed to recover stale heartbeat runs on startup");
|
|
161
|
+
}
|
|
162
|
+
if (this._startupMissedCount > 0 || this._startupCrashedCount > 0) {
|
|
163
|
+
log.info(
|
|
164
|
+
{
|
|
165
|
+
missedCount: this._startupMissedCount,
|
|
166
|
+
crashedCount: this._startupCrashedCount,
|
|
167
|
+
},
|
|
168
|
+
"Recovered stale heartbeat runs on startup",
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const total = this._startupMissedCount + this._startupCrashedCount;
|
|
172
|
+
const today = new Date().toISOString().split("T")[0];
|
|
173
|
+
void emitFeedEvent({
|
|
174
|
+
source: "assistant",
|
|
175
|
+
title: "Heartbeat Runs Missed",
|
|
176
|
+
summary: `${total} heartbeat run${total > 1 ? "s were" : " was"} missed while the assistant was offline.`,
|
|
177
|
+
dedupKey: `heartbeat:missed:${today}`,
|
|
178
|
+
priority: 55,
|
|
179
|
+
urgency: "high",
|
|
180
|
+
}).catch((err) => {
|
|
181
|
+
log.warn({ err }, "Failed to emit missed heartbeat feed event");
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (config.cronExpression != null) {
|
|
187
|
+
this.cronMode = true;
|
|
188
|
+
this.scheduleNextCronRun(config);
|
|
189
|
+
} else {
|
|
190
|
+
this.startIntervalMode(config);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private startIntervalMode(config: HeartbeatConfig): void {
|
|
195
|
+
this.cronMode = false;
|
|
196
|
+
if (this.timer) {
|
|
197
|
+
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
198
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
199
|
+
this.timer = null;
|
|
200
|
+
}
|
|
201
|
+
log.info(
|
|
202
|
+
{ intervalMs: config.intervalMs },
|
|
203
|
+
"Heartbeat service started (interval mode)",
|
|
204
|
+
);
|
|
133
205
|
this.scheduleNextRun(config.intervalMs);
|
|
134
206
|
this.timer = setInterval(() => {
|
|
135
207
|
this.runOnce().catch((err) => {
|
|
@@ -138,13 +210,69 @@ export class HeartbeatService {
|
|
|
138
210
|
}, config.intervalMs);
|
|
139
211
|
}
|
|
140
212
|
|
|
213
|
+
private scheduleNextCronRun(config: HeartbeatConfig): void {
|
|
214
|
+
if (this.stopped) return;
|
|
215
|
+
try {
|
|
216
|
+
const nextRunAt = computeNextRunAt({
|
|
217
|
+
syntax: "cron",
|
|
218
|
+
expression: config.cronExpression!,
|
|
219
|
+
timezone: config.timezone,
|
|
220
|
+
});
|
|
221
|
+
this._nextRunAt = nextRunAt;
|
|
222
|
+
if (this.timer) {
|
|
223
|
+
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
224
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
225
|
+
this.timer = null;
|
|
226
|
+
}
|
|
227
|
+
const MAX_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
228
|
+
const delayMs = Math.max(0, nextRunAt - Date.now());
|
|
229
|
+
const epoch = this.configEpoch;
|
|
230
|
+
if (delayMs > MAX_TIMEOUT_MS) {
|
|
231
|
+
// Re-evaluate after 24h — the actual cron time is still far away
|
|
232
|
+
this.timer = setTimeout(() => {
|
|
233
|
+
if (this.configEpoch === epoch) {
|
|
234
|
+
this.scheduleNextCronRun(getConfig().heartbeat);
|
|
235
|
+
}
|
|
236
|
+
}, MAX_TIMEOUT_MS);
|
|
237
|
+
} else {
|
|
238
|
+
this.timer = setTimeout(() => {
|
|
239
|
+
this.runOnce()
|
|
240
|
+
.catch((err) => log.error({ err }, "Cron heartbeat failed"))
|
|
241
|
+
.finally(() => {
|
|
242
|
+
if (this.configEpoch === epoch) {
|
|
243
|
+
this.scheduleNextCronRun(getConfig().heartbeat);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}, delayMs);
|
|
247
|
+
}
|
|
248
|
+
(this.timer as ReturnType<typeof setTimeout>).unref();
|
|
249
|
+
log.info(
|
|
250
|
+
{ nextRunAt: new Date(nextRunAt).toISOString(), delayMs },
|
|
251
|
+
"Heartbeat cron run scheduled",
|
|
252
|
+
);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
log.warn(
|
|
255
|
+
{ err },
|
|
256
|
+
"Failed to compute next cron run, falling back to interval mode",
|
|
257
|
+
);
|
|
258
|
+
this.startIntervalMode(config);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
141
262
|
/** Restart the timer with the latest config (e.g. after settings change). */
|
|
142
263
|
reconfigure(): void {
|
|
264
|
+
this.configEpoch++;
|
|
265
|
+
if (this._pendingRunId) {
|
|
266
|
+
supersedePendingRun(this._pendingRunId);
|
|
267
|
+
this._pendingRunId = null;
|
|
268
|
+
}
|
|
143
269
|
if (this.timer) {
|
|
144
|
-
|
|
270
|
+
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
271
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
145
272
|
this.timer = null;
|
|
146
273
|
}
|
|
147
274
|
this._nextRunAt = null;
|
|
275
|
+
this.cronMode = false;
|
|
148
276
|
this.start();
|
|
149
277
|
}
|
|
150
278
|
|
|
@@ -155,8 +283,15 @@ export class HeartbeatService {
|
|
|
155
283
|
*/
|
|
156
284
|
resetTimer(): void {
|
|
157
285
|
if (!this.timer) return;
|
|
286
|
+
if (this.cronMode) {
|
|
287
|
+
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
288
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
289
|
+
this.timer = null;
|
|
290
|
+
this.scheduleNextCronRun(getConfig().heartbeat);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
158
293
|
const config = getConfig().heartbeat;
|
|
159
|
-
clearInterval(this.timer);
|
|
294
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
160
295
|
this.scheduleNextRun(config.intervalMs);
|
|
161
296
|
this.timer = setInterval(() => {
|
|
162
297
|
this.runOnce().catch((err) => {
|
|
@@ -166,10 +301,16 @@ export class HeartbeatService {
|
|
|
166
301
|
}
|
|
167
302
|
|
|
168
303
|
async stop(): Promise<void> {
|
|
304
|
+
this.stopped = true;
|
|
169
305
|
if (this.timer) {
|
|
170
|
-
|
|
306
|
+
clearTimeout(this.timer as ReturnType<typeof setTimeout>);
|
|
307
|
+
clearInterval(this.timer as ReturnType<typeof setInterval>);
|
|
171
308
|
this.timer = null;
|
|
172
309
|
}
|
|
310
|
+
if (this._pendingRunId) {
|
|
311
|
+
supersedePendingRun(this._pendingRunId);
|
|
312
|
+
this._pendingRunId = null;
|
|
313
|
+
}
|
|
173
314
|
this._nextRunAt = null;
|
|
174
315
|
if (this.activeRun) {
|
|
175
316
|
let timerId: ReturnType<typeof setTimeout>;
|
|
@@ -186,7 +327,22 @@ export class HeartbeatService {
|
|
|
186
327
|
* When `force` is true (e.g. manual "Run Now"), skip enabled & active-hours guards. */
|
|
187
328
|
async runOnce({ force = false }: { force?: boolean } = {}): Promise<boolean> {
|
|
188
329
|
const config = getConfig().heartbeat;
|
|
189
|
-
|
|
330
|
+
|
|
331
|
+
let runId: string | null;
|
|
332
|
+
let scheduledFor: number;
|
|
333
|
+
if (force) {
|
|
334
|
+
scheduledFor = Date.now();
|
|
335
|
+
runId = insertPendingHeartbeatRun(scheduledFor);
|
|
336
|
+
} else {
|
|
337
|
+
runId = this._pendingRunId;
|
|
338
|
+
scheduledFor = this._nextRunAt ?? Date.now();
|
|
339
|
+
this._pendingRunId = null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!force && !config.enabled) {
|
|
343
|
+
if (runId) skipHeartbeatRun(runId, "disabled");
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
190
346
|
|
|
191
347
|
// Active hours guard — only applied when both bounds are set.
|
|
192
348
|
// The schema rejects configs where only one bound is provided.
|
|
@@ -195,7 +351,17 @@ export class HeartbeatService {
|
|
|
195
351
|
config.activeHoursStart != null &&
|
|
196
352
|
config.activeHoursEnd != null
|
|
197
353
|
) {
|
|
198
|
-
|
|
354
|
+
let hour: number;
|
|
355
|
+
if (this.cronMode && config.timezone) {
|
|
356
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
357
|
+
timeZone: config.timezone,
|
|
358
|
+
hourCycle: "h23",
|
|
359
|
+
hour: "numeric",
|
|
360
|
+
}).formatToParts(new Date());
|
|
361
|
+
hour = Number(parts.find((p) => p.type === "hour")!.value);
|
|
362
|
+
} else {
|
|
363
|
+
hour = this.deps.getCurrentHour?.() ?? new Date().getHours();
|
|
364
|
+
}
|
|
199
365
|
if (
|
|
200
366
|
!isWithinActiveHours(
|
|
201
367
|
hour,
|
|
@@ -211,7 +377,10 @@ export class HeartbeatService {
|
|
|
211
377
|
},
|
|
212
378
|
"Outside active hours, skipping",
|
|
213
379
|
);
|
|
214
|
-
|
|
380
|
+
if (runId) skipHeartbeatRun(runId, "outside_active_hours");
|
|
381
|
+
if (!this.cronMode) {
|
|
382
|
+
this.scheduleNextRun(config.intervalMs);
|
|
383
|
+
}
|
|
215
384
|
return false;
|
|
216
385
|
}
|
|
217
386
|
}
|
|
@@ -219,10 +388,14 @@ export class HeartbeatService {
|
|
|
219
388
|
// Overlap prevention
|
|
220
389
|
if (this.activeRun) {
|
|
221
390
|
log.debug("Previous heartbeat run still active, skipping");
|
|
391
|
+
if (runId) skipHeartbeatRun(runId, "overlap");
|
|
222
392
|
return false;
|
|
223
393
|
}
|
|
224
394
|
|
|
225
|
-
|
|
395
|
+
if (!runId) {
|
|
396
|
+
runId = insertPendingHeartbeatRun(scheduledFor);
|
|
397
|
+
}
|
|
398
|
+
const run = this.executeRun(runId, scheduledFor);
|
|
226
399
|
this.activeRun = run;
|
|
227
400
|
// Clear activeRun once executeRun finishes. On timeout, runOnce releases
|
|
228
401
|
// activeRun separately (see catch block below) so future runs aren't
|
|
@@ -252,16 +425,39 @@ export class HeartbeatService {
|
|
|
252
425
|
// Release activeRun so the overlap guard doesn't permanently block
|
|
253
426
|
// future heartbeat runs when executeRun hangs past the timeout.
|
|
254
427
|
this.activeRun = null;
|
|
428
|
+
const transitioned = runId
|
|
429
|
+
? completeHeartbeatRun(runId, {
|
|
430
|
+
status: "timeout",
|
|
431
|
+
error: "Heartbeat execution exceeded the 30-minute timeout",
|
|
432
|
+
})
|
|
433
|
+
: false;
|
|
434
|
+
if (transitioned) {
|
|
435
|
+
const today = new Date().toISOString().split("T")[0];
|
|
436
|
+
void emitFeedEvent({
|
|
437
|
+
source: "assistant",
|
|
438
|
+
title: "Heartbeat Timed Out",
|
|
439
|
+
summary: "Heartbeat execution exceeded the 30-minute timeout.",
|
|
440
|
+
dedupKey: `heartbeat:timeout:${today}`,
|
|
441
|
+
priority: 55,
|
|
442
|
+
urgency: "high",
|
|
443
|
+
}).catch(() => {});
|
|
444
|
+
}
|
|
255
445
|
} finally {
|
|
256
446
|
clearTimeout(timerId);
|
|
257
447
|
this._lastRunAt = Date.now();
|
|
258
|
-
this.
|
|
448
|
+
if (!this.cronMode) {
|
|
449
|
+
this.scheduleNextRun(getConfig().heartbeat.intervalMs);
|
|
450
|
+
}
|
|
259
451
|
}
|
|
260
452
|
return true;
|
|
261
453
|
}
|
|
262
454
|
|
|
263
455
|
private scheduleNextRun(intervalMs: number): void {
|
|
456
|
+
if (this._pendingRunId) {
|
|
457
|
+
supersedePendingRun(this._pendingRunId);
|
|
458
|
+
}
|
|
264
459
|
this._nextRunAt = Date.now() + intervalMs;
|
|
460
|
+
this._pendingRunId = insertPendingHeartbeatRun(this._nextRunAt);
|
|
265
461
|
}
|
|
266
462
|
|
|
267
463
|
/**
|
|
@@ -383,14 +579,20 @@ export class HeartbeatService {
|
|
|
383
579
|
}
|
|
384
580
|
}
|
|
385
581
|
|
|
386
|
-
private async executeRun(): Promise<void> {
|
|
582
|
+
private async executeRun(runId: string, scheduledFor: number): Promise<void> {
|
|
387
583
|
log.info("Running heartbeat");
|
|
388
584
|
|
|
585
|
+
startHeartbeatRun(runId);
|
|
586
|
+
|
|
587
|
+
const latenessMs = Date.now() - scheduledFor;
|
|
588
|
+
const LATE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
589
|
+
|
|
389
590
|
// Credential health check — surface broken credentials proactively
|
|
390
591
|
// before the LLM heartbeat prompt runs. Returns unhealthy provider
|
|
391
592
|
// names so the prompt can instruct the LLM to skip those providers.
|
|
392
593
|
const unhealthyProviders = await this.runCredentialHealthCheck();
|
|
393
594
|
|
|
595
|
+
let conversationId: string | undefined;
|
|
394
596
|
try {
|
|
395
597
|
const checklist = this.readChecklist();
|
|
396
598
|
const { prompt, includedReengagement } = this.buildPrompt(
|
|
@@ -405,6 +607,7 @@ export class HeartbeatService {
|
|
|
405
607
|
origin: "heartbeat",
|
|
406
608
|
systemHint: "Heartbeat",
|
|
407
609
|
});
|
|
610
|
+
conversationId = conversation.id;
|
|
408
611
|
|
|
409
612
|
this.deps.onConversationCreated?.({
|
|
410
613
|
conversationId: conversation.id,
|
|
@@ -425,50 +628,78 @@ export class HeartbeatService {
|
|
|
425
628
|
|
|
426
629
|
log.info({ conversationId: conversation.id }, "Heartbeat completed");
|
|
427
630
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
631
|
+
const transitioned = completeHeartbeatRun(runId, {
|
|
632
|
+
status: "ok",
|
|
633
|
+
conversationId,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (transitioned) {
|
|
637
|
+
let title = "Heartbeat";
|
|
638
|
+
try {
|
|
639
|
+
const row = getConversation(conversation.id);
|
|
640
|
+
if (row?.title && row.title !== GENERATING_TITLE) {
|
|
641
|
+
title = row.title;
|
|
642
|
+
}
|
|
643
|
+
} catch {
|
|
644
|
+
// Best-effort; fall back to generic title.
|
|
433
645
|
}
|
|
434
|
-
} catch {
|
|
435
|
-
// Best-effort; fall back to generic title.
|
|
436
|
-
}
|
|
437
646
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
647
|
+
const today = new Date().toISOString().split("T")[0];
|
|
648
|
+
void emitFeedEvent({
|
|
649
|
+
source: "assistant",
|
|
650
|
+
title,
|
|
651
|
+
summary: "Periodic check completed. Tap to see details.",
|
|
652
|
+
dedupKey: `heartbeat:ok:${today}`,
|
|
653
|
+
priority: 30,
|
|
654
|
+
}).catch((err) => {
|
|
655
|
+
log.warn(
|
|
656
|
+
{ err, conversationId: conversation.id },
|
|
657
|
+
"Failed to emit heartbeat feed event",
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
if (latenessMs > LATE_THRESHOLD_MS) {
|
|
662
|
+
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
663
|
+
void emitFeedEvent({
|
|
664
|
+
source: "assistant",
|
|
665
|
+
title: "Heartbeat Ran Late",
|
|
666
|
+
summary: `Heartbeat ran ${lateMinutes} minutes late (scheduled for ${new Date(scheduledFor).toLocaleTimeString()}).`,
|
|
667
|
+
dedupKey: `heartbeat:late:${today}`,
|
|
668
|
+
priority: 45,
|
|
669
|
+
urgency: "medium",
|
|
670
|
+
}).catch(() => {});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
451
673
|
} catch (err) {
|
|
452
674
|
log.error({ err }, "Heartbeat failed");
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
675
|
+
|
|
676
|
+
const transitioned = completeHeartbeatRun(runId, {
|
|
677
|
+
status: "error",
|
|
678
|
+
conversationId,
|
|
679
|
+
error: err instanceof Error ? err.message : String(err),
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
if (transitioned) {
|
|
683
|
+
try {
|
|
684
|
+
this.deps.alerter({
|
|
685
|
+
type: "heartbeat_alert",
|
|
686
|
+
title: "Heartbeat Failed",
|
|
687
|
+
body: err instanceof Error ? err.message : String(err),
|
|
688
|
+
});
|
|
689
|
+
} catch (alertErr) {
|
|
690
|
+
log.error({ alertErr }, "Failed to broadcast heartbeat alert");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const today = new Date().toISOString().split("T")[0];
|
|
694
|
+
void emitFeedEvent({
|
|
695
|
+
source: "assistant",
|
|
456
696
|
title: "Heartbeat Failed",
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
697
|
+
summary: `Heartbeat check failed: ${(err instanceof Error ? err.message : String(err)).slice(0, 200)}`,
|
|
698
|
+
dedupKey: `heartbeat:fail:${today}`,
|
|
699
|
+
priority: 55,
|
|
700
|
+
urgency: "high",
|
|
701
|
+
}).catch(() => {});
|
|
461
702
|
}
|
|
462
|
-
|
|
463
|
-
const today = new Date().toISOString().split("T")[0];
|
|
464
|
-
void emitFeedEvent({
|
|
465
|
-
source: "assistant",
|
|
466
|
-
title: "Heartbeat",
|
|
467
|
-
summary: "Heartbeat check failed. Check logs for details.",
|
|
468
|
-
dedupKey: `heartbeat:fail:${today}`,
|
|
469
|
-
priority: 55,
|
|
470
|
-
urgency: "medium",
|
|
471
|
-
}).catch(() => {});
|
|
472
703
|
}
|
|
473
704
|
}
|
|
474
705
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
// ─── assistantEventHub mock ────────────────────────────────────────────
|
|
7
|
+
const publishSpy = mock<(event: unknown) => Promise<void>>(async () => {});
|
|
8
|
+
|
|
9
|
+
mock.module("../../runtime/assistant-event-hub.js", () => ({
|
|
10
|
+
assistantEventHub: {
|
|
11
|
+
publish: publishSpy,
|
|
12
|
+
subscribe: () => () => {},
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const { emitPostConnectNudge } = await import("../post-connect-feed.js");
|
|
17
|
+
const { readHomeFeed } = await import("../feed-writer.js");
|
|
18
|
+
|
|
19
|
+
// ─── tmpdir workspace lifecycle ────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
let workspaceDir: string;
|
|
22
|
+
let origWorkspaceDir: string | undefined;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pcf-"));
|
|
26
|
+
origWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
27
|
+
process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
|
|
28
|
+
publishSpy.mockClear();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (origWorkspaceDir === undefined) {
|
|
33
|
+
delete process.env.VELLUM_WORKSPACE_DIR;
|
|
34
|
+
} else {
|
|
35
|
+
process.env.VELLUM_WORKSPACE_DIR = origWorkspaceDir;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
rmSync(workspaceDir, { recursive: true, force: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// best-effort
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Tests ─────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe("emitPostConnectNudge", () => {
|
|
47
|
+
test("emits a nudge feed item for google", async () => {
|
|
48
|
+
await emitPostConnectNudge("google");
|
|
49
|
+
|
|
50
|
+
const feed = readHomeFeed();
|
|
51
|
+
expect(feed.items).toHaveLength(1);
|
|
52
|
+
|
|
53
|
+
const item = feed.items[0]!;
|
|
54
|
+
expect(item.id).toBe("connect-nudge:google");
|
|
55
|
+
expect(item.type).toBe("nudge");
|
|
56
|
+
expect(item.source).toBe("gmail");
|
|
57
|
+
expect(item.title).toContain("Gmail connected");
|
|
58
|
+
expect(item.actions).toHaveLength(2);
|
|
59
|
+
expect(item.actions![0]!.label).toBe("Triage my inbox");
|
|
60
|
+
expect(item.actions![1]!.label).toBe("Set up daily digest");
|
|
61
|
+
expect(item.expiresAt).toBeDefined();
|
|
62
|
+
expect(item.author).toBe("platform");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("no-ops for non-email services", async () => {
|
|
66
|
+
await emitPostConnectNudge("slack");
|
|
67
|
+
await emitPostConnectNudge("notion");
|
|
68
|
+
await emitPostConnectNudge("linear");
|
|
69
|
+
|
|
70
|
+
const feed = readHomeFeed();
|
|
71
|
+
expect(feed.items).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("reconnecting appends a second nudge (same-author nudges don't deduplicate)", async () => {
|
|
75
|
+
await emitPostConnectNudge("google");
|
|
76
|
+
await emitPostConnectNudge("google");
|
|
77
|
+
|
|
78
|
+
const feed = readHomeFeed();
|
|
79
|
+
// Same-author (platform) same-source nudges both persist —
|
|
80
|
+
// the feed writer's author-resolution only handles cross-author
|
|
81
|
+
// replacement. In practice, reconnects are rare and the 7-day
|
|
82
|
+
// expiry prevents buildup.
|
|
83
|
+
expect(feed.items).toHaveLength(2);
|
|
84
|
+
expect(feed.items.every((i) => i.id === "connect-nudge:google")).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("nudge expires after 7 days", async () => {
|
|
88
|
+
await emitPostConnectNudge("google");
|
|
89
|
+
|
|
90
|
+
const feed = readHomeFeed();
|
|
91
|
+
const item = feed.items[0]!;
|
|
92
|
+
const created = new Date(item.createdAt).getTime();
|
|
93
|
+
const expires = new Date(item.expiresAt!).getTime();
|
|
94
|
+
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
|
95
|
+
|
|
96
|
+
// Allow 1 second tolerance for test execution time
|
|
97
|
+
expect(Math.abs(expires - created - sevenDaysMs)).toBeLessThan(1000);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -432,6 +432,12 @@ describe("relationship-state-writer", () => {
|
|
|
432
432
|
// Also sanity: it must be a real, recent date (not the epoch
|
|
433
433
|
// sentinel we emit when stat fails).
|
|
434
434
|
expect(Date.parse(first.hatchedDate)).toBeGreaterThan(0);
|
|
435
|
+
const sidecarPath = join(workspaceDir, "data", "hatched.json");
|
|
436
|
+
expect(existsSync(sidecarPath)).toBe(true);
|
|
437
|
+
const sidecar = JSON.parse(readFileSync(sidecarPath, "utf-8")) as {
|
|
438
|
+
hatchedAt: string;
|
|
439
|
+
};
|
|
440
|
+
expect(sidecar.hatchedAt).toBe(first.hatchedDate);
|
|
435
441
|
});
|
|
436
442
|
|
|
437
443
|
test("honors an explicit Hatched bullet in IDENTITY.md over file birthtime", async () => {
|
|
@@ -488,9 +494,10 @@ describe("relationship-state-writer", () => {
|
|
|
488
494
|
expect(state.hatchedDate).toBe("2025-01-15T00:00:00.000Z");
|
|
489
495
|
});
|
|
490
496
|
|
|
491
|
-
test("
|
|
492
|
-
// Seed
|
|
493
|
-
// explicit Hatched bullet —
|
|
497
|
+
test("sidecar takes precedence over IDENTITY.md metadata", async () => {
|
|
498
|
+
// Seed an existing sidecar and then an IDENTITY.md without an
|
|
499
|
+
// explicit Hatched bullet — the persisted sidecar wins so the
|
|
500
|
+
// date remains stable across later identity edits.
|
|
494
501
|
mkdirSync(join(workspaceDir, "data"), { recursive: true });
|
|
495
502
|
writeFileSync(
|
|
496
503
|
join(workspaceDir, "data", "hatched.json"),
|
|
@@ -500,12 +507,7 @@ describe("relationship-state-writer", () => {
|
|
|
500
507
|
writeFile("IDENTITY.md", "- **Name:** Sage\n- **Role:** Assistant\n");
|
|
501
508
|
|
|
502
509
|
const state = (await computeRelationshipState()) as RelationshipStateLike;
|
|
503
|
-
|
|
504
|
-
// written, so birthtime will be a much more recent date.
|
|
505
|
-
expect(state.hatchedDate).not.toBe("2020-06-01T00:00:00.000Z");
|
|
506
|
-
expect(Date.parse(state.hatchedDate)).toBeGreaterThan(
|
|
507
|
-
Date.parse("2020-06-01T00:00:00.000Z"),
|
|
508
|
-
);
|
|
510
|
+
expect(state.hatchedDate).toBe("2020-06-01T00:00:00.000Z");
|
|
509
511
|
});
|
|
510
512
|
});
|
|
511
513
|
|