@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
//
|
|
17
17
|
// Skill entries are kept in a small in-process cache so the render path can
|
|
18
18
|
// fetch a `SkillEntry` synchronously by id without round-tripping to Qdrant.
|
|
19
|
-
// The cache is replaced atomically at the end of a
|
|
20
|
-
//
|
|
19
|
+
// The cache is replaced atomically from the local catalog at the end of a seed
|
|
20
|
+
// run — including when the dense embedding backend is unavailable, in which
|
|
21
|
+
// case only the dense Qdrant write is deferred so the cache (and the v3 needle
|
|
22
|
+
// lane it feeds) still reflects the current skills. An unexpected error in
|
|
23
|
+
// another step leaves the prior cache intact (skills are best-effort).
|
|
21
24
|
|
|
22
25
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
23
26
|
import { getConfig } from "../../config/loader.js";
|
|
@@ -228,26 +231,26 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
228
231
|
);
|
|
229
232
|
}
|
|
230
233
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
234
|
+
// Build the dense + sparse vectors for the Qdrant write. Sparse (BM25/TF)
|
|
235
|
+
// encoding is computed locally and needs no backend; only the dense vectors
|
|
236
|
+
// require `embedWithBackend`, which is unconfigured during the cold-start
|
|
237
|
+
// window before a managed-proxy embedding credential is provisioned.
|
|
238
|
+
//
|
|
239
|
+
// A dense-embed failure is non-fatal to the in-memory cache: the v3 needle
|
|
240
|
+
// finder lane and always-candidate skill pinning read skills from `entries`
|
|
241
|
+
// / the page index, NOT from Qdrant, so the cache is populated from the
|
|
242
|
+
// local catalog regardless of backend state and skills stay discoverable
|
|
243
|
+
// from first boot. Only the dense Qdrant upsert is skipped; the managed-
|
|
244
|
+
// credential reseed (`maybeReseedCapabilitiesAfterManagedCredential`) and
|
|
245
|
+
// the v3 maintain pass backfill the dense vectors once the backend recovers.
|
|
235
246
|
const nextEntries = new Map<string, SkillEntry>();
|
|
236
247
|
let denseVectors: number[][] = [];
|
|
248
|
+
let denseAvailable = false;
|
|
249
|
+
let denseError: unknown = null;
|
|
237
250
|
let encodeSparse: (
|
|
238
251
|
input: string,
|
|
239
252
|
) => ReturnType<typeof generateSparseEmbedding> = generateSparseEmbedding;
|
|
240
253
|
if (seeds.length > 0) {
|
|
241
|
-
const embedded = await embedWithBackend(
|
|
242
|
-
config,
|
|
243
|
-
seeds.map((s) => s.content),
|
|
244
|
-
);
|
|
245
|
-
denseVectors = await Promise.all(
|
|
246
|
-
embedded.vectors.map((v) =>
|
|
247
|
-
applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
|
|
251
254
|
// Skills share the concept-page Qdrant collection, so the sparse vector
|
|
252
255
|
// must use the same stemmed BM25 encoding the concept-page documents
|
|
253
256
|
// carry — otherwise the stemmed BM25 query vectors used by callers (see
|
|
@@ -263,6 +266,24 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
263
266
|
b: config.memory.v2.bm25_b,
|
|
264
267
|
})
|
|
265
268
|
: generateSparseEmbedding(input);
|
|
269
|
+
try {
|
|
270
|
+
const embedded = await embedWithBackend(
|
|
271
|
+
config,
|
|
272
|
+
seeds.map((s) => s.content),
|
|
273
|
+
);
|
|
274
|
+
denseVectors = await Promise.all(
|
|
275
|
+
embedded.vectors.map((v) =>
|
|
276
|
+
applyCorrectionIfCalibrated(v, embedded.provider, embedded.model),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
denseAvailable = true;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
denseError = err;
|
|
282
|
+
log.warn(
|
|
283
|
+
{ err },
|
|
284
|
+
"Embedding backend unavailable — seeding skill cache without dense Qdrant vectors; the needle lane surfaces skills from the cache and the dense lane backfills when the backend recovers",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
if (generation !== requestedSeedGeneration) {
|
|
@@ -274,7 +295,17 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
274
295
|
return;
|
|
275
296
|
}
|
|
276
297
|
|
|
277
|
-
|
|
298
|
+
// Populate the in-memory cache (and therefore the page index / needle lane)
|
|
299
|
+
// from the local catalog regardless of dense availability.
|
|
300
|
+
for (const seed of seeds) {
|
|
301
|
+
nextEntries.set(seed.id, seed);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Write Qdrant points only when dense vectors were produced. In the
|
|
305
|
+
// degraded (backend-unavailable) path we skip Qdrant mutation entirely —
|
|
306
|
+
// both the upsert and the prune below — so we never write half-formed
|
|
307
|
+
// points or reconcile the collection against a set we did not persist.
|
|
308
|
+
if (seeds.length > 0 && denseAvailable) {
|
|
278
309
|
const now = Date.now();
|
|
279
310
|
await Promise.all(
|
|
280
311
|
seeds.map((seed, i) =>
|
|
@@ -287,16 +318,15 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
287
318
|
}),
|
|
288
319
|
),
|
|
289
320
|
);
|
|
290
|
-
for (const seed of seeds) {
|
|
291
|
-
nextEntries.set(seed.id, seed);
|
|
292
|
-
}
|
|
293
321
|
}
|
|
294
322
|
|
|
295
|
-
// Prune stale skill slugs.
|
|
296
|
-
// from network failure or cold cache
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
|
|
323
|
+
// Prune stale skill slugs. Skip when the catalog is unavailable (empty array
|
|
324
|
+
// from network failure or cold cache — we cannot enumerate which uninstalled
|
|
325
|
+
// catalog skills should exist) OR when dense vectors were not written this
|
|
326
|
+
// run (don't reconcile Qdrant against points we did not refresh). The
|
|
327
|
+
// `seeds.length === 0` branch still prunes under an available catalog so the
|
|
328
|
+
// all-disabled case clears stale rows.
|
|
329
|
+
if (catalogAvailable && (denseAvailable || seeds.length === 0)) {
|
|
300
330
|
// Tag legacy skill points missing `payload.kind` before pruning so the
|
|
301
331
|
// kind-scoped prune can see them. Once-per-process; the backfill is
|
|
302
332
|
// idempotent (server-side `is_empty` filter), so a partial failure
|
|
@@ -321,19 +351,26 @@ async function runSeedV2SkillEntries(generation: number): Promise<void> {
|
|
|
321
351
|
seeds.map((s) => s.id),
|
|
322
352
|
{ kind: SKILL_PAYLOAD_KIND },
|
|
323
353
|
);
|
|
324
|
-
} else {
|
|
354
|
+
} else if (!catalogAvailable) {
|
|
325
355
|
log.info(
|
|
326
356
|
"Catalog unavailable — skipping skill pruning to preserve prior catalog embeddings",
|
|
327
357
|
);
|
|
328
358
|
}
|
|
329
359
|
|
|
330
|
-
// Atomically replace the cache
|
|
331
|
-
|
|
332
|
-
//
|
|
333
|
-
//
|
|
360
|
+
// Atomically replace the cache from the freshly enumerated skills. The local
|
|
361
|
+
// resolution (`resolveSkillStates`) is authoritative, so a skill the config
|
|
362
|
+
// just disabled or removed drops out here even when the remote catalog is
|
|
363
|
+
// unavailable. Drop the page-index cache so the next router invocation
|
|
364
|
+
// observes the new skill set (skill entries share the unified concept-page
|
|
334
365
|
// collection and surface in the same index).
|
|
366
|
+
entries = nextEntries;
|
|
335
367
|
invalidatePageIndex();
|
|
336
|
-
|
|
368
|
+
|
|
369
|
+
// Surface a dense-embed failure to `throwOnError` callers (the managed-
|
|
370
|
+
// credential reseed and the operator reembed route) so the existing retry +
|
|
371
|
+
// maintain machinery backfills the dense lane. The in-memory cache is
|
|
372
|
+
// already updated above, so the needle lane is fixed regardless.
|
|
373
|
+
lastSeedError = denseError;
|
|
337
374
|
} catch (err) {
|
|
338
375
|
lastSeedError = err;
|
|
339
376
|
log.warn({ err }, "Failed to seed v2 skill entries");
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { and, asc, eq, gt, or } from "drizzle-orm";
|
|
2
|
+
import { v4 as uuid } from "uuid";
|
|
3
|
+
|
|
4
|
+
import { getCachedShareAnalytics } from "../platform/consent-cache.js";
|
|
5
|
+
import { getTelemetryDb } from "./db-connection.js";
|
|
6
|
+
import { watchdogEvents } from "./schema.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input for one `watchdog` telemetry event. Metadata only — never conversation
|
|
10
|
+
* content. `value` and `detail` are optional; omitting both yields the minimal
|
|
11
|
+
* event (check_name only). `detail` is a JSON bag serialized to text on persist
|
|
12
|
+
* and forwarded verbatim on flush.
|
|
13
|
+
*/
|
|
14
|
+
export interface WatchdogEventRecord {
|
|
15
|
+
checkName: string;
|
|
16
|
+
/** Measured magnitude (block ms, idle ms, ...). Null when the check carries no scalar. */
|
|
17
|
+
value?: number | null;
|
|
18
|
+
/** Open JSON bag for extra fields (reason codes, secondary numbers, ...). */
|
|
19
|
+
detail?: Record<string, unknown> | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A persisted watchdog event row. */
|
|
23
|
+
export interface WatchdogEvent {
|
|
24
|
+
id: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
checkName: string;
|
|
27
|
+
value: number | null;
|
|
28
|
+
/** Raw `detail` JSON text from the row, or null. Parsed by the reporter on flush. */
|
|
29
|
+
detail: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Record a `watchdog` telemetry event for a watchdog check firing. No-ops when
|
|
34
|
+
* usage data collection is disabled (the event is dropped to honor the opt-out,
|
|
35
|
+
* matching the rest of telemetry) — so opt-out rows never exist and the
|
|
36
|
+
* reporter's standard 0 watermark default is safe.
|
|
37
|
+
*/
|
|
38
|
+
export function recordWatchdogEvent(record: WatchdogEventRecord): void {
|
|
39
|
+
if (!getCachedShareAnalytics()) return;
|
|
40
|
+
const db = getTelemetryDb();
|
|
41
|
+
if (!db) return;
|
|
42
|
+
db.insert(watchdogEvents)
|
|
43
|
+
.values({
|
|
44
|
+
id: uuid(),
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
checkName: record.checkName,
|
|
47
|
+
value: record.value ?? null,
|
|
48
|
+
detail: record.detail != null ? JSON.stringify(record.detail) : null,
|
|
49
|
+
})
|
|
50
|
+
.run();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Query watchdog events that haven't been reported to telemetry yet.
|
|
55
|
+
* Uses a compound cursor (createdAt + id) for reliable watermarking.
|
|
56
|
+
*/
|
|
57
|
+
export function queryUnreportedWatchdogEvents(
|
|
58
|
+
afterCreatedAt: number,
|
|
59
|
+
afterId: string | undefined,
|
|
60
|
+
limit: number,
|
|
61
|
+
): WatchdogEvent[] {
|
|
62
|
+
const db = getTelemetryDb();
|
|
63
|
+
if (!db) return [];
|
|
64
|
+
return db
|
|
65
|
+
.select({
|
|
66
|
+
id: watchdogEvents.id,
|
|
67
|
+
createdAt: watchdogEvents.createdAt,
|
|
68
|
+
checkName: watchdogEvents.checkName,
|
|
69
|
+
value: watchdogEvents.value,
|
|
70
|
+
detail: watchdogEvents.detail,
|
|
71
|
+
})
|
|
72
|
+
.from(watchdogEvents)
|
|
73
|
+
.where(
|
|
74
|
+
afterId
|
|
75
|
+
? or(
|
|
76
|
+
gt(watchdogEvents.createdAt, afterCreatedAt),
|
|
77
|
+
and(
|
|
78
|
+
eq(watchdogEvents.createdAt, afterCreatedAt),
|
|
79
|
+
gt(watchdogEvents.id, afterId),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
: gt(watchdogEvents.createdAt, afterCreatedAt),
|
|
83
|
+
)
|
|
84
|
+
.orderBy(asc(watchdogEvents.createdAt), asc(watchdogEvents.id))
|
|
85
|
+
.limit(limit)
|
|
86
|
+
.all();
|
|
87
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared control surface for the memory jobs worker *process* — the detached
|
|
3
|
+
* OS process whose entry point is `worker-process.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Both the `assistant memory worker` CLI and the daemon lifecycle (when
|
|
6
|
+
* `memory.worker.enabled` is set) need to probe, spawn, and stop this process,
|
|
7
|
+
* so the PID-file bookkeeping lives here in one place.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
11
|
+
|
|
12
|
+
import { getMemoryWorkerPidPath } from "../util/platform.js";
|
|
13
|
+
|
|
14
|
+
export interface MemoryWorkerStatus {
|
|
15
|
+
status: "running" | "not_running";
|
|
16
|
+
pid?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Inspect the PID file to determine whether the worker process is alive.
|
|
21
|
+
* A stale PID file (pointing at a dead process) is cleaned up and reported
|
|
22
|
+
* as not_running.
|
|
23
|
+
*/
|
|
24
|
+
export function probeMemoryWorker(): MemoryWorkerStatus {
|
|
25
|
+
const pidPath = getMemoryWorkerPidPath();
|
|
26
|
+
if (!existsSync(pidPath)) return { status: "not_running" };
|
|
27
|
+
|
|
28
|
+
const raw = readFileSync(pidPath, "utf-8").trim();
|
|
29
|
+
const pid = parseInt(raw, 10);
|
|
30
|
+
if (!Number.isFinite(pid) || pid <= 0) return { status: "not_running" };
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
process.kill(pid, 0);
|
|
34
|
+
return { status: "running", pid };
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
if (
|
|
37
|
+
err &&
|
|
38
|
+
typeof err === "object" &&
|
|
39
|
+
"code" in err &&
|
|
40
|
+
err.code === "ESRCH"
|
|
41
|
+
) {
|
|
42
|
+
// Stale PID file — clean it up.
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(pidPath);
|
|
45
|
+
} catch {
|
|
46
|
+
// best-effort
|
|
47
|
+
}
|
|
48
|
+
return { status: "not_running" };
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class MemoryWorkerSpawnError extends Error {}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Spawn the memory worker as a detached background process.
|
|
58
|
+
*
|
|
59
|
+
* If a worker is already running, returns its PID with `alreadyRunning: true`
|
|
60
|
+
* rather than spawning a second one. Throws {@link MemoryWorkerSpawnError} if
|
|
61
|
+
* the child is spawned but never writes its PID file (i.e. failed to start).
|
|
62
|
+
*/
|
|
63
|
+
export async function spawnMemoryWorkerProcess(): Promise<{
|
|
64
|
+
pid: number;
|
|
65
|
+
alreadyRunning: boolean;
|
|
66
|
+
}> {
|
|
67
|
+
const current = probeMemoryWorker();
|
|
68
|
+
if (current.status === "running" && current.pid != null) {
|
|
69
|
+
return { pid: current.pid, alreadyRunning: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pidPath = getMemoryWorkerPidPath();
|
|
73
|
+
const entry = new URL("./worker-process.ts", import.meta.url);
|
|
74
|
+
|
|
75
|
+
// Spawn detached so the worker survives the spawning process exiting.
|
|
76
|
+
const child = Bun.spawn({
|
|
77
|
+
cmd: ["bun", "run", entry.pathname],
|
|
78
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
79
|
+
detached: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Unreference so the spawning process doesn't wait for the child.
|
|
83
|
+
child.unref();
|
|
84
|
+
|
|
85
|
+
// Wait briefly for the PID file to appear (the worker writes it on startup).
|
|
86
|
+
let pidWritten = false;
|
|
87
|
+
for (let i = 0; i < 10; i++) {
|
|
88
|
+
await Bun.sleep(100);
|
|
89
|
+
if (existsSync(pidPath)) {
|
|
90
|
+
pidWritten = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!pidWritten) {
|
|
96
|
+
throw new MemoryWorkerSpawnError(
|
|
97
|
+
"Memory worker was spawned but PID file did not appear within 1s",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
102
|
+
return { pid, alreadyRunning: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Send SIGTERM to the worker process if it is actually running.
|
|
107
|
+
*
|
|
108
|
+
* Returns the status observed before signalling, so callers can report
|
|
109
|
+
* whether anything was stopped. Only throws if `process.kill` itself fails
|
|
110
|
+
* (e.g. EPERM) — a not-running worker is a no-op.
|
|
111
|
+
*/
|
|
112
|
+
export function stopMemoryWorkerProcess(): MemoryWorkerStatus {
|
|
113
|
+
const current = probeMemoryWorker();
|
|
114
|
+
if (current.status === "running" && current.pid != null) {
|
|
115
|
+
process.kill(current.pid, "SIGTERM");
|
|
116
|
+
}
|
|
117
|
+
return current;
|
|
118
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone entry point for the memory jobs worker as its own OS process.
|
|
3
|
+
*
|
|
4
|
+
* Spawned by `assistant memory worker start`. Loads config, starts the
|
|
5
|
+
* worker loop, writes a PID file, and stays alive until SIGTERM/SIGINT.
|
|
6
|
+
*
|
|
7
|
+
* The worker's internal `setTimeout` calls `.unref()`, which is correct
|
|
8
|
+
* inside the daemon (don't keep the daemon alive for the worker) but would
|
|
9
|
+
* cause this standalone process to exit immediately. A ref'd keep-alive
|
|
10
|
+
* interval prevents that.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, unlinkSync, writeFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
import { getConfig } from "../config/loader.js";
|
|
16
|
+
import { getLogger } from "../util/logger.js";
|
|
17
|
+
import { getMemoryWorkerPidPath } from "../util/platform.js";
|
|
18
|
+
import { startInProcessMemoryJobsWorker } from "./jobs-worker.js";
|
|
19
|
+
|
|
20
|
+
const log = getLogger("memory-worker-process");
|
|
21
|
+
|
|
22
|
+
function cleanupPidFile(): void {
|
|
23
|
+
const pidPath = getMemoryWorkerPidPath();
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(pidPath)) unlinkSync(pidPath);
|
|
26
|
+
} catch {
|
|
27
|
+
// best-effort
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function main(): Promise<void> {
|
|
32
|
+
const config = getConfig();
|
|
33
|
+
const pidPath = getMemoryWorkerPidPath();
|
|
34
|
+
|
|
35
|
+
if (config.memory.enabled === false) {
|
|
36
|
+
log.info("Memory is disabled in config; worker process exiting");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Write PID file so `status` and `stop` can find us.
|
|
41
|
+
writeFileSync(pidPath, String(process.pid), { flag: "w" });
|
|
42
|
+
log.info({ pid: process.pid, pidPath }, "Memory worker process started");
|
|
43
|
+
|
|
44
|
+
const worker = startInProcessMemoryJobsWorker();
|
|
45
|
+
|
|
46
|
+
// Keep-alive: the worker's setTimeout timers are unref'd, so without
|
|
47
|
+
// this interval the process would exit immediately.
|
|
48
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
49
|
+
|
|
50
|
+
const shutdown = (signal: string) => {
|
|
51
|
+
log.info({ signal }, "Memory worker process shutting down");
|
|
52
|
+
worker.stop();
|
|
53
|
+
clearInterval(keepAlive);
|
|
54
|
+
cleanupPidFile();
|
|
55
|
+
process.exit(0);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
59
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
60
|
+
|
|
61
|
+
// Clean up if the process exits unexpectedly through any other path.
|
|
62
|
+
process.on("exit", () => {
|
|
63
|
+
worker.stop();
|
|
64
|
+
cleanupPidFile();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void main().catch((err) => {
|
|
69
|
+
log.error({ err }, "Memory worker process failed to start");
|
|
70
|
+
cleanupPidFile();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
@@ -27,14 +27,22 @@ mock.module("../copy-composer.js", () => ({
|
|
|
27
27
|
composeFallbackCopy: () => composeFallbackReturn,
|
|
28
28
|
}));
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
// Stub only getGuardianDelivery; keep the real selectors so this mock is
|
|
31
|
+
// harmless if it leaks into destination-resolver.test.ts under a shared run.
|
|
32
|
+
const realGuardianReader = await import(
|
|
33
|
+
"../../contacts/guardian-delivery-reader.js"
|
|
34
|
+
);
|
|
35
|
+
mock.module("../../contacts/guardian-delivery-reader.js", () => ({
|
|
36
|
+
...realGuardianReader,
|
|
37
|
+
getGuardianDelivery: async () => null,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Use the real destination-resolver (DB-free via the local-read stub below)
|
|
41
|
+
// so this mock does not leak into destination-resolver.test.ts under a shared
|
|
42
|
+
// bun-test invocation. With no guardian, the resolver still yields a vellum
|
|
43
|
+
// destination, which is all these tests exercise.
|
|
44
|
+
mock.module("../../contacts/contact-store.js", () => ({
|
|
45
|
+
findGuardianForChannel: () => null,
|
|
38
46
|
}));
|
|
39
47
|
|
|
40
48
|
mock.module("../conversation-pairing.js", () => ({
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for getConnectedChannels connectivity resolution.
|
|
3
|
+
*
|
|
4
|
+
* Connectivity must mirror destination-resolver's `resolveGuardian`:
|
|
5
|
+
* gateway-first, with a LOCAL contacts fallback on ANY per-channel no-match
|
|
6
|
+
* (gateway list null OR no active gateway entry for the channel). This keeps a
|
|
7
|
+
* channel from being marked connected when it can't be delivered (and
|
|
8
|
+
* vice-versa).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
14
|
+
|
|
15
|
+
mock.module("../../util/logger.js", () => ({
|
|
16
|
+
getLogger: () =>
|
|
17
|
+
new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
|
|
18
|
+
truncateForLog: (value: string) => value,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let deliverableChannels: string[] = [];
|
|
22
|
+
let gatewayGuardians: GuardianDelivery[] | null = null;
|
|
23
|
+
let localChatId: string | null = null;
|
|
24
|
+
|
|
25
|
+
const realConfig = await import("../../channels/config.js");
|
|
26
|
+
|
|
27
|
+
mock.module("../../channels/config.js", () => ({
|
|
28
|
+
...realConfig,
|
|
29
|
+
getDeliverableChannels: () => deliverableChannels,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const realReader = await import("../../contacts/guardian-delivery-reader.js");
|
|
33
|
+
|
|
34
|
+
mock.module("../../contacts/guardian-delivery-reader.js", () => ({
|
|
35
|
+
...realReader,
|
|
36
|
+
getGuardianDelivery: async () => gatewayGuardians,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const realContactStore = await import("../../contacts/contact-store.js");
|
|
40
|
+
|
|
41
|
+
mock.module("../../contacts/contact-store.js", () => ({
|
|
42
|
+
...realContactStore,
|
|
43
|
+
findGuardianForChannel: (_channelType: string) =>
|
|
44
|
+
localChatId === null
|
|
45
|
+
? null
|
|
46
|
+
: { contact: { principalId: "p1" }, channel: { externalChatId: localChatId } },
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const { getConnectedChannels } = await import("../emit-signal.js");
|
|
50
|
+
|
|
51
|
+
function gatewayBinding(channelType: string, externalChatId: string): GuardianDelivery {
|
|
52
|
+
return { channelType, contactId: "c1", address: "addr", externalChatId, status: "active" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
deliverableChannels = [];
|
|
57
|
+
gatewayGuardians = null;
|
|
58
|
+
localChatId = null;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("getConnectedChannels gateway-first-then-local connectivity", () => {
|
|
62
|
+
test("marks telegram connected from a gateway-only binding", async () => {
|
|
63
|
+
deliverableChannels = ["telegram"];
|
|
64
|
+
gatewayGuardians = [gatewayBinding("telegram", "123")];
|
|
65
|
+
localChatId = null;
|
|
66
|
+
|
|
67
|
+
expect(await getConnectedChannels()).toContain("telegram");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("falls back to a local binding when the gateway is unreachable (null)", async () => {
|
|
71
|
+
deliverableChannels = ["telegram"];
|
|
72
|
+
gatewayGuardians = null;
|
|
73
|
+
localChatId = "456";
|
|
74
|
+
|
|
75
|
+
expect(await getConnectedChannels()).toContain("telegram");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("marks telegram disconnected when neither source has a binding", async () => {
|
|
79
|
+
deliverableChannels = ["telegram"];
|
|
80
|
+
gatewayGuardians = null;
|
|
81
|
+
localChatId = null;
|
|
82
|
+
|
|
83
|
+
expect(await getConnectedChannels()).not.toContain("telegram");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("falls back to local when the gateway responds without that channel", async () => {
|
|
87
|
+
// Gateway present but with no active telegram entry ⇒ per-channel no-match,
|
|
88
|
+
// so connectivity falls back to the local mirror (mirrors
|
|
89
|
+
// destination-resolver's per-channel fallback).
|
|
90
|
+
deliverableChannels = ["telegram"];
|
|
91
|
+
gatewayGuardians = [];
|
|
92
|
+
localChatId = "789";
|
|
93
|
+
|
|
94
|
+
expect(await getConnectedChannels()).toContain("telegram");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("only marks slack connected for D-prefixed (DM) chat IDs", async () => {
|
|
98
|
+
deliverableChannels = ["slack"];
|
|
99
|
+
gatewayGuardians = [gatewayBinding("slack", "C-public")];
|
|
100
|
+
expect(await getConnectedChannels()).not.toContain("slack");
|
|
101
|
+
|
|
102
|
+
gatewayGuardians = [gatewayBinding("slack", "D-dm")];
|
|
103
|
+
expect(await getConnectedChannels()).toContain("slack");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("always reports vellum and platform connected", async () => {
|
|
107
|
+
deliverableChannels = ["vellum", "platform"];
|
|
108
|
+
gatewayGuardians = null;
|
|
109
|
+
|
|
110
|
+
const connected = await getConnectedChannels();
|
|
111
|
+
expect(connected).toContain("vellum");
|
|
112
|
+
expect(connected).toContain("platform");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -34,19 +34,38 @@ mock.module("../../prompts/system-prompt.js", () => ({
|
|
|
34
34
|
buildCoreIdentityContext: () => null,
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
+
// Guardian binding (ACL) is resolved via the gateway pull; notes (INFO) are
|
|
38
|
+
// joined locally by contactId. Tests drive both via mutable slots.
|
|
39
|
+
let guardianDeliveryFixture: Array<{ contactId: string }> = [];
|
|
40
|
+
let contactInfoFixture: Record<string, { notes: string | null } | null> = {};
|
|
41
|
+
|
|
42
|
+
mock.module("../../contacts/guardian-delivery-reader.js", () => ({
|
|
43
|
+
getGuardianDelivery: async () => guardianDeliveryFixture,
|
|
44
|
+
anyGuardian: (list: Array<{ contactId: string }>) => list[0],
|
|
45
|
+
}));
|
|
46
|
+
|
|
37
47
|
mock.module("../../contacts/contact-store.js", () => ({
|
|
38
|
-
|
|
48
|
+
findContactInfoById: (contactId: string) =>
|
|
49
|
+
contactInfoFixture[contactId] ?? null,
|
|
39
50
|
}));
|
|
40
51
|
|
|
41
|
-
// Provider mock
|
|
42
|
-
//
|
|
43
|
-
//
|
|
52
|
+
// Provider mock. By default `sendMessage` throws so the assistant_tool
|
|
53
|
+
// pass-through path (which must skip the LLM) fails loudly if it reaches the
|
|
54
|
+
// provider. LLM-path tests override `providerSendMessage` to capture inputs.
|
|
55
|
+
let providerSendMessage: (
|
|
56
|
+
messages: unknown[],
|
|
57
|
+
opts: { systemPrompt?: string },
|
|
58
|
+
) => Promise<unknown> = () => {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"provider.sendMessage should NOT be invoked for assistant_tool pass-through",
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
mock.module("../../providers/provider-send-message.js", () => ({
|
|
45
|
-
getConfiguredProvider: async () => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
},
|
|
65
|
+
getConfiguredProvider: async () => ({
|
|
66
|
+
sendMessage: (messages: unknown[], opts: { systemPrompt?: string }) =>
|
|
67
|
+
providerSendMessage(messages, opts),
|
|
68
|
+
}),
|
|
50
69
|
createTimeout: () => ({
|
|
51
70
|
signal: new AbortController().signal,
|
|
52
71
|
cleanup: () => {},
|
|
@@ -281,3 +300,53 @@ describe("assistant_tool pass-through in notification decision engine", () => {
|
|
|
281
300
|
expect(decision.renderedCopy.vellum?.body).toBe("fyi");
|
|
282
301
|
});
|
|
283
302
|
});
|
|
303
|
+
|
|
304
|
+
describe("recipient notes injection (ACL from gateway, notes joined locally)", () => {
|
|
305
|
+
function makeLlmSignal(): NotificationSignal {
|
|
306
|
+
return {
|
|
307
|
+
signalId: "sig-llm-notes-1",
|
|
308
|
+
createdAt: Date.now(),
|
|
309
|
+
sourceChannel: "scheduler",
|
|
310
|
+
sourceContextId: "schedule-1",
|
|
311
|
+
sourceEventName: "schedule.notify",
|
|
312
|
+
contextPayload: {},
|
|
313
|
+
attentionHints: {
|
|
314
|
+
requiresAction: false,
|
|
315
|
+
urgency: "low",
|
|
316
|
+
isAsyncBackground: false,
|
|
317
|
+
visibleInSourceNow: false,
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
test("injects the guardian's local notes, resolved via the gateway contactId", async () => {
|
|
323
|
+
guardianDeliveryFixture = [{ contactId: "contact-42" }];
|
|
324
|
+
contactInfoFixture = { "contact-42": { notes: "Prefers terse updates." } };
|
|
325
|
+
|
|
326
|
+
let capturedSystemPrompt: string | undefined;
|
|
327
|
+
providerSendMessage = async (_messages, opts) => {
|
|
328
|
+
capturedSystemPrompt = opts.systemPrompt;
|
|
329
|
+
return {};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
await evaluateSignal(makeLlmSignal(), ["vellum"] as NotificationChannel[]);
|
|
333
|
+
|
|
334
|
+
expect(capturedSystemPrompt).toContain("<recipient-context>");
|
|
335
|
+
expect(capturedSystemPrompt).toContain("Prefers terse updates.");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("omits recipient context when no guardian is bound", async () => {
|
|
339
|
+
guardianDeliveryFixture = [];
|
|
340
|
+
contactInfoFixture = {};
|
|
341
|
+
|
|
342
|
+
let capturedSystemPrompt: string | undefined;
|
|
343
|
+
providerSendMessage = async (_messages, opts) => {
|
|
344
|
+
capturedSystemPrompt = opts.systemPrompt;
|
|
345
|
+
return {};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
await evaluateSignal(makeLlmSignal(), ["vellum"] as NotificationChannel[]);
|
|
349
|
+
|
|
350
|
+
expect(capturedSystemPrompt).not.toContain("<recipient-context>");
|
|
351
|
+
});
|
|
352
|
+
});
|