@vellumai/assistant 0.7.2 → 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 +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- 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-error.test.ts +150 -3
- 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-runtime-assembly.test.ts +65 -0
- 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-app-control.test.ts +15 -4
- 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__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- 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__/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__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- 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__/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 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -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-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- 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 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- 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/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- 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/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- 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 +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- 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-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/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- 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 +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- 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 +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- 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-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- 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 +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- 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 +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- 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/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- 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/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 +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- 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 +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- 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/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- 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/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- 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/heartbeat-service.ts +19 -0
- 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/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -4,6 +4,11 @@ import { join } from "node:path";
|
|
|
4
4
|
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
5
5
|
import { getConfig } from "../config/loader.js";
|
|
6
6
|
import type { LLMCallSite } from "../config/schemas/llm.js";
|
|
7
|
+
import {
|
|
8
|
+
checkDiskPressureBackgroundGate,
|
|
9
|
+
diskPressureBackgroundSkipLogFields,
|
|
10
|
+
shouldLogDiskPressureBackgroundSkip,
|
|
11
|
+
} from "../daemon/disk-pressure-background-gate.js";
|
|
7
12
|
import { processMessage } from "../daemon/process-message.js";
|
|
8
13
|
import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
|
|
9
14
|
import { getLogger } from "../util/logger.js";
|
|
@@ -110,11 +115,8 @@ export class FilingService {
|
|
|
110
115
|
|
|
111
116
|
start(): void {
|
|
112
117
|
const fullConfig = getConfig();
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
fullConfig.memory.v2.enabled
|
|
116
|
-
) {
|
|
117
|
-
log.info("Filing service disabled — memory v2 is active");
|
|
118
|
+
if (isAssistantFeatureFlagEnabled("memory-v2-enabled", fullConfig)) {
|
|
119
|
+
log.info("Filing service disabled — memory v2 flag is set");
|
|
118
120
|
this._nextRunAt = null;
|
|
119
121
|
this._nextCompactionAt = null;
|
|
120
122
|
return;
|
|
@@ -195,6 +197,10 @@ export class FilingService {
|
|
|
195
197
|
const config = getConfig().filing;
|
|
196
198
|
if (!force && !config.enabled) return false;
|
|
197
199
|
|
|
200
|
+
if (!force && this.shouldSkipForDiskPressure("filing")) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
198
204
|
if (
|
|
199
205
|
!force &&
|
|
200
206
|
!this.isWithinActiveHoursNow(
|
|
@@ -245,6 +251,10 @@ export class FilingService {
|
|
|
245
251
|
const config = getConfig().filing;
|
|
246
252
|
if (!force && !config.compactionEnabled) return false;
|
|
247
253
|
|
|
254
|
+
if (!force && this.shouldSkipForDiskPressure("compaction")) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
248
258
|
if (
|
|
249
259
|
!force &&
|
|
250
260
|
!this.isWithinActiveHoursNow(
|
|
@@ -299,6 +309,21 @@ export class FilingService {
|
|
|
299
309
|
this._nextCompactionAt = Date.now() + intervalMs;
|
|
300
310
|
}
|
|
301
311
|
|
|
312
|
+
private shouldSkipForDiskPressure(source: "filing" | "compaction"): boolean {
|
|
313
|
+
const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
|
|
314
|
+
if (diskPressureGate.action === "allow") return false;
|
|
315
|
+
if (shouldLogDiskPressureBackgroundSkip(`filing-service:${source}`)) {
|
|
316
|
+
log.warn(
|
|
317
|
+
{
|
|
318
|
+
source,
|
|
319
|
+
...diskPressureBackgroundSkipLogFields(diskPressureGate),
|
|
320
|
+
},
|
|
321
|
+
"Filing service skipped during disk pressure cleanup mode",
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
302
327
|
private hasBufferContent(): boolean {
|
|
303
328
|
const bufferPath = join(getWorkspaceDir(), "pkb", "buffer.md");
|
|
304
329
|
if (!existsSync(bufferPath)) return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -15,6 +15,7 @@ mock.module("../heartbeat-run-store.js", () => ({
|
|
|
15
15
|
markStaleRunsAsMissed: () => 0,
|
|
16
16
|
markStaleRunningAsError: () => 0,
|
|
17
17
|
listHeartbeatRuns: () => [],
|
|
18
|
+
countCompletedHeartbeatRuns: () => 10,
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
// Stub the in-process SSE hub so the writer's publish path is a
|
|
@@ -146,6 +147,7 @@ interface OnDiskItem {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function readFeedItems(): OnDiskItem[] {
|
|
150
|
+
if (!existsSync(getHomeFeedPath())) return [];
|
|
149
151
|
const raw = JSON.parse(readFileSync(getHomeFeedPath(), "utf-8"));
|
|
150
152
|
return raw.items as OnDiskItem[];
|
|
151
153
|
}
|
|
@@ -174,7 +176,7 @@ afterEach(() => {
|
|
|
174
176
|
});
|
|
175
177
|
|
|
176
178
|
describe("heartbeat feed events", () => {
|
|
177
|
-
test("successful heartbeat
|
|
179
|
+
test("successful heartbeat without an alert does not emit a feed event", async () => {
|
|
178
180
|
_testProcessMessage = async () => ({ messageId: "msg-1" });
|
|
179
181
|
const service = new HeartbeatService({
|
|
180
182
|
alerter: () => {},
|
|
@@ -182,18 +184,12 @@ describe("heartbeat feed events", () => {
|
|
|
182
184
|
|
|
183
185
|
await service.runOnce({ force: true });
|
|
184
186
|
|
|
185
|
-
// Give
|
|
187
|
+
// Give any fire-and-forget surfacing work time to flush.
|
|
186
188
|
await new Promise((r) => setTimeout(r, 100));
|
|
187
189
|
|
|
188
190
|
const items = readFeedItems();
|
|
189
191
|
const heartbeatItem = items.find((i) => i.title === "Heartbeat");
|
|
190
|
-
expect(heartbeatItem).
|
|
191
|
-
expect(heartbeatItem!.summary).toBe(
|
|
192
|
-
"Periodic check completed. Tap to see details.",
|
|
193
|
-
);
|
|
194
|
-
expect(heartbeatItem!.priority).toBe(30);
|
|
195
|
-
expect(heartbeatItem!.urgency).toBeUndefined();
|
|
196
|
-
expect(heartbeatItem!.source).toBe("assistant");
|
|
192
|
+
expect(heartbeatItem).toBeUndefined();
|
|
197
193
|
});
|
|
198
194
|
|
|
199
195
|
test("failed heartbeat emits feed event with priority 55 and urgency medium", async () => {
|
|
@@ -218,13 +214,13 @@ describe("heartbeat feed events", () => {
|
|
|
218
214
|
expect(heartbeatItem!.source).toBe("assistant");
|
|
219
215
|
});
|
|
220
216
|
|
|
221
|
-
test("
|
|
217
|
+
test("repeated successful heartbeats without alerts stay silent", async () => {
|
|
222
218
|
_testProcessMessage = async () => ({ messageId: "msg-1" });
|
|
223
219
|
const service = new HeartbeatService({
|
|
224
220
|
alerter: () => {},
|
|
225
221
|
});
|
|
226
222
|
|
|
227
|
-
// Run twice —
|
|
223
|
+
// Run twice — neither should create a generic success item.
|
|
228
224
|
await service.runOnce({ force: true });
|
|
229
225
|
await new Promise((r) => setTimeout(r, 100));
|
|
230
226
|
await service.runOnce({ force: true });
|
|
@@ -232,9 +228,6 @@ describe("heartbeat feed events", () => {
|
|
|
232
228
|
|
|
233
229
|
const items = readFeedItems();
|
|
234
230
|
const heartbeatItems = items.filter((i) => i.title === "Heartbeat");
|
|
235
|
-
expect(heartbeatItems).toHaveLength(
|
|
236
|
-
|
|
237
|
-
const today = new Date().toISOString().split("T")[0];
|
|
238
|
-
expect(heartbeatItems[0]!.id).toBe(`emit:assistant:heartbeat:ok:${today}`);
|
|
231
|
+
expect(heartbeatItems).toHaveLength(0);
|
|
239
232
|
});
|
|
240
233
|
});
|
|
@@ -15,6 +15,7 @@ import { getDb } from "../../memory/db-connection.js";
|
|
|
15
15
|
import { initializeDb } from "../../memory/db-init.js";
|
|
16
16
|
import {
|
|
17
17
|
completeHeartbeatRun,
|
|
18
|
+
countCompletedHeartbeatRuns,
|
|
18
19
|
insertPendingHeartbeatRun,
|
|
19
20
|
listHeartbeatRuns,
|
|
20
21
|
markStaleRunningAsError,
|
|
@@ -213,4 +214,39 @@ describe("heartbeat-run-store", () => {
|
|
|
213
214
|
const rows = listHeartbeatRuns(3);
|
|
214
215
|
expect(rows).toHaveLength(3);
|
|
215
216
|
});
|
|
217
|
+
|
|
218
|
+
test("countCompletedHeartbeatRuns counts only ok rows", () => {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
|
|
221
|
+
// Insert runs with various statuses
|
|
222
|
+
const id1 = insertPendingHeartbeatRun(now);
|
|
223
|
+
startHeartbeatRun(id1);
|
|
224
|
+
completeHeartbeatRun(id1, { status: "ok", conversationId: "conv-1" });
|
|
225
|
+
|
|
226
|
+
const id2 = insertPendingHeartbeatRun(now + 1);
|
|
227
|
+
startHeartbeatRun(id2);
|
|
228
|
+
completeHeartbeatRun(id2, { status: "error", error: "something broke" });
|
|
229
|
+
|
|
230
|
+
const id3 = insertPendingHeartbeatRun(now + 2);
|
|
231
|
+
skipHeartbeatRun(id3, "disabled");
|
|
232
|
+
|
|
233
|
+
const id4 = insertPendingHeartbeatRun(now + 3);
|
|
234
|
+
startHeartbeatRun(id4);
|
|
235
|
+
completeHeartbeatRun(id4, { status: "ok", conversationId: "conv-2" });
|
|
236
|
+
|
|
237
|
+
expect(countCompletedHeartbeatRuns()).toBe(2);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("countCompletedHeartbeatRuns returns 0 when no ok rows exist", () => {
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
|
|
243
|
+
const id1 = insertPendingHeartbeatRun(now);
|
|
244
|
+
startHeartbeatRun(id1);
|
|
245
|
+
completeHeartbeatRun(id1, { status: "error", error: "fail" });
|
|
246
|
+
|
|
247
|
+
const id2 = insertPendingHeartbeatRun(now + 1);
|
|
248
|
+
skipHeartbeatRun(id2, "outside_active_hours");
|
|
249
|
+
|
|
250
|
+
expect(countCompletedHeartbeatRuns()).toBe(0);
|
|
251
|
+
});
|
|
216
252
|
});
|
|
@@ -202,6 +202,19 @@ export function markStaleRunningAsError(
|
|
|
202
202
|
return rawChanges();
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Count the number of heartbeat runs that completed with status `ok`.
|
|
207
|
+
*/
|
|
208
|
+
export function countCompletedHeartbeatRuns(): number {
|
|
209
|
+
const db = getDb();
|
|
210
|
+
const row = db
|
|
211
|
+
.select({ count: sql<number>`count(*)` })
|
|
212
|
+
.from(heartbeatRuns)
|
|
213
|
+
.where(eq(heartbeatRuns.status, "ok"))
|
|
214
|
+
.get();
|
|
215
|
+
return row?.count ?? 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
/**
|
|
206
219
|
* List heartbeat runs ordered by `scheduledFor` descending.
|
|
207
220
|
*/
|
|
@@ -3,11 +3,16 @@ import { join } from "node:path";
|
|
|
3
3
|
|
|
4
4
|
import { getConfig } from "../config/loader.js";
|
|
5
5
|
import type { HeartbeatConfig } from "../config/schemas/heartbeat.js";
|
|
6
|
+
import {
|
|
7
|
+
checkDiskPressureBackgroundGate,
|
|
8
|
+
diskPressureBackgroundSkipLogFields,
|
|
9
|
+
shouldLogDiskPressureBackgroundSkip,
|
|
10
|
+
} from "../daemon/disk-pressure-background-gate.js";
|
|
6
11
|
import type { HeartbeatAlert } from "../daemon/message-protocol.js";
|
|
7
12
|
import { processMessage } from "../daemon/process-message.js";
|
|
8
13
|
import { emitFeedEvent } from "../home/emit-feed-event.js";
|
|
9
14
|
import { bootstrapConversation } from "../memory/conversation-bootstrap.js";
|
|
10
|
-
import { getConversation } from "../memory/conversation-crud.js";
|
|
15
|
+
import { getConversation, getMessages } from "../memory/conversation-crud.js";
|
|
11
16
|
import { GENERATING_TITLE } from "../memory/conversation-title-service.js";
|
|
12
17
|
import {
|
|
13
18
|
GUARDIAN_PERSONA_TEMPLATE,
|
|
@@ -21,6 +26,7 @@ import { getWorkspaceDir, getWorkspacePromptPath } from "../util/platform.js";
|
|
|
21
26
|
import { stripCommentLines } from "../util/strip-comment-lines.js";
|
|
22
27
|
import {
|
|
23
28
|
completeHeartbeatRun,
|
|
29
|
+
countCompletedHeartbeatRuns,
|
|
24
30
|
insertPendingHeartbeatRun,
|
|
25
31
|
markStaleRunningAsError,
|
|
26
32
|
markStaleRunsAsMissed,
|
|
@@ -38,8 +44,12 @@ const DEFAULT_CHECKLIST = `- Check in with yourself. Read NOW.md. Is it still ac
|
|
|
38
44
|
- If you have a thought worth sharing, send it. A follow-up, a useful find, a check-in. Not every beat, but when it feels right.
|
|
39
45
|
- If something has happened since your last journal entry, write one. Even a few sentences. The journal is how future-you stays connected.`;
|
|
40
46
|
|
|
47
|
+
const EARLY_HEARTBEAT_THRESHOLD = 3;
|
|
41
48
|
const REENGAGEMENT_COOLDOWN_MS = 18 * 60 * 60 * 1000; // 18 hours
|
|
42
49
|
const HEARTBEAT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
50
|
+
const HEARTBEAT_ALERT_MARKER = "HEARTBEAT_ALERT";
|
|
51
|
+
const HEARTBEAT_OK_MARKER = "HEARTBEAT_OK";
|
|
52
|
+
const HEARTBEAT_ALERT_SUMMARY_MAX_CHARS = 700;
|
|
43
53
|
|
|
44
54
|
// Stripped-comment form of the guardian persona scaffold. Computed
|
|
45
55
|
// once at module load because stripping comment lines is deterministic
|
|
@@ -92,6 +102,69 @@ function recordReengagementTimestamp(): void {
|
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
type HeartbeatDisposition = "alert" | "ok" | "unknown";
|
|
106
|
+
|
|
107
|
+
function parseHeartbeatDisposition(text: string | null): HeartbeatDisposition {
|
|
108
|
+
if (!text) return "unknown";
|
|
109
|
+
const lines = text
|
|
110
|
+
.trim()
|
|
111
|
+
.split(/\r?\n/)
|
|
112
|
+
.map((line) => line.trim())
|
|
113
|
+
.filter((line) => line.length > 0);
|
|
114
|
+
const lastLine = lines.at(-1);
|
|
115
|
+
if (lastLine === HEARTBEAT_ALERT_MARKER) return "alert";
|
|
116
|
+
if (lastLine === HEARTBEAT_OK_MARKER) return "ok";
|
|
117
|
+
return "unknown";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripHeartbeatDispositionMarkers(text: string): string {
|
|
121
|
+
return text
|
|
122
|
+
.replace(
|
|
123
|
+
new RegExp(
|
|
124
|
+
`(?:\\r?\\n)?\\s*(?:${HEARTBEAT_ALERT_MARKER}|${HEARTBEAT_OK_MARKER})\\s*$`,
|
|
125
|
+
),
|
|
126
|
+
"",
|
|
127
|
+
)
|
|
128
|
+
.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function truncateSummary(text: string, maxChars: number): string {
|
|
132
|
+
if (text.length <= maxChars) return text;
|
|
133
|
+
return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildHeartbeatAlertSummary(text: string | null): string {
|
|
137
|
+
const summary = text ? stripHeartbeatDispositionMarkers(text) : "";
|
|
138
|
+
return truncateSummary(
|
|
139
|
+
summary || "Your assistant found something worth your attention.",
|
|
140
|
+
HEARTBEAT_ALERT_SUMMARY_MAX_CHARS,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function extractVisibleTextFromStoredMessageContent(raw: string): string {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
147
|
+
if (typeof parsed === "string") return parsed;
|
|
148
|
+
if (!Array.isArray(parsed)) return "";
|
|
149
|
+
const texts: string[] = [];
|
|
150
|
+
for (const block of parsed) {
|
|
151
|
+
if (
|
|
152
|
+
block != null &&
|
|
153
|
+
typeof block === "object" &&
|
|
154
|
+
"type" in block &&
|
|
155
|
+
block.type === "text" &&
|
|
156
|
+
"text" in block &&
|
|
157
|
+
typeof block.text === "string"
|
|
158
|
+
) {
|
|
159
|
+
texts.push(block.text);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return texts.join("\n").trim();
|
|
163
|
+
} catch {
|
|
164
|
+
return raw;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
95
168
|
export interface HeartbeatDeps {
|
|
96
169
|
alerter: (alert: HeartbeatAlert) => void;
|
|
97
170
|
onConversationCreated?: (info: {
|
|
@@ -168,18 +241,22 @@ export class HeartbeatService {
|
|
|
168
241
|
"Recovered stale heartbeat runs on startup",
|
|
169
242
|
);
|
|
170
243
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
244
|
+
if (!isDiskPressureBackgroundLocked("heartbeat-startup")) {
|
|
245
|
+
const total = this._startupMissedCount + this._startupCrashedCount;
|
|
246
|
+
const today = new Date().toISOString().split("T")[0];
|
|
247
|
+
void emitFeedEvent({
|
|
248
|
+
source: "assistant",
|
|
249
|
+
title: "Heartbeat Runs Missed",
|
|
250
|
+
summary: `${total} heartbeat run${
|
|
251
|
+
total > 1 ? "s were" : " was"
|
|
252
|
+
} missed while the assistant was offline.`,
|
|
253
|
+
dedupKey: `heartbeat:missed:${today}`,
|
|
254
|
+
priority: 55,
|
|
255
|
+
urgency: "high",
|
|
256
|
+
}).catch((err) => {
|
|
257
|
+
log.warn({ err }, "Failed to emit missed heartbeat feed event");
|
|
258
|
+
});
|
|
259
|
+
}
|
|
183
260
|
}
|
|
184
261
|
}
|
|
185
262
|
|
|
@@ -328,6 +405,10 @@ export class HeartbeatService {
|
|
|
328
405
|
async runOnce({ force = false }: { force?: boolean } = {}): Promise<boolean> {
|
|
329
406
|
const config = getConfig().heartbeat;
|
|
330
407
|
|
|
408
|
+
if (!force && isDiskPressureBackgroundLocked("heartbeat")) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
331
412
|
let runId: string | null;
|
|
332
413
|
let scheduledFor: number;
|
|
333
414
|
if (force) {
|
|
@@ -579,6 +660,65 @@ export class HeartbeatService {
|
|
|
579
660
|
}
|
|
580
661
|
}
|
|
581
662
|
|
|
663
|
+
private getLatestAssistantMessage(
|
|
664
|
+
conversationId: string,
|
|
665
|
+
): { id: string; text: string } | null {
|
|
666
|
+
try {
|
|
667
|
+
const messages = getMessages(conversationId);
|
|
668
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
669
|
+
const message = messages[i]!;
|
|
670
|
+
if (message.role !== "assistant") continue;
|
|
671
|
+
return {
|
|
672
|
+
id: message.id,
|
|
673
|
+
text: extractVisibleTextFromStoredMessageContent(message.content),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
} catch (err) {
|
|
677
|
+
log.warn(
|
|
678
|
+
{ err, conversationId },
|
|
679
|
+
"Failed to read heartbeat assistant message",
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private async emitHeartbeatAlertNotification(params: {
|
|
686
|
+
runId: string;
|
|
687
|
+
conversationId: string;
|
|
688
|
+
messageId?: string;
|
|
689
|
+
conversationTitle: string;
|
|
690
|
+
summary: string;
|
|
691
|
+
}): Promise<void> {
|
|
692
|
+
const { emitNotificationSignal } =
|
|
693
|
+
await import("../notifications/emit-signal.js");
|
|
694
|
+
|
|
695
|
+
await emitNotificationSignal({
|
|
696
|
+
sourceEventName: "heartbeat.alert",
|
|
697
|
+
sourceChannel: "watcher",
|
|
698
|
+
sourceContextId: params.runId,
|
|
699
|
+
dedupeKey: `heartbeat:alert:${params.runId}`,
|
|
700
|
+
attentionHints: {
|
|
701
|
+
requiresAction: true,
|
|
702
|
+
urgency: "medium",
|
|
703
|
+
isAsyncBackground: true,
|
|
704
|
+
visibleInSourceNow: false,
|
|
705
|
+
},
|
|
706
|
+
contextPayload: {
|
|
707
|
+
title: "Heartbeat Alert",
|
|
708
|
+
summary: params.summary,
|
|
709
|
+
conversationTitle: params.conversationTitle,
|
|
710
|
+
conversationId: params.conversationId,
|
|
711
|
+
messageId: params.messageId,
|
|
712
|
+
},
|
|
713
|
+
routingIntent: "single_channel",
|
|
714
|
+
conversationAffinityHint: { vellum: params.conversationId },
|
|
715
|
+
conversationMetadata: {
|
|
716
|
+
source: "heartbeat",
|
|
717
|
+
groupId: "system:background",
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
582
722
|
private async executeRun(runId: string, scheduledFor: number): Promise<void> {
|
|
583
723
|
log.info("Running heartbeat");
|
|
584
724
|
|
|
@@ -595,9 +735,11 @@ export class HeartbeatService {
|
|
|
595
735
|
let conversationId: string | undefined;
|
|
596
736
|
try {
|
|
597
737
|
const checklist = this.readChecklist();
|
|
738
|
+
const completedRunCount = countCompletedHeartbeatRuns();
|
|
598
739
|
const { prompt, includedReengagement } = this.buildPrompt(
|
|
599
740
|
checklist,
|
|
600
741
|
unhealthyProviders,
|
|
742
|
+
completedRunCount,
|
|
601
743
|
);
|
|
602
744
|
|
|
603
745
|
const conversation = bootstrapConversation({
|
|
@@ -609,11 +751,6 @@ export class HeartbeatService {
|
|
|
609
751
|
});
|
|
610
752
|
conversationId = conversation.id;
|
|
611
753
|
|
|
612
|
-
this.deps.onConversationCreated?.({
|
|
613
|
-
conversationId: conversation.id,
|
|
614
|
-
title: "Heartbeat",
|
|
615
|
-
});
|
|
616
|
-
|
|
617
754
|
await processMessage(conversation.id, prompt, undefined, {
|
|
618
755
|
trustContext: {
|
|
619
756
|
sourceChannel: "vellum",
|
|
@@ -644,20 +781,32 @@ export class HeartbeatService {
|
|
|
644
781
|
// Best-effort; fall back to generic title.
|
|
645
782
|
}
|
|
646
783
|
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
784
|
+
const assistantMessage = this.getLatestAssistantMessage(
|
|
785
|
+
conversation.id,
|
|
786
|
+
);
|
|
787
|
+
const disposition = parseHeartbeatDisposition(
|
|
788
|
+
assistantMessage?.text ?? null,
|
|
789
|
+
);
|
|
790
|
+
if (disposition === "alert") {
|
|
791
|
+
this.deps.onConversationCreated?.({
|
|
792
|
+
conversationId: conversation.id,
|
|
793
|
+
title,
|
|
794
|
+
});
|
|
795
|
+
void this.emitHeartbeatAlertNotification({
|
|
796
|
+
runId,
|
|
797
|
+
conversationId: conversation.id,
|
|
798
|
+
messageId: assistantMessage?.id,
|
|
799
|
+
conversationTitle: title,
|
|
800
|
+
summary: buildHeartbeatAlertSummary(assistantMessage?.text ?? null),
|
|
801
|
+
}).catch((err) => {
|
|
802
|
+
log.warn(
|
|
803
|
+
{ err, conversationId: conversation.id },
|
|
804
|
+
"Failed to emit heartbeat alert notification",
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
660
808
|
|
|
809
|
+
const today = new Date().toISOString().split("T")[0];
|
|
661
810
|
if (latenessMs > LATE_THRESHOLD_MS) {
|
|
662
811
|
const lateMinutes = Math.round(latenessMs / 60_000);
|
|
663
812
|
void emitFeedEvent({
|
|
@@ -714,6 +863,7 @@ export class HeartbeatService {
|
|
|
714
863
|
buildPrompt(
|
|
715
864
|
checklist: string,
|
|
716
865
|
unhealthyProviders: string[] = [],
|
|
866
|
+
completedRunCount: number = Infinity,
|
|
717
867
|
): { prompt: string; includedReengagement: boolean } {
|
|
718
868
|
let prompt = `You are running a periodic heartbeat check. Review the following checklist and take any necessary actions.
|
|
719
869
|
|
|
@@ -730,11 +880,20 @@ Do NOT attempt to use tools for these providers — they will fail. Skip any che
|
|
|
730
880
|
}
|
|
731
881
|
|
|
732
882
|
prompt += `\n\n<heartbeat-disposition>
|
|
883
|
+
This heartbeat runs frequently. Do not manufacture a report just because it ran.
|
|
884
|
+
If there is nothing genuinely useful, actionable, or interesting to surface, keep the response brief and end with HEARTBEAT_OK.
|
|
885
|
+
If there is something worth interrupting the guardian for, write a concise guardian-facing note first: what happened, why it matters, and the recommended next step. Then end with HEARTBEAT_ALERT. That note may be used as notification copy.
|
|
733
886
|
After completing your review, end your response with one of:
|
|
734
887
|
- HEARTBEAT_OK — if everything looks good, no action needed
|
|
735
888
|
- HEARTBEAT_ALERT — if you found issues that need attention (describe them before this marker)
|
|
736
889
|
</heartbeat-disposition>`;
|
|
737
890
|
|
|
891
|
+
if (completedRunCount < EARLY_HEARTBEAT_THRESHOLD) {
|
|
892
|
+
prompt += `\n\n<early-heartbeat>
|
|
893
|
+
This is one of your first heartbeats. Your user hasn't heard from you yet and may not know you're here. Find something genuinely useful to share — a follow-up from a recent conversation, something you noticed, or a quick check-in. Lean toward HEARTBEAT_ALERT this time. First impressions matter.
|
|
894
|
+
</early-heartbeat>`;
|
|
895
|
+
}
|
|
896
|
+
|
|
738
897
|
let includedReengagement = false;
|
|
739
898
|
if (isShallowProfile() && isReengagementCooldownElapsed()) {
|
|
740
899
|
includedReengagement = true;
|
|
@@ -745,6 +904,21 @@ After completing your review, end your response with one of:
|
|
|
745
904
|
}
|
|
746
905
|
}
|
|
747
906
|
|
|
907
|
+
function isDiskPressureBackgroundLocked(logKey: string): boolean {
|
|
908
|
+
const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
|
|
909
|
+
if (diskPressureGate.action === "allow") return false;
|
|
910
|
+
if (shouldLogDiskPressureBackgroundSkip(logKey)) {
|
|
911
|
+
log.warn(
|
|
912
|
+
{
|
|
913
|
+
source: "heartbeat",
|
|
914
|
+
...diskPressureBackgroundSkipLogFields(diskPressureGate),
|
|
915
|
+
},
|
|
916
|
+
"Heartbeat skipped during disk pressure cleanup mode",
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
return true;
|
|
920
|
+
}
|
|
921
|
+
|
|
748
922
|
/**
|
|
749
923
|
* Check if the given hour falls within the active window.
|
|
750
924
|
* Handles overnight windows (e.g. start=22, end=6).
|
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
* scheduler needing to touch the event hub.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import {
|
|
32
|
+
checkDiskPressureBackgroundGate,
|
|
33
|
+
diskPressureBackgroundSkipLogFields,
|
|
34
|
+
shouldLogDiskPressureBackgroundSkip,
|
|
35
|
+
} from "../daemon/disk-pressure-background-gate.js";
|
|
31
36
|
import { getLogger } from "../util/logger.js";
|
|
32
37
|
import type { FeedItem } from "./feed-types.js";
|
|
33
38
|
import {
|
|
@@ -119,6 +124,19 @@ export function startFeedScheduler(
|
|
|
119
124
|
rollupRan: false,
|
|
120
125
|
};
|
|
121
126
|
if (stopped || tickRunning) return summary;
|
|
127
|
+
const diskPressureGate = checkDiskPressureBackgroundGate("background-work");
|
|
128
|
+
if (diskPressureGate.action === "skip") {
|
|
129
|
+
if (shouldLogDiskPressureBackgroundSkip("home-feed-scheduler")) {
|
|
130
|
+
log.warn(
|
|
131
|
+
{
|
|
132
|
+
source: "feed-scheduler",
|
|
133
|
+
...diskPressureBackgroundSkipLogFields(diskPressureGate),
|
|
134
|
+
},
|
|
135
|
+
"Home feed scheduler skipped during disk pressure cleanup mode",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return summary;
|
|
139
|
+
}
|
|
122
140
|
tickRunning = true;
|
|
123
141
|
const nowMs = now.getTime();
|
|
124
142
|
try {
|
|
@@ -16,11 +16,7 @@
|
|
|
16
16
|
* callback_url that external services should use.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
getPlatformAssistantId,
|
|
21
|
-
getPlatformBaseUrl,
|
|
22
|
-
getPlatformInternalApiKey,
|
|
23
|
-
} from "../config/env.js";
|
|
19
|
+
import { getPlatformAssistantId, getPlatformBaseUrl } from "../config/env.js";
|
|
24
20
|
import { getIsPlatform } from "../config/env-registry.js";
|
|
25
21
|
import { credentialKey } from "../security/credential-key.js";
|
|
26
22
|
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
@@ -32,7 +28,6 @@ export interface PlatformCallbackRegistrationContext {
|
|
|
32
28
|
isPlatform: boolean;
|
|
33
29
|
platformBaseUrl: string;
|
|
34
30
|
assistantId: string;
|
|
35
|
-
hasInternalApiKey: boolean;
|
|
36
31
|
hasAssistantApiKey: boolean;
|
|
37
32
|
authHeader: string | null;
|
|
38
33
|
enabled: boolean;
|
|
@@ -54,20 +49,18 @@ export async function resolvePlatformCallbackRegistrationContext(): Promise<Plat
|
|
|
54
49
|
);
|
|
55
50
|
const assistantId =
|
|
56
51
|
getPlatformAssistantId().trim() || storedAssistantIdRaw?.trim() || "";
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: null;
|
|
52
|
+
const envAssistantCredential = process.env.ASSISTANT_API_KEY?.trim();
|
|
53
|
+
const assistantCredential =
|
|
54
|
+
storedAssistantApiKeyRaw?.trim() || envAssistantCredential || undefined;
|
|
55
|
+
const authHeader = assistantCredential
|
|
56
|
+
? `Api-Key ${assistantCredential}`
|
|
57
|
+
: null;
|
|
64
58
|
|
|
65
59
|
return {
|
|
66
60
|
isPlatform: platform,
|
|
67
61
|
platformBaseUrl,
|
|
68
62
|
assistantId,
|
|
69
|
-
|
|
70
|
-
hasAssistantApiKey: assistantApiKey.length > 0,
|
|
63
|
+
hasAssistantApiKey: !!assistantCredential,
|
|
71
64
|
authHeader,
|
|
72
65
|
// Enabled when we have enough context to register callback routes.
|
|
73
66
|
// Does NOT require IS_PLATFORM — self-hosted assistants with stored
|